import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:page_transition/page_transition.dart'; import 'package:provider/provider.dart'; import '/backend/schema/enums/enums.dart'; import '/auth/custom_auth/custom_auth_user_provider.dart'; import '/index.dart'; import '/main.dart'; import '/flutterlib/flutter_theme.dart'; import '/flutterlib/lat_lng.dart'; import '/flutterlib/place.dart'; import '/flutterlib/flutter_util.dart'; import 'serialization_util.dart'; export 'package:go_router/go_router.dart'; export 'serialization_util.dart'; const kTransitionInfoKey = '__transition_info__'; class AppStateNotifier extends ChangeNotifier { AppStateNotifier._(); static AppStateNotifier? _instance; static AppStateNotifier get instance => _instance ??= AppStateNotifier._(); NumstationAuthUser? initialUser; NumstationAuthUser? user; bool showSplashImage = true; String? _redirectLocation; /// Determines whether the app will refresh and build again when a sign /// in or sign out happens. This is useful when the app is launched or /// on an unexpected logout. However, this must be turned off when we /// intend to sign in/out and then navigate or perform any actions after. /// Otherwise, this will trigger a refresh and interrupt the action(s). bool notifyOnAuthChange = true; bool get loading => user == null || showSplashImage; bool get loggedIn => user?.loggedIn ?? false; bool get initiallyLoggedIn => initialUser?.loggedIn ?? false; bool get shouldRedirect => loggedIn && _redirectLocation != null; String getRedirectLocation() => _redirectLocation!; bool hasRedirect() => _redirectLocation != null; void setRedirectLocationIfUnset(String loc) => _redirectLocation ??= loc; void clearRedirectLocation() => _redirectLocation = null; /// Mark as not needing to notify on a sign in / out when we intend /// to perform subsequent actions (such as navigation) afterwards. void updateNotifyOnAuthChange(bool notify) => notifyOnAuthChange = notify; void update(NumstationAuthUser newUser) { final shouldUpdate = user?.uid == null || newUser.uid == null || user?.uid != newUser.uid; initialUser ??= newUser; user = newUser; // Refresh the app on auth change unless explicitly marked otherwise. // No need to update unless the user has changed. if (notifyOnAuthChange && shouldUpdate) { notifyListeners(); } // Once again mark the notifier as needing to update on auth change // (in order to catch sign in / out events). updateNotifyOnAuthChange(true); } void stopShowingSplashImage() { showSplashImage = false; notifyListeners(); } } GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter( initialLocation: '/', debugLogDiagnostics: true, refreshListenable: appStateNotifier, errorBuilder: (context, state) => appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(), routes: [ FFRoute( name: '_initialize', path: '/', builder: (context, _) => appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(), ), FFRoute( name: 'login', path: '/login', builder: (context, params) => LoginWidget(), ), FFRoute( name: 'signup', path: '/signup', builder: (context, params) => SignupWidget(), ), FFRoute( name: 'login1', path: '/login1', builder: (context, params) => Login1Widget(), ), FFRoute( name: 'login2', path: '/login2', builder: (context, params) => Login2Widget(), ), FFRoute( name: 'login3', path: '/login3', builder: (context, params) => Login3Widget(), ), FFRoute( name: 'login4', path: '/login4', builder: (context, params) => Login4Widget(), ), FFRoute( name: 'login5', path: '/login5', builder: (context, params) => Login5Widget(), ), FFRoute( name: 'tnc', path: '/tnc', builder: (context, params) => TncWidget(), ), FFRoute( name: 'policy', path: '/policy', builder: (context, params) => PolicyWidget(), ), FFRoute( name: 'menu', path: '/menu', builder: (context, params) => MenuWidget(), ), FFRoute( name: 'dashboard', path: '/dashboard', builder: (context, params) => DashboardWidget(), ), FFRoute( name: 'test', path: '/test', builder: (context, params) => TestWidget(), ), FFRoute( name: 'setting', path: '/setting', builder: (context, params) => SettingWidget(), ), FFRoute( name: 'user', path: '/user', builder: (context, params) => UserWidget(), ), FFRoute( name: 'bk1', path: '/bk1', builder: (context, params) => Bk1Widget(), ), FFRoute( name: 'enquiry', path: '/enquiry', builder: (context, params) => EnquiryWidget(), ), FFRoute( name: 'myenquiry', path: '/myenquiry', builder: (context, params) => MyenquiryWidget(), ), FFRoute( name: 'myenquiry1', path: '/myenquiry1', builder: (context, params) => Myenquiry1Widget(), ), FFRoute( name: 'language', path: '/language', builder: (context, params) => LanguageWidget(), ), FFRoute( name: 'account1', path: '/account1', builder: (context, params) => Account1Widget(), ), FFRoute( name: 'chatbox', path: '/chatbox', builder: (context, params) => ChatboxWidget(), ), FFRoute( name: 'compsec', path: '/compsec', builder: (context, params) => CompsecWidget(), ), FFRoute( name: 'compsec1', path: '/compsec1', builder: (context, params) => Compsec1Widget(), ), FFRoute( name: 'compsec2', path: '/compsec2', builder: (context, params) => Compsec2Widget(), ), FFRoute( name: 'compsec3', path: '/compsec3', builder: (context, params) => Compsec3Widget(), ), FFRoute( name: 'compsec4', path: '/compsec4', builder: (context, params) => Compsec4Widget(), ), FFRoute( name: 'compsec5', path: '/compsec5', builder: (context, params) => Compsec5Widget(), ), FFRoute( name: 'bk2', path: '/bk2', builder: (context, params) => Bk2Widget(), ), FFRoute( name: 'bk3', path: '/bk3', builder: (context, params) => Bk3Widget(), ), FFRoute( name: 'bk4', path: '/bk4', builder: (context, params) => Bk4Widget(), ), FFRoute( name: 'bk4_1', path: '/bk41', builder: (context, params) => Bk41Widget(), ), FFRoute( name: 'bk4_2', path: '/bk42', builder: (context, params) => Bk42Widget(), ), FFRoute( name: 'search', path: '/search', builder: (context, params) => SearchWidget(), ), FFRoute( name: 'bk_checkout', path: '/bkCheckout', builder: (context, params) => BkCheckoutWidget(), ), FFRoute( name: 'bk_checkout1', path: '/bkCheckout1', builder: (context, params) => BkCheckout1Widget(), ), FFRoute( name: 'compsec_checkout', path: '/compsecCheckout', builder: (context, params) => CompsecCheckoutWidget(), ), FFRoute( name: 'compsec_checkout1', path: '/compsecCheckout1', builder: (context, params) => CompsecCheckout1Widget(), ), FFRoute( name: 'compsec_checkout2', path: '/compsecCheckout2', builder: (context, params) => CompsecCheckout2Widget(), ), FFRoute( name: 'compsecEnq', path: '/compsecEnq', builder: (context, params) => CompsecEnqWidget(), ), FFRoute( name: 'compsecEnq1', path: '/compsecEnq1', builder: (context, params) => CompsecEnq1Widget(), ), FFRoute( name: 'compsec_changename', path: '/compsecChangename', builder: (context, params) => CompsecChangenameWidget(), ), FFRoute( name: 'compsec_changeaddress', path: '/compsecChangeaddress', builder: (context, params) => CompsecChangeaddressWidget(), ), FFRoute( name: 'compsec_changedirector', path: '/compsecChangedirector', builder: (context, params) => CompsecChangedirectorWidget(), ), FFRoute( name: 'compsec_changesecretary', path: '/compsecChangesecretary', builder: (context, params) => CompsecChangesecretaryWidget(), ), FFRoute( name: 'compsec_changesecrORdir', path: '/compsecChangesecrORdir', builder: (context, params) => CompsecChangesecrORdirWidget(), ), FFRoute( name: 'compsec_resignation', path: '/compsecResignation', builder: (context, params) => CompsecResignationWidget(), ), FFRoute( name: 'compsec_transferofShares', path: '/compsecTransferofShares', builder: (context, params) => CompsecTransferofSharesWidget(), ), FFRoute( name: 'compsec_otherRequest', path: '/compsecOtherRequest', builder: (context, params) => CompsecOtherRequestWidget(), ), FFRoute( name: 'account', path: '/account', builder: (context, params) => AccountWidget(), ), FFRoute( name: 'notification', path: '/notification', builder: (context, params) => NotificationWidget(), ), FFRoute( name: 'compsecQueue', path: '/compsecQueue', builder: (context, params) => CompsecQueueWidget(), ), FFRoute( name: 'compsecQueue1_1', path: '/compsecQueue11', builder: (context, params) => CompsecQueue11Widget(), ), FFRoute( name: 'testing', path: '/testing', builder: (context, params) => TestingWidget(), ), FFRoute( name: 'newuser', path: '/newuser', builder: (context, params) => NewuserWidget( checkPermission23: params.getParam('checkPermission23', ParamType.bool), checkPremission22: params.getParam('checkPremission22', ParamType.bool), ), ), FFRoute( name: 'userDetail', path: '/userDetail', builder: (context, params) => UserDetailWidget( firstName: params.getParam('firstName', ParamType.String), lastName: params.getParam('lastName', ParamType.String), phone: params.getParam('phone', ParamType.int), email: params.getParam('email', ParamType.String), status: params.getParam('status', ParamType.String), ), ), FFRoute( name: 'compsecDigitalTrans_list', path: '/compsecDigitalTransList', builder: (context, params) => CompsecDigitalTransListWidget(), ), FFRoute( name: 'compsecServicedraft', path: '/compsecServicedraft', builder: (context, params) => CompsecServicedraftWidget(), ), FFRoute( name: 'setting1', path: '/setting1', builder: (context, params) => Setting1Widget(), ), FFRoute( name: 'editUser', path: '/editUser', builder: (context, params) => EditUserWidget( firstName: params.getParam('firstName', ParamType.String), lastName: params.getParam('lastName', ParamType.String), phone: params.getParam('phone', ParamType.int), email: params.getParam('email', ParamType.String), userID: params.getParam('userID', ParamType.int), checkPermission23: params.getParam('checkPermission23', ParamType.bool), checkPremission22: params.getParam('checkPremission22', ParamType.bool), roleID: params.getParam('roleID', ParamType.String), ), ), FFRoute( name: 'bk_view', path: '/bkView', builder: (context, params) => BkViewWidget( fileUrl: params.getParam('fileUrl', ParamType.JSON), ), ), FFRoute( name: 'search1', path: '/search1', builder: (context, params) => Search1Widget( search: params.getParam('search', ParamType.String), status: params.getParam('status', ParamType.String), category: params.getParam('category', ParamType.String), fixDate: params.getParam('fixDate', ParamType.String), dateFrom: params.getParam('dateFrom', ParamType.String), dateTo: params.getParam('dateTo', ParamType.String), ), ), FFRoute( name: 'search_page', path: '/searchPage', builder: (context, params) => SearchPageWidget(), ) ].map((r) => r.toRoute(appStateNotifier)).toList(), ); extension NavParamExtensions on Map { Map get withoutNulls => Map.fromEntries( entries .where((e) => e.value != null) .map((e) => MapEntry(e.key, e.value!)), ); } extension NavigationExtensions on BuildContext { void goNamedAuth( String name, bool mounted, { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, bool ignoreRedirect = false, }) => !mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect) ? null : goNamed( name, pathParameters: pathParameters, queryParameters: queryParameters, extra: extra, ); void pushNamedAuth( String name, bool mounted, { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, bool ignoreRedirect = false, }) => !mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect) ? null : pushNamed( name, pathParameters: pathParameters, queryParameters: queryParameters, extra: extra, ); void safePop() { // If there is only one route on the stack, navigate to the initial // page instead of popping. if (canPop()) { pop(); } else { go('/'); } } } extension GoRouterExtensions on GoRouter { AppStateNotifier get appState => AppStateNotifier.instance; void prepareAuthEvent([bool ignoreRedirect = false]) => appState.hasRedirect() && !ignoreRedirect ? null : appState.updateNotifyOnAuthChange(false); bool shouldRedirect(bool ignoreRedirect) => !ignoreRedirect && appState.hasRedirect(); void clearRedirectLocation() => appState.clearRedirectLocation(); void setRedirectLocationIfUnset(String location) => appState.updateNotifyOnAuthChange(false); } extension _GoRouterStateExtensions on GoRouterState { Map get extraMap => extra != null ? extra as Map : {}; Map get allParams => {} ..addAll(pathParameters) ..addAll(queryParameters) ..addAll(extraMap); TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey) ? extraMap[kTransitionInfoKey] as TransitionInfo : TransitionInfo.appDefault(); } class FFParameters { FFParameters(this.state, [this.asyncParams = const {}]); final GoRouterState state; final Map Function(String)> asyncParams; Map futureParamValues = {}; // Parameters are empty if the params map is empty or if the only parameter // present is the special extra parameter reserved for the transition info. bool get isEmpty => state.allParams.isEmpty || (state.extraMap.length == 1 && state.extraMap.containsKey(kTransitionInfoKey)); bool isAsyncParam(MapEntry param) => asyncParams.containsKey(param.key) && param.value is String; bool get hasFutures => state.allParams.entries.any(isAsyncParam); Future completeFutures() => Future.wait( state.allParams.entries.where(isAsyncParam).map( (param) async { final doc = await asyncParams[param.key]!(param.value) .onError((_, __) => null); if (doc != null) { futureParamValues[param.key] = doc; return true; } return false; }, ), ).onError((_, __) => [false]).then((v) => v.every((e) => e)); dynamic getParam( String paramName, ParamType type, [ bool isList = false, ]) { if (futureParamValues.containsKey(paramName)) { return futureParamValues[paramName]; } if (!state.allParams.containsKey(paramName)) { return null; } final param = state.allParams[paramName]; // Got parameter from `extras`, so just directly return it. if (param is! String) { return param; } // Return serialized value. return deserializeParam( param, type, isList, ); } } class FFRoute { const FFRoute({ required this.name, required this.path, required this.builder, this.requireAuth = false, this.asyncParams = const {}, this.routes = const [], }); final String name; final String path; final bool requireAuth; final Map Function(String)> asyncParams; final Widget Function(BuildContext, FFParameters) builder; final List routes; GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute( name: name, path: path, redirect: (context, state) { if (appStateNotifier.shouldRedirect) { final redirectLocation = appStateNotifier.getRedirectLocation(); appStateNotifier.clearRedirectLocation(); return redirectLocation; } if (requireAuth && !appStateNotifier.loggedIn) { appStateNotifier.setRedirectLocationIfUnset(state.location); return '/login'; } return null; }, pageBuilder: (context, state) { final ffParams = FFParameters(state, asyncParams); final page = ffParams.hasFutures ? FutureBuilder( future: ffParams.completeFutures(), builder: (context, _) => builder(context, ffParams), ) : builder(context, ffParams); final child = appStateNotifier.loading ? Center( child: SizedBox( width: 50.0, height: 50.0, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( FlutterTheme.of(context).info, ), ), ), ) : page; final transitionInfo = state.transitionInfo; return transitionInfo.hasTransition ? CustomTransitionPage( key: state.pageKey, child: child, transitionDuration: transitionInfo.duration, transitionsBuilder: (context, animation, secondaryAnimation, child) => PageTransition( type: transitionInfo.transitionType, duration: transitionInfo.duration, reverseDuration: transitionInfo.duration, alignment: transitionInfo.alignment, child: child, ).buildTransitions( context, animation, secondaryAnimation, child, ), ) : MaterialPage(key: state.pageKey, child: child); }, routes: routes, ); } class TransitionInfo { const TransitionInfo({ required this.hasTransition, this.transitionType = PageTransitionType.fade, this.duration = const Duration(milliseconds: 300), this.alignment, }); final bool hasTransition; final PageTransitionType transitionType; final Duration duration; final Alignment? alignment; static TransitionInfo appDefault() => TransitionInfo(hasTransition: false); } class RootPageContext { const RootPageContext(this.isRootPage, [this.errorRoute]); final bool isRootPage; final String? errorRoute; static bool isInactiveRootPage(BuildContext context) { final rootPageContext = context.read(); final isRootPage = rootPageContext?.isRootPage ?? false; final location = GoRouter.of(context).location; return isRootPage && location != '/' && location != rootPageContext?.errorRoute; } static Widget wrap(Widget child, {String? errorRoute}) => Provider.value( value: RootPageContext(true, errorRoute), child: child, ); }