/ app / lib / core / router.dart
router.dart
  1  import 'package:flutter/material.dart';
  2  import 'package:go_router/go_router.dart';
  3  import 'package:flutter_riverpod/flutter_riverpod.dart';
  4  
  5  import 'theme/app_responsive.dart';
  6  import 'utils/platform_utils.dart';
  7  import '../screens/desktop_shell.dart';
  8  import '../screens/onboarding/onboarding_screen.dart';
  9  import '../screens/onboarding/create_identity_screen.dart';
 10  import '../screens/contacts/contacts_screen.dart';
 11  import '../screens/contacts/add_contact_screen.dart';
 12  import '../screens/contacts/scan_qr_screen.dart';
 13  import '../screens/contacts/show_qr_screen.dart';
 14  import '../screens/contacts/confirm_contact_screen.dart';
 15  import '../screens/contacts/nearby_contacts_screen.dart';
 16  import '../screens/chat/chat_screen.dart';
 17  import '../screens/contacts/contact_info_screen.dart';
 18  import '../screens/contacts/verify_contact_screen.dart';
 19  import '../screens/groups/create_group_screen.dart';
 20  import '../screens/groups/group_chat_screen.dart';
 21  import '../screens/groups/group_settings_screen.dart';
 22  import '../screens/settings/settings_screen.dart';
 23  import '../providers/identity_provider.dart';
 24  import '../services/dead_drop_service.dart';
 25  import '../services/notification_service.dart';
 26  
 27  /// App routes
 28  abstract final class AppRoutes {
 29    static const onboarding = '/onboarding';
 30    static const createIdentity = '/onboarding/create';
 31    static const contacts = '/contacts';
 32    static const addContact = '/contacts/add';
 33    static const scanQr = '/contacts/scan';
 34    static const showQr = '/contacts/qr';
 35    static const confirmContact = '/contacts/confirm';
 36    static const nearbyContacts = '/contacts/nearby';
 37    static const chat = '/chat/:contactId';
 38    static const contactInfo = '/chat/:contactId/info';
 39    static const verifyContact = '/chat/:contactId/verify';
 40    static const createGroup = '/groups/create';
 41    static const groupChat = '/group/:groupId';
 42    static const groupSettings = '/group/:groupId/settings';
 43    static const settings = '/settings';
 44  
 45    /// Build chat route with contact ID
 46    static String chatPath(String contactIdHex) => '/chat/$contactIdHex';
 47  
 48    /// Build contact info route
 49    static String contactInfoPath(String contactIdHex) =>
 50        '/chat/$contactIdHex/info';
 51  
 52    /// Build verify contact route
 53    static String verifyContactPath(String contactIdHex) =>
 54        '/chat/$contactIdHex/verify';
 55  
 56    /// Build group chat route with group ID
 57    static String groupChatPath(String groupIdHex) => '/group/$groupIdHex';
 58  
 59    /// Build group settings route with group ID
 60    static String groupSettingsPath(String groupIdHex) =>
 61        '/group/$groupIdHex/settings';
 62  }
 63  
 64  /// Provider for pending contact (passed between scan and confirm screens)
 65  final pendingContactProvider = StateProvider<PendingContact?>((ref) => null);
 66  
 67  /// Router provider
 68  final routerProvider = Provider<GoRouter>((ref) {
 69    final hasIdentity = ref.watch(hasIdentityProvider);
 70  
 71    return GoRouter(
 72      navigatorKey: NotificationService.navigatorKey,
 73      initialLocation: AppRoutes.onboarding,
 74      redirect: (context, state) {
 75        final isOnboarding = state.matchedLocation.startsWith(
 76          AppRoutes.onboarding,
 77        );
 78  
 79        return hasIdentity.when(
 80          data: (hasId) {
 81            if (hasId && isOnboarding) {
 82              return AppRoutes.contacts;
 83            }
 84            if (!hasId && !isOnboarding) {
 85              return AppRoutes.onboarding;
 86            }
 87            return null;
 88          },
 89          loading: () => null,
 90          error: (_, __) => AppRoutes.onboarding,
 91        );
 92      },
 93      routes: [
 94        // Onboarding (outside shell — no sidebar)
 95        GoRoute(
 96          path: AppRoutes.onboarding,
 97          builder: (context, state) => const OnboardingScreen(),
 98          routes: [
 99            GoRoute(
100              path: 'create',
101              builder: (context, state) => const CreateIdentityScreen(),
102            ),
103          ],
104        ),
105  
106        // Main app routes — wrapped in ShellRoute for desktop layout
107        ShellRoute(
108          builder: (context, state, child) {
109            if (AppResponsive.isWideScreen(context)) {
110              return DesktopShell(
111                child: child,
112                location: state.matchedLocation,
113              );
114            }
115            return child;
116          },
117          routes: [
118            // Contacts (no sub-routes — flattened so desktop shell can
119            // swap the right pane without a nested navigator stack)
120            GoRoute(
121              path: AppRoutes.contacts,
122              builder: (context, state) => const ContactsScreen(),
123            ),
124            GoRoute(
125              path: AppRoutes.addContact,
126              builder: (context, state) => const AddContactScreen(),
127            ),
128            GoRoute(
129              path: AppRoutes.scanQr,
130              builder: (context, state) => const ScanQrScreen(),
131            ),
132            GoRoute(
133              path: AppRoutes.showQr,
134              builder: (context, state) => const ShowQrScreen(),
135            ),
136            GoRoute(
137              path: AppRoutes.confirmContact,
138              builder: (context, state) => const ConfirmContactScreen(),
139            ),
140            if (PlatformUtils.supportsBle)
141              GoRoute(
142                path: AppRoutes.nearbyContacts,
143                builder: (context, state) => const NearbyContactsScreen(),
144              ),
145  
146            // Chat
147            GoRoute(
148              path: '/chat/:contactId',
149              builder: (context, state) {
150                final contactIdHex = state.pathParameters['contactId']!;
151                return ChatScreen(key: ValueKey(contactIdHex), contactIdHex: contactIdHex);
152              },
153              routes: [
154                GoRoute(
155                  path: 'info',
156                  builder: (context, state) {
157                    final contactIdHex = state.pathParameters['contactId']!;
158                    return ContactInfoScreen(contactIdHex: contactIdHex);
159                  },
160                ),
161                GoRoute(
162                  path: 'verify',
163                  builder: (context, state) {
164                    final contactIdHex = state.pathParameters['contactId']!;
165                    return VerifyContactScreen(contactIdHex: contactIdHex);
166                  },
167                ),
168              ],
169            ),
170  
171            // Groups
172            GoRoute(
173              path: AppRoutes.createGroup,
174              builder: (context, state) => const CreateGroupScreen(),
175            ),
176            GoRoute(
177              path: '/group/:groupId',
178              builder: (context, state) {
179                final groupIdHex = state.pathParameters['groupId']!;
180                return GroupChatScreen(key: ValueKey(groupIdHex), groupIdHex: groupIdHex);
181              },
182              routes: [
183                GoRoute(
184                  path: 'settings',
185                  builder: (context, state) {
186                    final groupIdHex = state.pathParameters['groupId']!;
187                    return GroupSettingsScreen(groupIdHex: groupIdHex);
188                  },
189                ),
190              ],
191            ),
192  
193            // Settings
194            GoRoute(
195              path: AppRoutes.settings,
196              builder: (context, state) => const SettingsScreen(),
197            ),
198          ],
199        ),
200      ],
201    );
202  });