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) => SetupScreen(onConnected: onSetupComplete),
),
GoRoute(path: '/login', builder: (context, state) => const LoginScreen()),
GoRoute(
path: '/forgot-password',
builder: (context, state) => const ForgotPasswordScreen(),
),
GoRoute(
path: '/reset-password',
builder: (context, state) => const ResetPasswordScreen(),
),
GoRoute(
path: '/mfa-verify',
builder: (context, state) => const MfaVerifyScreen(),
),
GoRoute(
path: '/mfa-enroll',
builder: (context, state) => const MfaEnrollScreen(),
),
GoRoute(
path: '/persona',
builder: (context, state) => const PersonaScreen(),
),
// First-connection onboarding (Plan 49) — full-screen, no nav bar.
GoRoute(
path: '/onboarding/profile',
builder: (context, state) => const ProfileCompletionScreen(),
),
GoRoute(
path: '/onboarding/welcome',
builder: (context, state) => const OnboardingWelcomeScreen(),
),
GoRoute(
path: '/onboarding/why',
builder: (context, state) => const OnboardingWhyScreen(),
),
GoRoute(
path: '/onboarding/session',
builder: (context, state) => const OnboardingFirstSessionScreen(),
),
GoRoute(
path: '/onboarding/review',
builder: (context, state) => const OnboardingReviewScreen(),
),
GoRoute(
path: '/onboarding/mastery',
builder: (context, state) => const OnboardingMasteryScreen(),
),
GoRoute(
path: '/admin-onboarding/org-picker',
builder: (context, state) => const AdminOnboardingOrgPickerScreen(),
),
GoRoute(
path: '/admin-onboarding/org-basics',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return AdminOnboardingOrgBasicsScreen(orgId: orgId);
},
),
GoRoute(
path: '/admin-onboarding/members',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return AdminOnboardingMembersScreen(orgId: orgId);
},
),
GoRoute(
path: '/admin-onboarding/curriculum',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return AdminOnboardingCurriculumScreen(orgId: orgId);
},
),
GoRoute(
path: '/admin-onboarding/tour',
builder: (context, state) {
final orgId = state.uri.queryParameters['orgId'] ?? '';
return 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) =>
const NoTransitionPage(child: DomainListScreen()),
routes: [
GoRoute(
path: 'curriculum-creator',
builder: (context, state) => const CurriculumCreatorScreen(),
),
GoRoute(
path: 'upload-curriculum',
builder: (context, state) => const UploadCurriculumScreen(),
),
GoRoute(
path: ':domainSlug/topics',
builder: (context, state) {
final domainSlug = state.pathParameters['domainSlug']!;
final domainName = state.uri.queryParameters['name'];
return 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 LessonViewerScreen(
topicId: topicId,
topicTitle: topicTitle,
domainSlug: domainSlug,
);
},
),
],
),
],
),
GoRoute(
path: '/tutor',
pageBuilder: (context, state) =>
const NoTransitionPage(child: 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 ChatScreen(
topicId: topicId,
mode: mode,
topicTitle: topicTitle,
from: from,
isValidation: isValidation,
validationTarget: validationTarget,
);
},
),
],
),
GoRoute(
path: '/review',
pageBuilder: (context, state) =>
const NoTransitionPage(child: ReviewSessionScreen()),
routes: [
GoRoute(
path: 'proposals',
builder: (context, state) => const CardProposalsScreen(),
),
],
),
GoRoute(
path: '/progress',
pageBuilder: (context, state) => NoTransitionPage(
child: ProgressDashboardScreen(
initialTab: state.uri.queryParameters['tab'],
),
),
routes: [
GoRoute(
path: 'guide',
builder: (context, state) => const LearningGuideScreen(),
),
GoRoute(
path: 'overrides',
builder: (context, state) => const 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 NoteDetailScreen(
topicId: topicId,
sessionId: sessionId,
initialContent: content,
);
},
),
GoRoute(
path: 'notes/:noteId',
builder: (context, state) {
final noteId = state.pathParameters['noteId']!;
return NoteDetailScreen(noteId: noteId);
},
),
],
),
GoRoute(
path: '/goals',
pageBuilder: (context, state) =>
const NoTransitionPage(child: GoalsListScreen()),
),
// 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: OrgShellScaffold(child: child)),
),
),
routes: [
GoRoute(
path: '/org',
redirect: (context, state) {
final container = ProviderScope.containerOf(
context,
listen: false,
);
final lastOrg = container
.read(lastActiveOrgIdProvider)
.valueOrNull;
if (lastOrg != null) return '/org/$lastOrg';
final admins = container
.read(adminEligibleOrgsProvider)
.valueOrNull;
if (admins != null && admins.isNotEmpty) {
return '/org/${admins.first.id}';
}
final orgs = container.read(myOrganizationsProvider).valueOrNull;
if (orgs != null && orgs.isNotEmpty) {
return '/org/${orgs.first.id}';
}
return '/';
},
builder: (context, state) => const SizedBox.shrink(),
),
GoRoute(
path: '/org/:orgId',
pageBuilder: (context, state) =>
const NoTransitionPage(child: OrgDashboardScreen()),
),
GoRoute(
path: '/org/:orgId/members',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return MemberManagementScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/assignments',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return CurriculumAssignmentScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/analytics',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return OrgAnalyticsScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/settings',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return OrgSettingsScreen(orgId: orgId);
},
),
],
),
// Other /org/:orgId/* sub-pages — full-screen, no shell layout, each
// with its own AppBar/back button. Kept in a ShellRoute only so the
// auth/entitlement/terms/onboarding gates still wrap them.
ShellRoute(
builder: (context, state, child) => EntitlementBootstrap(
child: TermsGate(child: OnboardingGate(child: child)),
),
routes: [
GoRoute(
path: '/org/:orgId/teams',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return TeamManagementScreen(orgId: orgId);
},
routes: [
GoRoute(
path: ':teamId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final teamId = state.pathParameters['teamId']!;
return TeamDetailScreen(orgId: orgId, teamId: teamId);
},
),
],
),
GoRoute(
path: '/org/:orgId/reports',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return OrgProgressReportScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/assignments/:assignmentId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final assignmentId = state.pathParameters['assignmentId']!;
return 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 MemberDetailScreen(orgId: orgId, userId: userId);
},
),
GoRoute(
path: '/org/:orgId/notifications',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return NotificationCenterScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/paths',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return LearningPathsScreen(orgId: orgId);
},
routes: [
GoRoute(
path: ':pathId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final pathId = state.pathParameters['pathId']!;
return PathDetailScreen(orgId: orgId, pathId: pathId);
},
routes: [
GoRoute(
path: 'progress',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final pathId = state.pathParameters['pathId']!;
return PathProgressScreen(orgId: orgId, pathId: pathId);
},
),
],
),
],
),
GoRoute(
path: '/org/:orgId/skills',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return SkillFrameworkScreen(orgId: orgId);
},
routes: [
GoRoute(
path: 'gaps',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return SkillGapScreen(orgId: orgId);
},
),
],
),
GoRoute(
path: '/org/:orgId/roles',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return RoleProfilesScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/certifications',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return CertificationsScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/compliance',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return ComplianceDashboardScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/certificates/:certId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
final certId = state.pathParameters['certId']!;
return CertificateViewScreen(orgId: orgId, certificateId: certId);
},
),
GoRoute(
path: '/org/:orgId/leaderboard',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return LeaderboardScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/badges',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return BadgesScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/gamification-settings',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return GamificationSettingsScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/sso',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return SsoSettingsScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/api-keys',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return ApiKeysScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/webhooks',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return WebhooksScreen(orgId: orgId);
},
),
GoRoute(
path: '/org/:orgId/billing',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return OrgBillingScreen(orgId: orgId);
},
),
],
),
// Backoffice routes (platform admin only)
GoRoute(
path: '/backoffice',
builder: (context, state) =>
const PlatformAdminGuard(child: BackofficeScreen()),
routes: [
GoRoute(
path: 'users',
builder: (context, state) => const BackofficeUsersScreen(),
),
GoRoute(
path: ':orgId',
builder: (context, state) {
final orgId = state.pathParameters['orgId']!;
return PlatformAdminGuard(
child: BackofficeOrgDetailScreen(orgId: orgId),
);
},
),
],
),
// Settings is outside the shell (no bottom nav)
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsScreen(),
routes: [
GoRoute(
path: 'account',
builder: (_, _) => const AccountSettingsScreen(),
),
GoRoute(
path: 'change-password',
builder: (_, _) => const ChangePasswordScreen(),
),
GoRoute(
path: 'subscription',
builder: (_, _) => const SubscriptionScreen(),
),
GoRoute(
path: 'learning',
builder: (_, _) => const LearningSettingsScreen(),
),
GoRoute(
path: 'notifications',
builder: (_, _) => const NotificationsSettingsScreen(),
),
GoRoute(
path: 'privacy',
builder: (_, _) => const PrivacySettingsScreen(),
),
GoRoute(path: 'ai', builder: (_, _) => const AiSettingsScreen()),
],
),
],
);
return _activeRouter!;
}