235 lines
7.0 KiB
Dart
235 lines
7.0 KiB
Dart
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<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
|
|
|
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(),
|
|
),
|
|
],
|
|
);
|
|
}
|