main function Overview
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),
),
);
}