createRouter function
- required bool supabaseReady,
- VoidCallback? onSetupComplete,
Creates the app router. When supabaseReady is false, all routes
redirect to /setup and the auth listener is not attached.
onSetupComplete is called by the setup screen after Supabase is
successfully initialized, triggering an app-level rebuild.
Implementation
GoRouter createRouter({
required bool supabaseReady,
VoidCallback? onSetupComplete,
}) {
_activeRouter = GoRouter(
initialLocation: supabaseReady ? '/' : '/setup',
refreshListenable: supabaseReady ? AuthChangeNotifier() : null,
redirect: (context, state) {
if (!supabaseReady) {
if (state.matchedLocation != '/setup') return '/setup';
return null;
}
final session = Supabase.instance.client.auth.currentSession;
final container = ProviderScope.containerOf(context, listen: false);
final lastMode = container.read(lastUsedModeProvider).valueOrNull;
final adminOrgs = container.read(adminEligibleOrgsProvider).valueOrNull;
final lastActiveOrg = container.read(lastActiveOrgIdProvider).valueOrNull;
return authRedirect(
session,
state,
lastMode: lastMode,
adminOrgIds: adminOrgs?.map((o) => o.id).toList(),
lastActiveOrgId: lastActiveOrg,
);
},
routes: [
GoRoute(
path: '/setup',
builder: (context, state) => DeferredScreen(
loader: setup.loadLibrary,
builder: () => setup.SetupScreen(onConnected: onSetupComplete),
),
),
GoRoute(
path: '/login',
builder: (context, state) => DeferredScreen(
loader: auth.loadLibrary,
builder: () => auth.LoginScreen(),
),
),
GoRoute(
path: '/forgot-password',
builder: (context, state) => DeferredScreen(
loader: auth.loadLibrary,
builder: () => auth.ForgotPasswordScreen(),
),
),
GoRoute(
path: '/reset-password',
builder: (context, state) => DeferredScreen(
loader: auth.loadLibrary,
builder: () => auth.ResetPasswordScreen(),
),
),
GoRoute(
path: '/mfa-verify',
builder: (context, state) => DeferredScreen(
loader: mfa.loadLibrary,
builder: () => mfa.MfaVerifyScreen(),
),
),
GoRoute(
path: '/mfa-enroll',
builder: (context, state) => DeferredScreen(
loader: mfa.loadLibrary,
builder: () => mfa.MfaEnrollScreen(),
),
),
GoRoute(
path: '/persona',
builder: (context, state) => DeferredScreen(
loader: setup.loadLibrary,
builder: () => setup.PersonaScreen(),
),
),
// First-connection onboarding (Plan 49) — full-screen, no nav bar.
GoRoute(
path: '/onboarding/profile',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.ProfileCompletionScreen(),
),
),
GoRoute(
path: '/onboarding/welcome',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.OnboardingWelcomeScreen(),
),
),
GoRoute(
path: '/onboarding/why',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.OnboardingWhyScreen(),
),
),
GoRoute(
path: '/onboarding/session',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.OnboardingFirstSessionScreen(),
),
),
GoRoute(
path: '/onboarding/review',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.OnboardingReviewScreen(),
),
),
GoRoute(
path: '/onboarding/mastery',
builder: (context, state) => DeferredScreen(
loader: onboarding.loadLibrary,
builder: () => onboarding.OnboardingMasteryScreen(),
),
),
GoRoute(
path: '/admin-onboarding/org-picker',
builder: (context, state) => DeferredScreen(
loader: adminonboarding.loadLibrary,
builder: () => adminonboarding.AdminOnboardingOrgPickerScreen(),
),
),
GoRoute(
path: '/admin-onboarding/org-basics',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return DeferredScreen(
loader: adminonboarding.loadLibrary,
builder: () =>
adminonboarding.AdminOnboardingOrgBasicsScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/admin-onboarding/members',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return DeferredScreen(
loader: adminonboarding.loadLibrary,
builder: () =>
adminonboarding.AdminOnboardingMembersScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/admin-onboarding/curriculum',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return DeferredScreen(
loader: adminonboarding.loadLibrary,
builder: () =>
adminonboarding.AdminOnboardingCurriculumScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/admin-onboarding/tour',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return DeferredScreen(
loader: adminonboarding.loadLibrary,
builder: () =>
adminonboarding.AdminOnboardingTourScreen(orgId: orgId),
);
},
),
ShellRoute(
builder: (context, state, child) => EntitlementBootstrap(
child: TermsGate(
child: OnboardingGate(child: ScaffoldWithNavBar(child: child)),
),
),
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) =>
const NoTransitionPage(child: HomeScreen()),
),
GoRoute(
path: '/domains',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: domains.loadLibrary,
builder: () => domains.DomainListScreen(),
),
),
routes: [
GoRoute(
path: 'curriculum-creator',
builder: (context, state) => DeferredScreen(
loader: curriculumcreator.loadLibrary,
builder: () => curriculumcreator.CurriculumCreatorScreen(),
),
),
GoRoute(
path: 'upload-curriculum',
builder: (context, state) => DeferredScreen(
loader: curriculumcreator.loadLibrary,
builder: () => curriculumcreator.UploadCurriculumScreen(),
),
),
GoRoute(
path: ':domainSlug/topics',
builder: (context, state) {
final domainSlug = state.pathParameters['domainSlug']!;
final domainName = state.uri.queryParameters['name'];
return DeferredScreen(
loader: domains.loadLibrary,
builder: () => domains.TopicTreeScreen(
domainSlug: domainSlug,
domainName: domainName,
),
);
},
routes: [
GoRoute(
path: ':topicId/lesson',
builder: (context, state) {
final domainSlug = state.pathParameters['domainSlug']!;
final topicId = state.pathParameters['topicId']!;
final topicTitle = state.uri.queryParameters['title'];
return DeferredScreen(
loader: lesson.loadLibrary,
builder: () => lesson.LessonViewerScreen(
topicId: topicId,
topicTitle: topicTitle,
domainSlug: domainSlug,
),
);
},
),
],
),
],
),
GoRoute(
path: '/tutor',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: tutor.loadLibrary,
builder: () => tutor.TutorModeScreen(),
),
),
routes: [
GoRoute(
path: 'chat',
builder: (context, state) {
final topicId = state.uri.queryParameters['topicId'];
final mode = state.uri.queryParameters['mode'] ?? 'socratic';
final topicTitle = state.uri.queryParameters['topicTitle'];
final from = state.uri.queryParameters['from'];
final isValidation =
state.uri.queryParameters['isValidation'] == 'true';
final validationTarget =
state.uri.queryParameters['validationTarget'];
return DeferredScreen(
loader: tutor.loadLibrary,
builder: () => tutor.ChatScreen(
topicId: topicId,
mode: mode,
topicTitle: topicTitle,
from: from,
isValidation: isValidation,
validationTarget: validationTarget,
),
);
},
),
],
),
GoRoute(
path: '/review',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: review.loadLibrary,
builder: () => review.ReviewSessionScreen(),
),
),
routes: [
GoRoute(
path: 'proposals',
builder: (context, state) => DeferredScreen(
loader: review.loadLibrary,
builder: () => review.CardProposalsScreen(),
),
),
],
),
GoRoute(
path: '/progress',
pageBuilder: (context, state) {
final initialTab = state.uri.queryParameters['tab'];
return NoTransitionPage(
child: DeferredScreen(
loader: progress.loadLibrary,
builder: () =>
progress.ProgressDashboardScreen(initialTab: initialTab),
),
);
},
routes: [
GoRoute(
path: 'guide',
builder: (context, state) => DeferredScreen(
loader: progress.loadLibrary,
builder: () => progress.LearningGuideScreen(),
),
),
GoRoute(
path: 'overrides',
builder: (context, state) => DeferredScreen(
loader: progress.loadLibrary,
builder: () => progress.OverrideHistoryScreen(),
),
),
GoRoute(
path: 'notes/new',
builder: (context, state) {
final topicId = state.uri.queryParameters['topicId'];
final sessionId = state.uri.queryParameters['sessionId'];
final content = state.uri.queryParameters['content'];
return DeferredScreen(
loader: notes.loadLibrary,
builder: () => notes.NoteDetailScreen(
topicId: topicId,
sessionId: sessionId,
initialContent: content,
),
);
},
),
GoRoute(
path: 'notes/:noteId',
builder: (context, state) {
final noteId = state.pathParameters['noteId']!;
return DeferredScreen(
loader: notes.loadLibrary,
builder: () => notes.NoteDetailScreen(noteId: noteId),
);
},
),
],
),
GoRoute(
path: '/goals',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: progress.loadLibrary,
builder: () => progress.GoalsListScreen(),
),
),
),
GoRoute(
path: '/bookmarks',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: progress.loadLibrary,
builder: () => progress.BookmarksScreen(),
),
),
),
// Settings now lives inside the shell so the sidebar persists
// when the user opens it. Sub-screens stay scoped under
// `/settings/...` so deep links keep working.
GoRoute(
path: '/settings',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.SettingsScreen(),
),
),
routes: [
GoRoute(
path: 'account',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.AccountSettingsScreen(),
),
),
GoRoute(
path: 'change-password',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.ChangePasswordScreen(),
),
),
GoRoute(
path: 'subscription',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.SubscriptionScreen(),
),
),
GoRoute(
path: 'learning',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.LearningSettingsScreen(),
),
),
GoRoute(
path: 'notifications',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.NotificationsSettingsScreen(),
),
),
GoRoute(
path: 'privacy',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.PrivacySettingsScreen(),
),
),
GoRoute(
path: 'ai',
builder: (_, _) => DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.AiSettingsScreen(),
),
),
],
),
// Billing redirect landing pages (after Stripe Checkout completes
// or is cancelled). Inside the shell so the nav bar stays visible.
GoRoute(
path: '/billing/success',
builder: (context, state) => const _BillingLandingScreen(
title: 'Thank you!',
body:
'Your subscription is being activated. It should appear '
'in your billing page within a few seconds.',
icon: Icons.check_circle_rounded,
),
),
GoRoute(
path: '/billing/cancel',
builder: (context, state) => const _BillingLandingScreen(
title: 'Checkout cancelled',
body:
'No charge was made. You can try again any time from the '
'billing page.',
icon: Icons.cancel_outlined,
),
),
],
),
// Org admin shell — 5 destinations (Dashboard, Members, Assignments,
// Analytics, Org Settings) shown in OrgShellScaffold's nav rail/bar.
// `/org` (no orgId) redirects to `/org/:lastActiveOrgId`, falling back
// to the first admin-eligible org, or `/` if the user has none.
ShellRoute(
builder: (context, state, child) => EntitlementBootstrap(
child: TermsGate(
child: OnboardingGate(
child: DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgShellScaffold(child: child),
),
),
),
),
routes: [
GoRoute(
path: '/org',
redirect: (context, state) {
final container = ProviderScope.containerOf(
context,
listen: false,
);
final lastOrgAsync = container.read(lastActiveOrgIdProvider);
// Fast path: lastActiveOrgId already loaded with a value.
if (lastOrgAsync.hasValue && lastOrgAsync.value != null) {
return '/org/${lastOrgAsync.value}';
}
final adminsAsync = container.read(adminEligibleOrgsProvider);
final orgsAsync = container.read(myOrganizationsProvider);
// If any of the three is still loading, stay on /org and let
// the loading-screen builder watch the providers; it will
// forward once they settle.
if (lastOrgAsync.isLoading ||
adminsAsync.isLoading ||
orgsAsync.isLoading) {
return null;
}
final admins = adminsAsync.valueOrNull;
if (admins != null && admins.isNotEmpty) {
return '/org/${admins.first.id}';
}
final orgs = orgsAsync.valueOrNull;
if (orgs != null && orgs.isNotEmpty) {
return '/org/${orgs.first.id}';
}
return '/';
},
builder: (context, state) => const _OrgRedirectLoadingScreen(),
),
GoRoute(
path: '/org/:orgId',
pageBuilder: (context, state) => NoTransitionPage(
child: DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgDashboardScreen(),
),
),
),
GoRoute(
path: '/org/:orgId/members',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.MemberManagementScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/assignments',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.CurriculumAssignmentScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/analytics',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgAnalyticsScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/settings',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgSettingsScreen(orgId: orgId),
);
},
),
],
),
// Other /org/:orgId/* sub-pages — wrapped uniformly in OrgDetailScaffold
// so every detail screen gets the same AppBar (with back button) and
// mobile bottom-nav / desktop NavigationRail chrome. The title is
// derived from the matched location via `_orgDetailRouteTitle` so
// adding a new detail route auto-inherits the wrapper.
ShellRoute(
builder: (context, state, child) {
final orgId = state.pathParameters['orgId'] ?? '';
final title = orgDetailRouteTitle(state.uri.path, orgId);
return EntitlementBootstrap(
child: TermsGate(
child: OnboardingGate(
child: DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgDetailScaffold(
orgId: orgId,
title: title,
body: child,
),
),
),
),
);
},
routes: [
GoRoute(
path: '/org/:orgId/teams',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.TeamManagementScreen(orgId: orgId),
);
},
routes: [
GoRoute(
path: ':teamId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final teamId = state.pathParameters['teamId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () =>
org.TeamDetailScreen(orgId: orgId, teamId: teamId),
);
},
),
],
),
GoRoute(
path: '/org/:orgId/reports',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgProgressReportScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/assignments/:assignmentId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final assignmentId = state.pathParameters['assignmentId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.AssignmentDetailScreen(
orgId: orgId,
assignmentId: assignmentId,
),
);
},
),
GoRoute(
path: '/org/:orgId/members/:userId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final userId = state.pathParameters['userId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () =>
org.MemberDetailScreen(orgId: orgId, userId: userId),
);
},
),
GoRoute(
path: '/org/:orgId/notifications',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.NotificationCenterScreen(orgId: orgId),
);
},
routes: [
// Per-org preferences for notification channels and types.
// Reuses the global NotificationsSettingsScreen with the
// current org pre-selected so the URL is shareable and
// deep-linkable.
GoRoute(
path: 'preferences',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: settings.loadLibrary,
builder: () => settings.NotificationsSettingsScreen(
preselectedOrgId: orgId,
),
);
},
),
],
),
GoRoute(
path: '/org/:orgId/paths',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.LearningPathsScreen(orgId: orgId),
);
},
routes: [
GoRoute(
path: ':pathId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final pathId = state.pathParameters['pathId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () =>
org.PathDetailScreen(orgId: orgId, pathId: pathId),
);
},
routes: [
GoRoute(
path: 'progress',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final pathId = state.pathParameters['pathId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.PathProgressScreen(
orgId: orgId,
pathId: pathId,
),
);
},
),
],
),
],
),
GoRoute(
path: '/org/:orgId/skills',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.SkillFrameworkScreen(orgId: orgId),
);
},
routes: [
GoRoute(
path: 'gaps',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.SkillGapScreen(orgId: orgId),
);
},
),
],
),
GoRoute(
path: '/org/:orgId/roles',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.RoleProfilesScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/certifications',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.CertificationsScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/compliance',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.ComplianceDashboardScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/certificates/:certId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final certId = state.pathParameters['certId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.CertificateViewScreen(
orgId: orgId,
certificateId: certId,
),
);
},
),
GoRoute(
path: '/org/:orgId/leaderboard',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.LeaderboardScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/badges',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.BadgesScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/gamification-settings',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.GamificationSettingsScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/sso',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.SsoSettingsScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/api-keys',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.ApiKeysScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/webhooks',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.WebhooksScreen(orgId: orgId),
);
},
),
GoRoute(
path: '/org/:orgId/billing',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: org.loadLibrary,
builder: () => org.OrgBillingScreen(orgId: orgId),
);
},
),
],
),
// Org-admin lesson viewer. Lives outside the org detail shell so the
// lesson keeps its own AppBar (no double-AppBar from
// [OrgDetailScaffold]); the URL prefix preserves the org context so
// `context.pop()` from the lesson returns to the assignment detail.
GoRoute(
path: '/org/:orgId/assignments/:assignmentId/topics/:topicId/lesson',
builder: (context, state) {
final topicId = state.pathParameters['topicId']!;
final topicTitle = state.uri.queryParameters['title'];
final domainSlug = state.uri.queryParameters['slug'] ?? '';
return DeferredScreen(
loader: lesson.loadLibrary,
builder: () => lesson.LessonViewerScreen(
topicId: topicId,
topicTitle: topicTitle,
domainSlug: domainSlug,
),
);
},
),
// Backoffice routes (platform admin only)
GoRoute(
path: '/backoffice',
builder: (context, state) => DeferredScreen(
loader: backoffice.loadLibrary,
builder: () => backoffice.PlatformAdminGuard(
child: backoffice.BackofficeScreen(),
),
),
routes: [
GoRoute(
path: 'users',
builder: (context, state) => DeferredScreen(
loader: backoffice.loadLibrary,
builder: () => backoffice.BackofficeUsersScreen(),
),
),
GoRoute(
path: ':orgId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return DeferredScreen(
loader: backoffice.loadLibrary,
builder: () => backoffice.PlatformAdminGuard(
child: backoffice.BackofficeOrgDetailScreen(orgId: orgId),
),
);
},
),
],
),
],
errorBuilder: (context, state) =>
_RouteNotFoundScreen(missingPath: state.matchedLocation),
);
return _activeRouter!;
}