import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../widgets/safety_redirect_sheet.dart'; /// External Link Traffic Controller /// /// Provides safe URL routing with a domain whitelist. /// Only domains in the whitelist are allowed without safety warnings. /// All other domains will show a confirmation sheet before opening. /// /// This is a "whitelist-only" approach: everything not explicitly trusted /// requires user confirmation. class ExternalLinkController { /// Whitelist of safe domains that open without any warning /// These are trusted sources with established editorial standards static const List _whitelist = [ // sojorn & Core Platforms 'sojorn.com', 'youtube.com', 'youtu.be', 'wikipedia.org', 'wikimedia.org', 'github.com', 'instagram.com', 'linkedin.com', 'medium.com', 'reddit.com', 'vimeo.com', 'spotify.com', 'apple.com', 'google.com', 'maps.google.com', 'openstreetmap.org', // Established Progressive/Left Sources 'msnow.com', 'huffpost.com', 'jacobin.com', 'wsws.org', 'counterpunch.org', 'truthout.org', 'commondreams.org', 'theintercept.com', 'leftvoice.org', 'liberationnews.org', 'inthesetimes.com', 'monthlyreview.org', 'currentaffairs.org', 'alternet.org', 'rawstory.com', 'therealnews.com', 'popularresistance.org', 'blackagendareport.com', 'socialistworker.org', 'peoplesworld.org', 'marxists.org', 'roarmag.org', 'versobooks.com', 'tribunemag.co.uk', 'novaramedia.com', 'opendemocracy.net', 'redpepper.org.uk', 'thenation.com', 'dissentmagazine.org', 'democracynow.org', 'fifthestate.org', 'crimethinc.com', ]; /// Handles URL routing with whitelist safety checks. /// /// [context] - BuildContext for showing dialogs/sheets /// [url] - The URL to open /// /// Flow: /// 1. Parse the URL and extract the host (domain) /// 2. Check if host ends with any whitelisted domain (case-insensitive) /// 3. If whitelisted: launch immediately /// 4. If not whitelisted: show SafetyRedirectSheet for user confirmation static Future handleUrl(BuildContext context, String url) async { if (url.trim().isEmpty) return; final Uri? uri = Uri.tryParse(url); if (uri == null) return; // Extract domain from URL final String host = uri.host.toLowerCase(); // Check if domain is in whitelist if (_isWhitelisted(host)) { await _launchUrl(context, uri); } else { _showSafetyRedirectSheet(context, uri); } } /// Check if the domain is in the whitelist static bool _isWhitelisted(String host) { return _whitelist.any((domain) => host.endsWith(domain)); } /// Launch URL using url_launcher static Future _launchUrl(BuildContext context, Uri uri) async { try { if (await canLaunchUrl(uri)) { await launchUrl( uri, mode: LaunchMode.externalApplication, ); } else { _showError(context, 'Could not open link.'); } } catch (e) { _showError(context, 'Error opening link: $e'); } } /// Show the Safety Redirect Sheet for non-whitelisted domains static void _showSafetyRedirectSheet(BuildContext context, Uri uri) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => SafetyRedirectSheet( url: uri.toString(), domain: uri.host, ), ); } /// Show error snackbar static void _showError(BuildContext context, String message) { final messenger = ScaffoldMessenger.of(context); messenger.showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red.shade700, ), ); } /// Add a domain to the whitelist at runtime static void addToWhitelist(String domain) { final normalizedDomain = domain.toLowerCase(); if (!_whitelist.contains(normalizedDomain)) { _whitelist.add(normalizedDomain); } } /// Check if a domain is currently whitelisted static bool isWhitelisted(String domain) { return _isWhitelisted(domain.toLowerCase()); } /// Get all whitelisted domains (for debugging/admin purposes) static List getWhitelist() { return List.unmodifiable(_whitelist); } }