resolveRedirect function

String? resolveRedirect({
  1. required bool isJailbroken,
  2. required StartupStatus startupStatus,
  3. required AuthenticationStatus authStatus,
  4. 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:

  1. Jailbreak detected → /jailbreak (terminal, overrides all other gates).
  2. Startup failed or incomplete → hold on /splash.
  3. Authenticated → redirect to /home from splash or login.
  4. Unauthenticated → redirect to /login. Pure redirect-decision logic for the router guard, extracted out of createRouter's redirect callback 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;
}