11import 'package:flutter/material.dart' ;
2- import 'package:flutter/services.dart' show rootBundle, Clipboard, ClipboardData;
2+ import 'package:flutter/services.dart'
3+ show rootBundle, Clipboard, ClipboardData;
34import 'package:flutter_markdown/flutter_markdown.dart' ;
45import 'package:url_launcher/url_launcher.dart' ;
56
@@ -26,9 +27,9 @@ class LegalSupportSections extends StatelessWidget {
2627 required this .appName,
2728 required this .version,
2829 this .contactEmail = 'contact@files-tech.com' ,
29- this .websiteUrl = 'https://files-tech.com' ,
30+ this .websiteUrl = 'https://files-tech.com' ,
3031 this .privacyAsset = 'assets/legal/PRIVACY.fr.md' ,
31- this .termsAsset = 'assets/legal/TERMS.fr.md' ,
32+ this .termsAsset = 'assets/legal/TERMS.fr.md' ,
3233 });
3334
3435 /// Schemes autorisés pour `_openUrl` et les liens Markdown.
@@ -51,68 +52,85 @@ class LegalSupportSections extends StatelessWidget {
5152 _section (context, 'Aide & support' ),
5253 const SizedBox (height: 8 ),
5354 Card (
54- child: Column (children: [
55- ListTile (
56- leading: Icon (Icons .email_outlined, color: cs.primary),
57- title: const Text ('Contacter le support' ),
58- subtitle: Text (contactEmail),
59- trailing: const Icon (Icons .open_in_new, size: 16 ),
60- onTap: () => _openMail (context, contactEmail,
61- '$appName v$version — support' ),
62- ),
63- const Divider (height: 1 ),
64- ListTile (
65- leading: Icon (Icons .public, color: cs.primary),
66- title: const Text ('Site officiel' ),
67- subtitle: Text (_displayHost (websiteUrl)),
68- trailing: const Icon (Icons .open_in_new, size: 16 ),
69- onTap: () => _openUrl (context, websiteUrl),
70- ),
71- const Divider (height: 1 ),
72- ListTile (
73- leading: Icon (Icons .bug_report_outlined, color: cs.primary),
74- title: const Text ('Signaler un bug' ),
75- subtitle: const Text ('Email avec version pré-remplie' ),
76- onTap: () => _openMail (context, contactEmail,
55+ child: Column (
56+ children: [
57+ ListTile (
58+ leading: Icon (Icons .email_outlined, color: cs.primary),
59+ title: const Text ('Contacter le support' ),
60+ subtitle: Text (contactEmail),
61+ trailing: const Icon (Icons .open_in_new, size: 16 ),
62+ onTap: () => _openMail (
63+ context,
64+ contactEmail,
65+ '$appName v$version — support' ,
66+ ),
67+ ),
68+ const Divider (height: 1 ),
69+ ListTile (
70+ leading: Icon (Icons .public, color: cs.primary),
71+ title: const Text ('Site officiel' ),
72+ subtitle: Text (_displayHost (websiteUrl)),
73+ trailing: const Icon (Icons .open_in_new, size: 16 ),
74+ onTap: () => _openUrl (context, websiteUrl),
75+ ),
76+ const Divider (height: 1 ),
77+ ListTile (
78+ leading: Icon (Icons .bug_report_outlined, color: cs.primary),
79+ title: const Text ('Signaler un bug' ),
80+ subtitle: const Text ('Email avec version pré-remplie' ),
81+ onTap: () => _openMail (
82+ context,
83+ contactEmail,
7784 '$appName v$version — bug' ,
78- body: 'Décrivez le problème rencontré :\n\n\n '
79- '— Version : $version \n — Appareil : ' ),
80- ),
81- ]),
85+ body:
86+ 'Décrivez le problème rencontré :\n\n\n '
87+ '— Version : $version \n — Appareil : ' ,
88+ ),
89+ ),
90+ ],
91+ ),
8292 ),
8393
8494 const SizedBox (height: 24 ),
8595
8696 _section (context, 'Mentions légales' ),
8797 const SizedBox (height: 8 ),
8898 Card (
89- child: Column (children: [
90- ListTile (
91- leading: Icon (Icons .privacy_tip_outlined, color: cs.primary),
92- title: const Text ('Politique de confidentialité' ),
93- trailing: const Icon (Icons .chevron_right),
94- onTap: () => _openLegal (context,
99+ child: Column (
100+ children: [
101+ ListTile (
102+ leading: Icon (Icons .privacy_tip_outlined, color: cs.primary),
103+ title: const Text ('Politique de confidentialité' ),
104+ trailing: const Icon (Icons .chevron_right),
105+ onTap: () => _openLegal (
106+ context,
95107 title: 'Politique de confidentialité' ,
96- asset: privacyAsset),
97- ),
98- const Divider (height: 1 ),
99- ListTile (
100- leading: Icon (Icons .gavel_outlined, color: cs.primary),
101- title: const Text ('Conditions d\' utilisation' ),
102- trailing: const Icon (Icons .chevron_right),
103- onTap: () => _openLegal (context,
108+ asset: privacyAsset,
109+ ),
110+ ),
111+ const Divider (height: 1 ),
112+ ListTile (
113+ leading: Icon (Icons .gavel_outlined, color: cs.primary),
114+ title: const Text ('Conditions d\' utilisation' ),
115+ trailing: const Icon (Icons .chevron_right),
116+ onTap: () => _openLegal (
117+ context,
104118 title: 'Conditions d\' utilisation' ,
105- asset: termsAsset),
106- ),
107- const Divider (height: 1 ),
108- ListTile (
109- leading: Icon (Icons .copyright_outlined, color: cs.primary),
110- title: const Text ('Licence' ),
111- subtitle: const Text ('Apache 2.0' ),
112- onTap: () => _openUrl (context,
113- 'https://www.apache.org/licenses/LICENSE-2.0' ),
114- ),
115- ]),
119+ asset: termsAsset,
120+ ),
121+ ),
122+ const Divider (height: 1 ),
123+ ListTile (
124+ leading: Icon (Icons .copyright_outlined, color: cs.primary),
125+ title: const Text ('Licence' ),
126+ subtitle: const Text ('Apache 2.0' ),
127+ onTap: () => _openUrl (
128+ context,
129+ 'https://www.apache.org/licenses/LICENSE-2.0' ,
130+ ),
131+ ),
132+ ],
133+ ),
116134 ),
117135 const SizedBox (height: 16 ),
118136 Center (
@@ -133,42 +151,57 @@ class LegalSupportSections extends StatelessWidget {
133151 Widget _section (BuildContext context, String title) {
134152 return Padding (
135153 padding: const EdgeInsets .only (left: 2 ),
136- child: Text (title,
137- style: Theme .of (context).textTheme.titleSmall? .copyWith (
138- color: Colors .grey.shade600,
139- fontWeight: FontWeight .w700,
140- letterSpacing: 0.5 )),
154+ child: Text (
155+ title,
156+ style: Theme .of (context).textTheme.titleSmall? .copyWith (
157+ color: Colors .grey.shade600,
158+ fontWeight: FontWeight .w700,
159+ letterSpacing: 0.5 ,
160+ ),
161+ ),
141162 );
142163 }
143164
144165 Future <void > _openUrl (BuildContext context, String url) async {
145166 final messenger = ScaffoldMessenger .of (context);
146167 final uri = Uri .tryParse (url);
147168 if (uri == null || ! _isSafeUri (uri)) {
148- messenger.showSnackBar (const SnackBar (
149- content: Text ('Lien refusé pour des raisons de sécurité.' ),
150- ));
169+ messenger.showSnackBar (
170+ const SnackBar (
171+ content: Text ('Lien refusé pour des raisons de sécurité.' ),
172+ ),
173+ );
151174 return ;
152175 }
176+ // On NE FAIT PLUS de canLaunchUrl() au préalable : ce check est connu
177+ // pour échouer même quand launchUrl() fonctionne (bug url_launcher
178+ // Android — voir https://github.com/flutter/flutter/issues/93765).
179+ // launchUrl jette PlatformException si le scheme n'a aucun handler →
180+ // on capture proprement et on affiche le bon message.
153181 try {
154- if (await canLaunchUrl (uri)) {
155- await launchUrl (uri, mode: LaunchMode .externalApplication);
156- } else {
182+ final ok = await launchUrl (uri, mode: LaunchMode .externalApplication);
183+ if (! ok) {
157184 messenger.showSnackBar (
158- SnackBar (content: Text ('Impossible d\' ouvrir : $url ' )));
185+ SnackBar (content: Text ('Impossible d\' ouvrir : $url ' )),
186+ );
159187 }
160188 } catch (_) {
161- messenger.showSnackBar (const SnackBar (
162- content: Text ('Erreur d\' ouverture du lien.' )));
189+ messenger.showSnackBar (
190+ const SnackBar (content: Text ('Aucune application disponible.' )),
191+ );
163192 }
164193 }
165194
166- Future <void > _openMail (BuildContext context, String to, String subject,
167- {String ? body}) async {
195+ Future <void > _openMail (
196+ BuildContext context,
197+ String to,
198+ String subject, {
199+ String ? body,
200+ }) async {
168201 final messenger = ScaffoldMessenger .of (context);
169202 // Strip CRLF dans les headers (anti mailto header injection).
170203 final safeSubject = subject.replaceAll (RegExp (r'[\r\n]' ), ' ' );
171- final safeBody = body? .replaceAll (RegExp (r'\r\n?' ), '\n ' );
204+ final safeBody = body? .replaceAll (RegExp (r'\r\n?' ), '\n ' );
172205 final uri = Uri (
173206 scheme: 'mailto' ,
174207 path: to,
@@ -177,20 +210,25 @@ class LegalSupportSections extends StatelessWidget {
177210 'body' : ? safeBody,
178211 },
179212 );
213+ // Idem _openUrl : on n'utilise plus canLaunchUrl (faux négatifs).
214+ // Si launchUrl jette ou retourne false, fallback clipboard.
180215 try {
181- if ( await canLaunchUrl (uri)) {
182- await launchUrl (uri) ;
183- return ;
184- }
185- } catch (_) { /* fall through */ }
216+ final ok = await launchUrl (uri);
217+ if (ok) return ;
218+ } catch (_) {
219+ /* fall through */
220+ }
186221 await Clipboard .setData (ClipboardData (text: to));
187- messenger.showSnackBar (SnackBar (
188- content: Text ('Aucune app mail. Adresse copiée : $to ' ),
189- )) ;
222+ messenger.showSnackBar (
223+ SnackBar ( content: Text ('Aucune app mail. Adresse copiée : $to ' ) ),
224+ );
190225 }
191226
192- void _openLegal (BuildContext context,
193- {required String title, required String asset}) {
227+ void _openLegal (
228+ BuildContext context, {
229+ required String title,
230+ required String asset,
231+ }) {
194232 Navigator .push <void >(
195233 context,
196234 MaterialPageRoute <void >(
@@ -231,11 +269,12 @@ class _LegalScreen extends StatelessWidget {
231269 if (uri == null ) return ;
232270 if (! _allowedSchemes.contains (uri.scheme.toLowerCase ())) return ;
233271 if (uri.userInfo.isNotEmpty) return ;
272+ // Idem _openUrl : pas de canLaunchUrl (faux négatifs Android 11+).
234273 try {
235- if ( await canLaunchUrl (uri)) {
236- await launchUrl (uri, mode : LaunchMode .externalApplication);
237- }
238- } catch (_) { /* silent */ }
274+ await launchUrl (uri, mode : LaunchMode .externalApplication);
275+ } catch (_) {
276+ /* silent */
277+ }
239278 },
240279 );
241280 },
0 commit comments