sojorn/sojorn_app/lib/utils/url_launcher_helper.dart

181 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../theme/app_theme.dart';
/// Helper class for safely launching URLs with user warnings for unknown sites
class UrlLauncherHelper {
// List of known safe domains
static const List<String> _safeDomains = [
'mp.ls', 'www.mp.ls', 'patrick.mp.ls'
'sojorn.net', 'www.sojorn.net'
'youtube.com', 'www.youtube.com', 'youtu.be',
'instagram.com', 'www.instagram.com',
'twitter.com', 'www.twitter.com', 'x.com', 'www.x.com',
'facebook.com', 'www.facebook.com',
'tiktok.com', 'www.tiktok.com',
'linkedin.com', 'www.linkedin.com',
'github.com', 'www.github.com',
'twitch.tv', 'www.twitch.tv',
'reddit.com', 'www.reddit.com',
'medium.com', 'www.medium.com',
'substack.com',
'patreon.com', 'www.patreon.com',
'discord.com', 'www.discord.com', 'discord.gg',
'spotify.com', 'www.spotify.com',
'pinterest.com', 'www.pinterest.com',
'snapchat.com', 'www.snapchat.com',
'telegram.org', 'www.telegram.org', 't.me',
];
/// Check if a URL is from a known safe domain
static bool isKnownSafeDomain(String url) {
try {
final uri = Uri.parse(url.startsWith('http') ? url : 'https://$url');
final host = uri.host.toLowerCase();
return _safeDomains.any((domain) =>
host == domain || host.endsWith('.$domain')
);
} catch (e) {
return false;
}
}
/// Safely launch a URL with user confirmation for unknown sites
static Future<void> launchUrlSafely(
BuildContext context,
String url, {
bool forceWarning = false,
}) async {
// Validate URL scheme to prevent shell escaping
try {
final uri = Uri.parse(url.startsWith('http') ? url : 'https://$url');
if (!['http:', 'https:'].contains(uri.scheme)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid URL scheme. Only HTTP and HTTPS URLs are allowed.'),
backgroundColor: AppTheme.error,
),
);
return;
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid URL format.'),
backgroundColor: AppTheme.error,
),
);
return;
}
final isSafe = isKnownSafeDomain(url);
// Show warning dialog for unknown sites
if (!isSafe || forceWarning) {
final shouldLaunch = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('External Link'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'You are about to visit an external website:',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.egyptianBlue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
url,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 16),
if (!isSafe) ...[
Row(
children: [
Icon(
Icons.warning_amber,
color: Colors.orange,
size: 20,
),
const SizedBox(width: 8),
const Expanded(
child: Text(
'This site is not recognized as a known safe website. Proceed with caution.',
style: TextStyle(
fontSize: 13,
color: Colors.orange,
),
),
),
],
),
const SizedBox(height: 8),
],
const Text(
'Always be careful when visiting external links and never share your password or personal information.',
style: TextStyle(fontSize: 13),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: isSafe ? AppTheme.royalPurple : Colors.orange,
),
child: const Text('Continue'),
),
],
),
);
if (shouldLaunch != true) return;
}
// Launch the URL
try {
final uri = Uri.parse(url.startsWith('http') ? url : 'https://$url');
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not open link: $url'),
backgroundColor: AppTheme.error,
),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error opening link: ${e.toString()}'),
backgroundColor: AppTheme.error,
),
);
}
}
}
}