resolveRedirect function
- required bool isJailbroken,
- required StartupStatus startupStatus,
- required AuthenticationStatus authStatus,
- required String location,
Creates and configures the application's GoRouter instance.
Must be called from _AppView.didChangeDependencies() (not initState)
because it reads AuthenticationBloc and StartupCubit from context,
which are not available until after the first build.
The router's refreshListenable is a GoRouterRefreshStream that merges
the AuthenticationBloc and StartupCubit streams so guard logic
re-evaluates on every state change.
Guard evaluation order:
- Jailbreak detected →
/jailbreak(terminal, overrides all other gates). - Startup failed or incomplete → hold on
/splash. - Authenticated → redirect to
/homefrom splash or login. - Unauthenticated → redirect to
/login. Pure redirect-decision logic for the router guard, extracted out of createRouter'sredirectcallback so it is directly unit-testable without constructing a GoRouter or mocking AuthenticationBloc/ StartupCubit.
Returns the path to redirect to, or null to leave location unchanged.
See createRouter's doc comment for the full guard evaluation order.
Implementation
String? resolveRedirect({
required bool isJailbroken,
required StartupStatus startupStatus,
required AuthenticationStatus authStatus,
required String location,
}) {
// Gate 1: Jailbreak — terminal state, overrides everything.
if (isJailbroken) {
return location == AppRoutes.jailbreak ? null : AppRoutes.jailbreak;
}
// Not jailbroken: /jailbreak is never a legitimate destination on its own —
// fall through to the normal splash/auth redirect flow below rather than
// leaving the user stranded there (e.g. a stale location from a prior
// jailbroken session).
// Gate 2: Startup failed — hold on splash.
// TODO: Replace with a dedicated error/retry screen.
if (startupStatus == StartupStatus.failed) {
return location == AppRoutes.splash ? null : AppRoutes.splash;
}
// Gate 3: Startup not done — hold on splash.
if (startupStatus != StartupStatus.completed) {
return location == AppRoutes.splash ? null : AppRoutes.splash;
}
// Gate 4: Auth routing — startup is complete.
if (authStatus == AuthenticationStatus.authenticated) {
// /jailbreak is included here even though it's a "known" route — Gate 1
// already established the device isn't actually jailbroken by this point.
if (location == AppRoutes.splash || location == AppRoutes.login || location == AppRoutes.jailbreak) {
return AppRoutes.home;
}
// An unrecognised location is redirected home rather than falling
// through to route matching and errorBuilder below — the unauthenticated
// branch already has an equivalent catch-all (redirecting to login);
// this closes the same gap for authenticated users. A legitimately
// matched location (e.g. /home, /onboarding) is left alone.
if (!AppRoutes.all.contains(location)) {
return AppRoutes.home;
}
return null;
}
// Unauthenticated — only expected status at this point is unauthenticated.
// unknown should never reach here because Gate 3 holds on splash until
// startup (and therefore auth resolution) is complete.
assert(authStatus == AuthenticationStatus.unauthenticated, 'Unhandled AuthenticationStatus: $authStatus');
if (location == AppRoutes.splash) return AppRoutes.login;
if (location == AppRoutes.login) return null;
return AppRoutes.login;
}