main function Overview

void main()

App entry point. Boots Supabase, spawns DuTaToApp, and lazily starts Firebase + notification services in the background so the UI paints without waiting on native init.

Implementation

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // -- Date-format symbols for supported locales --
  //
  // `DateFormat` needs its locale symbol data loaded before it can format for
  // a non-`en_US` locale. Load the app's supported locales up front so the
  // locale-aware `DateFormat` call-sites (driven by `Intl.defaultLocale`, set
  // from the LocalePref provider) render correctly for French too (audit M6).
  await initializeDateFormatting('en');
  await initializeDateFormatting('fr');

  // -- Web URL strategy --
  //
  // Use clean path-based URLs (`/progress`) instead of Flutter's default
  // hash fragment (`/#/progress`). No-op on iOS/Android — the
  // `flutter_web_plugins` public API resolves to a non-web stub off-web, so
  // this compiles and runs everywhere without a `kIsWeb` guard.
  //
  // Without this, only `/#/…` URLs resolve: a bare path leaves the fragment
  // empty and GoRouter falls back to Home. That silently breaks path-style
  // deep links, shared links, browser refreshes, AND the path URLs the app
  // already builds for itself — the Stripe `success_url`/`cancel_url` and
  // billing-portal `return_url` in
  // `supabase/functions/create-{checkout,portal}-session`, plus the
  // `https://dutato.app/org/<id>/billing` link in org_billing_screen.dart.
  //
  // The host side is already wired: Cloudflare Pages serves `index.html` for
  // every unmatched path via `web/_redirects` (`/* /index.html 200`), so the
  // SPA boots and GoRouter parses the real path. Both halves of this
  // contract are locked down by
  // `test/platform/web_url_strategy_config_test.dart`.
  usePathUrlStrategy();

  // -- Supabase (must be before runApp — auth state drives routing) --
  //
  // Production builds keep the consent gate: even with compile-time demo
  // credentials baked in via --dart-define, the user must click "Use Demo
  // Instance" on /setup to opt in. Local dev/audit builds set the
  // DUTATO_SKIP_SETUP flag (see tools/dto/dto_cli/shared.py:build_web for the
  // localhost-detection logic), which trusts the baked-in demo creds and
  // boots the app straight to /login.
  const bool skipSetup = bool.fromEnvironment('DUTATO_SKIP_SETUP');
  bool supabaseReady = false;
  try {
    await SupabaseConfig.load();
    final modeAllowed =
        SupabaseConfig.mode == InstanceMode.custom ||
        (skipSetup && SupabaseConfig.mode == InstanceMode.demo);
    if (SupabaseConfig.isConfigured && modeAllowed) {
      await Supabase.initialize(
        url: SupabaseConfig.url,
        anonKey: SupabaseConfig.anonKey,
        authOptions: const FlutterAuthClientOptions(
          authFlowType: AuthFlowType.pkce,
        ),
      );
      supabaseReady = true;
    }
  } catch (e) {
    dev.log('Supabase init failed: $e', name: 'main');
  }

  // -- Local LLM cleanup (one-time, reclaims disk space on devices that
  // downloaded GGUF weights before the feature was gated off). Runs only
  // when `EnvConfig.enableLocalLlm` is false AND the versioned done-flag
  // isn't stamped yet; otherwise no-op. Awaited so storage writes settle
  // before the first read in `selectedAiProviderProvider`.
  if (!kIsWeb) {
    try {
      await const LocalLlmCleanupService().run(const FlutterSecureStorage());
    } catch (e) {
      dev.log('LocalLlmCleanupService failed: $e', name: 'main');
    }
  }

  // Show the UI immediately — Firebase & notifications init in the background.
  runApp(
    ProviderScope(
      // Riverpod 3 retries failing providers automatically with backoff.
      // We opt out and keep Riverpod 2's semantics — a failed provider stays
      // failed until something asks it to reload — for two reasons:
      //
      //  1. Screens already surface errors with an explicit "Retry" action.
      //     A silent background retry replaces that actionable error with an
      //     indefinite spinner, which is exactly what
      //     test/presentation/screens/no_stuck_loading_smoke_test.dart exists
      //     to prevent.
      //  2. AI calls run on the learner's own API key. Retrying a failing
      //     request behind their back spends their money without asking.
      //
      // Providers that genuinely want retry can opt back in individually via
      // `@Riverpod(retry: ...)`.
      retry: (_, _) => null,
      child: DuTaToApp(supabaseReady: supabaseReady),
    ),
  );
}