From c635da552d89692d85029ab6fa843555c867f4c4 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Wed, 4 Feb 2026 17:42:37 -0600 Subject: [PATCH] Implement robust notification deep linking - Create SecureChatLoaderScreen for linking to conversations by ID - Add /secure-chat/:id route to AppRoutes - Update NotificationService to use AppRoutes.router for all navigation - Fix Follow and Post navigation routes in NotificationService - Decouple notification handling from manual Navigator pushes --- sojorn_app/lib/routes/app_routes.dart | 10 +++ .../secure_chat_loader_screen.dart | 84 +++++++++++++++++++ .../lib/services/notification_service.dart | 67 ++++++++------- 3 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 sojorn_app/lib/screens/secure_chat/secure_chat_loader_screen.dart diff --git a/sojorn_app/lib/routes/app_routes.dart b/sojorn_app/lib/routes/app_routes.dart index 22b0ac6..5fa32b5 100644 --- a/sojorn_app/lib/routes/app_routes.dart +++ b/sojorn_app/lib/routes/app_routes.dart @@ -19,6 +19,7 @@ import '../screens/profile/blocked_users_screen.dart'; import '../screens/auth/auth_gate.dart'; import '../screens/discover/discover_screen.dart'; import '../screens/secure_chat/secure_chat_full_screen.dart'; +import '../screens/secure_chat/secure_chat_loader_screen.dart'; import '../screens/post/threaded_conversation_screen.dart'; /// App routing config (GoRouter). @@ -65,6 +66,15 @@ class AppRoutes { path: secureChat, parentNavigatorKey: rootNavigatorKey, builder: (_, __) => const SecureChatFullScreen(), + routes: [ + GoRoute( + path: ':id', + parentNavigatorKey: rootNavigatorKey, + builder: (_, state) => SecureChatLoaderScreen( + conversationId: state.pathParameters['id'] ?? '', + ), + ), + ], ), GoRoute( path: '$postPrefix/:id', diff --git a/sojorn_app/lib/screens/secure_chat/secure_chat_loader_screen.dart b/sojorn_app/lib/screens/secure_chat/secure_chat_loader_screen.dart new file mode 100644 index 0000000..27acbdf --- /dev/null +++ b/sojorn_app/lib/screens/secure_chat/secure_chat_loader_screen.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import '../../services/secure_chat_service.dart'; +import '../../theme/app_theme.dart'; +import 'secure_chat_screen.dart'; + +/// Loading wrapper to fetch conversation data before showing chat screen +class SecureChatLoaderScreen extends StatefulWidget { + final String conversationId; + + const SecureChatLoaderScreen({ + super.key, + required this.conversationId, + }); + + @override + State createState() => _SecureChatLoaderScreenState(); +} + +class _SecureChatLoaderScreenState extends State { + String? _error; + + @override + void initState() { + super.initState(); + _loadConversation(); + } + + Future _loadConversation() async { + try { + final conversation = await SecureChatService.instance + .getConversationById(widget.conversationId); + + if (!mounted) return; + + if (conversation != null) { + // Replace this loader with the actual chat screen + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => SecureChatScreen(conversation: conversation), + ), + ); + } else { + setState(() { + _error = 'Conversation not found'; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Failed to load conversation: $e'; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_error != null) { + return Scaffold( + appBar: AppBar(title: const Text('Error')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error!, style: TextStyle(color: AppTheme.error)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Go Back'), + ), + ], + ), + ), + ); + } + + return Scaffold( + backgroundColor: AppTheme.scaffoldBg, + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/sojorn_app/lib/services/notification_service.dart b/sojorn_app/lib/services/notification_service.dart index 365a3f8..e770669 100644 --- a/sojorn_app/lib/services/notification_service.dart +++ b/sojorn_app/lib/services/notification_service.dart @@ -10,7 +10,6 @@ import 'package:permission_handler/permission_handler.dart'; import '../config/firebase_web_config.dart'; import '../routes/app_routes.dart'; import '../services/secure_chat_service.dart'; -import '../screens/secure_chat/secure_chat_screen.dart'; import 'api_service.dart'; /// NotificationPreferences model @@ -465,15 +464,13 @@ class NotificationService { Future _handleMessageOpen(RemoteMessage message) async { final data = message.data; + // Try to get type from data, fallback to notification title parsing if needed final type = data['type'] as String?; debugPrint('[FCM] Handling message open - type: $type, data: $data'); - final navigator = AppRoutes.rootNavigatorKey.currentState; - if (navigator == null) { - debugPrint('[FCM] Navigator not available'); - return; - } + // Use the router directly for reliability + final router = AppRoutes.router; switch (type) { case 'chat': @@ -482,6 +479,8 @@ class NotificationService { final conversationId = data['conversation_id']; if (conversationId != null) { await _openConversation(conversationId.toString()); + } else { + router.go(AppRoutes.secureChat); } break; @@ -495,7 +494,7 @@ class NotificationService { final postId = data['post_id'] ?? data['beacon_id']; final target = data['target']; if (postId != null) { - _navigateToPost(navigator, postId.toString(), target?.toString()); + _navigateToPost(postId.toString(), target?.toString()); } break; @@ -505,9 +504,9 @@ class NotificationService { case 'follow_accepted': final followerId = data['follower_id']; if (followerId != null) { - navigator.context.push('${AppRoutes.userPrefix}/$followerId'); + router.push('${AppRoutes.userPrefix}/$followerId'); } else { - navigator.context.go(AppRoutes.profile); + router.go(AppRoutes.profile); } break; @@ -515,50 +514,60 @@ class NotificationService { case 'beacon_report': final beaconId = data['beacon_id'] ?? data['post_id']; if (beaconId != null) { - _navigateToPost(navigator, beaconId.toString(), 'beacon_map'); + _navigateToPost(beaconId.toString(), 'beacon_map'); } else { - navigator.context.go(AppRoutes.beaconPrefix); + router.go(AppRoutes.beaconPrefix); } break; default: debugPrint('[FCM] Unknown notification type: $type'); + // Retrieve generic target if available + final target = data['target']; + if (target != null) { + _handleGenericTarget(target.toString()); + } break; } } - void _navigateToPost(NavigatorState navigator, String postId, String? target) { + void _navigateToPost(String postId, String? target) { + final router = AppRoutes.router; switch (target) { case 'beacon_map': - navigator.context.go(AppRoutes.beaconPrefix); + router.go(AppRoutes.beaconPrefix); break; case 'quip_feed': - navigator.context.go(AppRoutes.quips); + router.go(AppRoutes.quips); break; case 'thread_view': case 'main_feed': default: - navigator.context.push('${AppRoutes.postPrefix}/$postId'); + router.push('${AppRoutes.postPrefix}/$postId'); break; } } - Future _openConversation(String conversationId) async { - final conversation = - await SecureChatService.instance.getConversationById(conversationId); - if (conversation == null) { - debugPrint('[FCM] Conversation not found: $conversationId'); - return; + void _handleGenericTarget(String target) { + final router = AppRoutes.router; + switch (target) { + case 'secure_chat': + router.go(AppRoutes.secureChat); + break; + case 'profile': + router.go(AppRoutes.profile); + break; + case 'beacon_map': + router.go(AppRoutes.beaconPrefix); + break; + case 'quip_feed': + router.go(AppRoutes.quips); + break; } + } - final navigator = AppRoutes.rootNavigatorKey.currentState; - if (navigator == null) return; - - navigator.push( - MaterialPageRoute( - builder: (_) => SecureChatScreen(conversation: conversation), - ), - ); + void _openConversation(String conversationId) { + AppRoutes.router.push('${AppRoutes.secureChat}/$conversationId'); } void dispose() {