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
This commit is contained in:
Patrick Britton 2026-02-04 17:42:37 -06:00
parent 69358b016f
commit c635da552d
3 changed files with 132 additions and 29 deletions

View file

@ -19,6 +19,7 @@ import '../screens/profile/blocked_users_screen.dart';
import '../screens/auth/auth_gate.dart'; import '../screens/auth/auth_gate.dart';
import '../screens/discover/discover_screen.dart'; import '../screens/discover/discover_screen.dart';
import '../screens/secure_chat/secure_chat_full_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'; import '../screens/post/threaded_conversation_screen.dart';
/// App routing config (GoRouter). /// App routing config (GoRouter).
@ -65,6 +66,15 @@ class AppRoutes {
path: secureChat, path: secureChat,
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const SecureChatFullScreen(), builder: (_, __) => const SecureChatFullScreen(),
routes: [
GoRoute(
path: ':id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
),
),
],
), ),
GoRoute( GoRoute(
path: '$postPrefix/:id', path: '$postPrefix/:id',

View file

@ -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<SecureChatLoaderScreen> createState() => _SecureChatLoaderScreenState();
}
class _SecureChatLoaderScreenState extends State<SecureChatLoaderScreen> {
String? _error;
@override
void initState() {
super.initState();
_loadConversation();
}
Future<void> _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(),
),
);
}
}

View file

@ -10,7 +10,6 @@ import 'package:permission_handler/permission_handler.dart';
import '../config/firebase_web_config.dart'; import '../config/firebase_web_config.dart';
import '../routes/app_routes.dart'; import '../routes/app_routes.dart';
import '../services/secure_chat_service.dart'; import '../services/secure_chat_service.dart';
import '../screens/secure_chat/secure_chat_screen.dart';
import 'api_service.dart'; import 'api_service.dart';
/// NotificationPreferences model /// NotificationPreferences model
@ -465,15 +464,13 @@ class NotificationService {
Future<void> _handleMessageOpen(RemoteMessage message) async { Future<void> _handleMessageOpen(RemoteMessage message) async {
final data = message.data; final data = message.data;
// Try to get type from data, fallback to notification title parsing if needed
final type = data['type'] as String?; final type = data['type'] as String?;
debugPrint('[FCM] Handling message open - type: $type, data: $data'); debugPrint('[FCM] Handling message open - type: $type, data: $data');
final navigator = AppRoutes.rootNavigatorKey.currentState; // Use the router directly for reliability
if (navigator == null) { final router = AppRoutes.router;
debugPrint('[FCM] Navigator not available');
return;
}
switch (type) { switch (type) {
case 'chat': case 'chat':
@ -482,6 +479,8 @@ class NotificationService {
final conversationId = data['conversation_id']; final conversationId = data['conversation_id'];
if (conversationId != null) { if (conversationId != null) {
await _openConversation(conversationId.toString()); await _openConversation(conversationId.toString());
} else {
router.go(AppRoutes.secureChat);
} }
break; break;
@ -495,7 +494,7 @@ class NotificationService {
final postId = data['post_id'] ?? data['beacon_id']; final postId = data['post_id'] ?? data['beacon_id'];
final target = data['target']; final target = data['target'];
if (postId != null) { if (postId != null) {
_navigateToPost(navigator, postId.toString(), target?.toString()); _navigateToPost(postId.toString(), target?.toString());
} }
break; break;
@ -505,9 +504,9 @@ class NotificationService {
case 'follow_accepted': case 'follow_accepted':
final followerId = data['follower_id']; final followerId = data['follower_id'];
if (followerId != null) { if (followerId != null) {
navigator.context.push('${AppRoutes.userPrefix}/$followerId'); router.push('${AppRoutes.userPrefix}/$followerId');
} else { } else {
navigator.context.go(AppRoutes.profile); router.go(AppRoutes.profile);
} }
break; break;
@ -515,50 +514,60 @@ class NotificationService {
case 'beacon_report': case 'beacon_report':
final beaconId = data['beacon_id'] ?? data['post_id']; final beaconId = data['beacon_id'] ?? data['post_id'];
if (beaconId != null) { if (beaconId != null) {
_navigateToPost(navigator, beaconId.toString(), 'beacon_map'); _navigateToPost(beaconId.toString(), 'beacon_map');
} else { } else {
navigator.context.go(AppRoutes.beaconPrefix); router.go(AppRoutes.beaconPrefix);
} }
break; break;
default: default:
debugPrint('[FCM] Unknown notification type: $type'); debugPrint('[FCM] Unknown notification type: $type');
// Retrieve generic target if available
final target = data['target'];
if (target != null) {
_handleGenericTarget(target.toString());
}
break; break;
} }
} }
void _navigateToPost(NavigatorState navigator, String postId, String? target) { void _navigateToPost(String postId, String? target) {
final router = AppRoutes.router;
switch (target) { switch (target) {
case 'beacon_map': case 'beacon_map':
navigator.context.go(AppRoutes.beaconPrefix); router.go(AppRoutes.beaconPrefix);
break; break;
case 'quip_feed': case 'quip_feed':
navigator.context.go(AppRoutes.quips); router.go(AppRoutes.quips);
break; break;
case 'thread_view': case 'thread_view':
case 'main_feed': case 'main_feed':
default: default:
navigator.context.push('${AppRoutes.postPrefix}/$postId'); router.push('${AppRoutes.postPrefix}/$postId');
break; break;
} }
} }
Future<void> _openConversation(String conversationId) async { void _handleGenericTarget(String target) {
final conversation = final router = AppRoutes.router;
await SecureChatService.instance.getConversationById(conversationId); switch (target) {
if (conversation == null) { case 'secure_chat':
debugPrint('[FCM] Conversation not found: $conversationId'); router.go(AppRoutes.secureChat);
return; 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; void _openConversation(String conversationId) {
if (navigator == null) return; AppRoutes.router.push('${AppRoutes.secureChat}/$conversationId');
navigator.push(
MaterialPageRoute(
builder: (_) => SecureChatScreen(conversation: conversation),
),
);
} }
void dispose() { void dispose() {