···
1
+
import 'package:flutter/foundation.dart';
2
+
import 'package:flutter/material.dart';
3
+
import 'package:url_launcher/url_launcher.dart';
5
+
/// Utility class for safely launching external URLs
7
+
/// Provides security validation and error handling for opening URLs
8
+
/// in external browsers or applications.
10
+
UrlLauncher._(); // Private constructor to prevent instantiation
12
+
/// Allowed URL schemes for security
13
+
static const _allowedSchemes = ['http', 'https'];
15
+
/// Launches an external URL with security validation
17
+
/// Returns true if the URL was successfully launched, false otherwise.
20
+
/// - Only allows http and https schemes
21
+
/// - Blocks potentially malicious schemes (javascript:, file:, etc.)
22
+
/// - Opens in external browser for user control
24
+
/// If [context] is provided and mounted, shows a user-friendly error message
25
+
/// when the URL cannot be opened.
26
+
static Future<bool> launchExternalUrl(
28
+
BuildContext? context,
31
+
final uri = Uri.parse(url);
33
+
// Validate URL scheme for security
34
+
if (!_allowedSchemes.contains(uri.scheme.toLowerCase())) {
36
+
debugPrint('Blocked non-http(s) URL scheme: ${uri.scheme}');
38
+
_showErrorIfPossible(context, 'Invalid link format');
42
+
// Check if URL can be launched
43
+
if (await canLaunchUrl(uri)) {
44
+
return await launchUrl(uri, mode: LaunchMode.externalApplication);
48
+
debugPrint('Could not launch URL: $url');
50
+
// ignore: use_build_context_synchronously
51
+
_showErrorIfPossible(context, 'Could not open link');
53
+
} on FormatException catch (e) {
55
+
debugPrint('Invalid URL format: $url - $e');
57
+
// ignore: use_build_context_synchronously
58
+
_showErrorIfPossible(context, 'Invalid link format');
60
+
} on Exception catch (e) {
62
+
debugPrint('Error launching URL: $url - $e');
64
+
// ignore: use_build_context_synchronously
65
+
_showErrorIfPossible(context, 'Could not open link');
70
+
/// Shows an error snackbar if context is available and mounted
71
+
static void _showErrorIfPossible(BuildContext? context, String message) {
72
+
if (context != null && context.mounted) {
73
+
ScaffoldMessenger.of(
75
+
).showSnackBar(SnackBar(content: Text(message)));