Skip to content

Commit f97ffa2

Browse files
gitubpatriceclaude
andcommitted
fix(legal): retire canLaunchUrl (faux negatifs Android 11+) -> launchUrl direct
Bug rapporte : le lien Apache 2.0 dans 'A propos' affichait 'Impossible d ouvrir' au tap sur les 4 apps Files Tech, MEME apres avoir ajoute les queries https/mailto au manifest. Cause : canLaunchUrl() est connu pour retourner false meme quand launchUrl() fonctionne (flutter/flutter#93765) suite a une logique de package visibility plus stricte sur Android 11+. Fix : suppression du pre-check canLaunchUrl dans 3 sites de legal_support_sections.dart : - _openUrl (Apache 2.0, site web) - _openMail (contact email) - onTapLink Markdown (PRIVACY/TERMS) On appelle launchUrl directement, et on capture PlatformException avec fallback (snackbar / clipboard pour mail). Aucune regression UX : l erreur est toujours affichee proprement si vraiment aucun handler. Et le cas nominal (handler present) marche. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c99df45 commit f97ffa2

1 file changed

Lines changed: 125 additions & 86 deletions

File tree

lib/src/legal/legal_support_sections.dart

Lines changed: 125 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import '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;
34
import 'package:flutter_markdown/flutter_markdown.dart';
45
import '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

Comments
 (0)