class DeeplinkService { static final DeeplinkService _instance = DeeplinkService._internal(); factory DeeplinkService() => _instance; DeeplinkService._internal(); final AppLinks _appLinks = AppLinks(); StreamSubscription? _subscription; bool _isInitialized = false; static _QueuedLink? _queuedDeeplink; // only one at a time String? _lastHandledDeeplink; DateTime? _lastHandledTime; void initialize() { if (_isInitialized) return; _listenForAppLinks(); _initializeOneSignalListeners(); _isInitialized = true; } /// ----------------------------- /// App Links (URL scheme / universal links) /// ----------------------------- void _listenForAppLinks() { _handleInitialLink(); // first launch _subscription = _appLinks.uriLinkStream.listen( (uri) => _handleWebDeeplink(uri.toString()), onError: (err) => debugPrint('Deeplink error: $err'), ); } Future _handleInitialLink() async { try { final uri = await _appLinks.getInitialAppLink(); if (uri != null) _handleWebDeeplink(uri.toString()); } catch (e) { // debugPrint('Error getting initial link: $e'); } } /// ----------------------------- /// Push Notifications /// ----------------------------- void _initializeOneSignalListeners() { OneSignal.Notifications.addClickListener(_handleNotificationClicked); } void _handleNotificationClicked(OSNotificationClickEvent event) { // debugPrint( // 'Notification clicked: ${event.notification.jsonRepresentation()}', // ); _processNotificationDeeplink(event.notification); } void _processNotificationDeeplink(OSNotification notification) { try { final deeplink = notification.additionalData?['deeplink'] as String? ?? notification.launchUrl; if (deeplink != null) { _handleOneSignalDeeplink(deeplink); } else { // debugPrint('No deeplink found in notification'); } } catch (e) { // debugPrint('Error processing notification deeplink: $e'); } } /// ----------------------------- /// Main Deep Link Handler /// ----------------------------- void _handleDeeplink(String link, {required String source}) { if (_lastHandledDeeplink == link && _lastHandledTime != null && DateTime.now().difference(_lastHandledTime!) < const Duration(seconds: 2)) { // debugPrint('Duplicate deeplink ignored: $link from $source'); return; } _lastHandledDeeplink = link; _lastHandledTime = DateTime.now(); // debugPrint('[$source] Received deeplink: $link'); try { final uri = Uri.parse(link); final path = uri.path.isNotEmpty ? uri.path : "/${uri.host}"; final params = uri.queryParameters; if (source == 'onesignal' && uri.scheme == 'cheda') { _navigateByPath(path, params); } else if (source == 'applinks' && uri.scheme.startsWith('https')) { _navigateByPath(path, params); } else { debugPrint('Source mismatch or unsupported link: $link'); } } catch (e) { // debugPrint('Error parsing deeplink: $e'); } } void _handleOneSignalDeeplink(String link) { _handleDeeplink(link, source: 'onesignal'); } void _handleWebDeeplink(String link) { _handleDeeplink(link, source: 'applinks'); } /// Single navigation decision table void _navigateByPath(String path, Map? params) { final context = Routes.navKey.currentContext; if (context == null) return; switch (path) { case '/verify-identity': _checkAuthAndNavigate(context, Routes.verifyIdentity, params); break; case '/home': _checkAuthAndNavigate(context, Routes.home, params); break; case '/bank-accounts': _checkAuthAndNavigate(context, Routes.bankAccounts, params); break; case '/profile': _checkAuthAndNavigate(context, Routes.profile, params); break; case '/assets': _checkAuthAndNavigate(context, Routes.home, params, tabIndex: 1); break; case '/history': _checkAuthAndNavigate(context, Routes.home, params, tabIndex: 2); break; default: debugPrint('Unknown deeplink path: $path'); } } /// ----------------------------- /// Authentication + Queuing /// ----------------------------- void _checkAuthAndNavigate( BuildContext context, String route, Map? params, { int? tabIndex, }) { try { final authBloc = context.read(); if (authBloc.isSignedIn) { _performNavigation(context, route, params, tabIndex: tabIndex); } else { _queueDeeplink(route, params, tabIndex: tabIndex); Navigator.of( context, ).pushNamedAndRemoveUntil(Routes.login, (_) => false); // debugPrint('User not authenticated, deeplink queued: $route'); } } catch (e) { // debugPrint('Auth check failed: $e'); _performNavigation(context, route, params, tabIndex: tabIndex); } } void _performNavigation( BuildContext context, String route, Map? params, { int? tabIndex, }) { if (route == Routes.home) { Navigator.of(context).pushNamedAndRemoveUntil(route, (_) => false); if (tabIndex != null) { Future.delayed(const Duration(milliseconds: 500), () { final currentContext = Routes.navKey.currentContext; if (currentContext != null && currentContext.mounted) { TabNavigationService().switchToTab(tabIndex); } }); } } else if ([Routes.bankAccounts, Routes.profile].contains(route)) { Navigator.of(context).pushNamedAndRemoveUntil(Routes.home, (_) => false); Future.delayed(const Duration(milliseconds: 500), () { final ctx = Routes.navKey.currentContext; if (ctx != null && ctx.mounted) Navigator.of(ctx).pushNamed(route); }); } else { Navigator.of(context).pushNamedAndRemoveUntil(route, (_) => false); } } void _queueDeeplink( String route, Map? params, { int? tabIndex, }) { _queuedDeeplink = _QueuedLink(route, params, tabIndex); debugPrint('Deeplink queued: $route'); } static void processQueuedDeeplinks(BuildContext context) { if (_queuedDeeplink == null) return; final queued = _queuedDeeplink!; _queuedDeeplink = null; // clear immediately DeeplinkService()._checkAuthAndNavigate( context, queued.route, queued.params, tabIndex: queued.tabIndex, ); } void dispose() { _subscription?.cancel(); _isInitialized = false; } } class _QueuedLink { final String route; final Map? params; final int? tabIndex; _QueuedLink(this.route, this.params, this.tabIndex); }