createRouter function

GoRouter createRouter({
  1. required bool supabaseReady,
  2. 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!;
}