import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/admin_pin_view_model.dart'; import '../viewmodels/pin_lock_view_model.dart'; import '../viewmodels/remote_sync_view_model.dart'; import '../views/admin_screen_view.dart'; import '../views/bar/bar_screen_view.dart'; import '../views/dev_menu_view.dart'; import '../views/error_screen_view.dart'; import '../views/history_screen_view.dart'; import '../views/pin_lock_view.dart'; import '../views/product_form_view.dart'; import '../views/product_list_view.dart'; import '../views/registration_view.dart'; import '../views/settings_view.dart'; import '../views/update_progress_view.dart'; final RouteObserver routeObserver = RouteObserver(); String? _redirectApp({ required PinLockViewModel pinLockViewModel, required AdminPinViewModel adminPinViewModel, required RemoteSyncViewModel remoteSyncViewModel, required GoRouterState state, }) { if (!pinLockViewModel.hasLoaded || !adminPinViewModel.hasLoaded || !remoteSyncViewModel.hasLoaded) { return null; } final location = state.uri.path; final returnTo = _returnLocation(state); if (remoteSyncViewModel.isConfigured && location == '/register') { return '/admin'; } final goingToLock = location == '/lock'; final goingToAdminLock = location == '/admin/lock'; final mustBeOnLock = _mustBeOnLock(pinLockViewModel); if (mustBeOnLock && !goingToLock) return _lockLocation('/lock', state); if (!mustBeOnLock && goingToLock) return returnTo ?? '/bar'; final isAdminArea = _isAdminArea(location); final isDebugShortcutArea = _isDebugShortcutArea(location); if (isAdminArea && !isDebugShortcutArea && !goingToAdminLock && !adminPinViewModel.isUnlocked) { return _lockLocation('/admin/lock', state); } if (goingToAdminLock && adminPinViewModel.isUnlocked) { return returnTo ?? '/admin'; } return null; } /// Sends the user back where they were headed once the PIN is entered. String _lockLocation(String lockPath, GoRouterState state) { return Uri( path: lockPath, queryParameters: {'from': state.uri.toString()}, ).toString(); } String? _returnLocation(GoRouterState state) { final from = state.uri.queryParameters['from']; if (from == null || !from.startsWith('/')) return null; final path = Uri.parse(from).path; if (path == '/lock' || path == '/admin/lock') return null; return from; } bool _mustBeOnLock(PinLockViewModel pinLockViewModel) { final needsPinSetup = !pinLockViewModel.isPinSet && pinLockViewModel.pinRequired; final isLocked = pinLockViewModel.isPinSet && !pinLockViewModel.isUnlocked && pinLockViewModel.pinRequired; return needsPinSetup || isLocked; } bool _isAdminArea(String location) { return location == '/admin' || location.startsWith('/admin/') || location == '/settings' || location == '/dev' || location == '/register' || location == '/products' || location.startsWith('/products/') || location == '/history' || location.startsWith('/history/'); } bool _isDebugShortcutArea(String location) { if (!kDebugMode) return false; return location == '/products' || location.startsWith('/products/') || location == '/history' || location.startsWith('/history/'); } GoRouter createAppRouter({ required PinLockViewModel pinLockViewModel, required AdminPinViewModel adminPinViewModel, required RemoteSyncViewModel remoteSyncViewModel, }) { // go_router drops `extra` when it rebuilds a route from its location, e.g. // when the app returns from Android's installer, so keep the last update UpdateInfo? lastUpdate; return GoRouter( initialLocation: '/bar', observers: [routeObserver, SentryNavigatorObserver()], refreshListenable: Listenable.merge([ pinLockViewModel, adminPinViewModel, remoteSyncViewModel, ]), redirect: (_, state) => _redirectApp( pinLockViewModel: pinLockViewModel, adminPinViewModel: adminPinViewModel, remoteSyncViewModel: remoteSyncViewModel, state: state, ), routes: [ GoRoute(path: '/', redirect: (context, state) => '/bar'), GoRoute( path: '/register', builder: (context, state) => const RegistrationView(), ), GoRoute( path: '/lock', builder: (context, state) => PinEntryView( mode: pinLockViewModel.isPinSet ? PinEntryMode.unlock : PinEntryMode.create, onSuccess: () => context.go(_returnLocation(state) ?? '/bar'), ), ), GoRoute(path: '/bar', builder: (context, state) => const BarScreenView()), GoRoute( path: '/admin/lock', builder: (context, state) => PinEntryView( mode: adminPinViewModel.isPinSet ? PinEntryMode.unlock : PinEntryMode.create, isAdmin: true, onVerify: adminPinViewModel.verify, onSet: adminPinViewModel.setPin, error: () => adminPinViewModel.error, onSuccess: () => context.go(_returnLocation(state) ?? '/admin'), onClose: () => context.canPop() ? context.pop() : context.go('/bar'), ), ), GoRoute( path: '/admin', builder: (context, state) => const AdminScreenView(), ), GoRoute( path: '/products', builder: (context, state) => const ProductListView(), routes: [ GoRoute( path: 'new', builder: (context, state) => const ProductFormView(), ), GoRoute( path: ':id/edit', builder: (context, state) { final productId = state.pathParameters['id']!; return ProductFormView(productId: productId); }, ), ], ), GoRoute( path: '/history', builder: (context, state) => const HistoryScreenView(), ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreenView(), ), GoRoute( path: '/update-progress', redirect: (context, state) { if (state.extra case final UpdateInfo update) lastUpdate = update; return lastUpdate == null ? '/settings' : null; }, builder: (context, state) { final update = lastUpdate!; final simulate = state.uri.queryParameters['simulate'] == 'true'; final simulateError = state.uri.queryParameters['simulateError'] == 'true'; return UpdateProgressView( update: update, simulate: simulate, simulateError: simulateError, ); }, ), GoRoute(path: '/dev', builder: (context, state) => const DevMenuView()), GoRoute( path: '/error', builder: (context, state) => const ErrorScreenView(), ), ], ); }