import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'state/app_scope.dart'; import 'state/app_state.dart'; import 'ui/screens/auth/login_screen.dart'; import 'ui/screens/shell_screen.dart'; import 'ui/tokens.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); // Draw behind the system bars — expected on Android 15+ and the reason the // screens manage their own SafeArea insets. SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); runApp(const DoormileApp()); } /// Doormile — customer app. /// /// Book pickup → Track → Delivered. /// /// `lib/data/doormile_api.dart` is the service surface, and it has exactly one /// implementation: the live customer API. Nothing on screen is invented. class DoormileApp extends StatefulWidget { const DoormileApp({super.key, this.initialState}); /// Injectable for tests. final AppState? initialState; @override State createState() => _DoormileAppState(); } class _DoormileAppState extends State { late final AppState _state = widget.initialState ?? AppState(); @override void initState() { super.initState(); // A returning customer's session outlives the process: the refresh token is // good for 60 days. Look for it before deciding which screen to open. unawaited(_state.restoreSession()); } @override void dispose() { _state.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AppScope( state: _state, child: MaterialApp( title: 'Doormile', debugShowCheckedModeBanner: false, theme: buildDoormileTheme(), home: const _Launch(), // No text-scale clamp: components adapt instead. Rows grow, important // text wraps, and ellipsis is reserved for secondary information. builder: (context, child) { return AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: Brightness.dark, ), child: child ?? const SizedBox.shrink(), ); }, ), ); } } ThemeData buildDoormileTheme() { const scheme = ColorScheme.light( primary: DmColors.brand, onPrimary: Colors.white, secondary: DmColors.brand, onSecondary: Colors.white, surface: DmColors.surface, onSurface: DmColors.ink, error: DmColors.danger, ); return ThemeData( useMaterial3: true, fontFamily: 'Manrope', colorScheme: scheme, scaffoldBackgroundColor: DmColors.bg, splashFactory: InkSparkle.splashFactory, highlightColor: const Color(0x0A17110F), textSelectionTheme: const TextSelectionThemeData( cursorColor: DmColors.brand, selectionColor: Color(0x338F0F06), selectionHandleColor: DmColors.brand, ), textTheme: const TextTheme().apply( bodyColor: DmColors.ink, displayColor: DmColors.ink, ), dividerTheme: const DividerThemeData( color: DmColors.hairline, thickness: 1, space: 1, ), pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: _DmPageTransitions(), TargetPlatform.iOS: _DmPageTransitions(), TargetPlatform.macOS: _DmPageTransitions(), TargetPlatform.windows: _DmPageTransitions(), TargetPlatform.linux: _DmPageTransitions(), }, ), ); } /// Forward navigation slides in from the right and fades, matching the /// prototype's screen transition. class _DmPageTransitions extends PageTransitionsBuilder { const _DmPageTransitions(); @override Widget buildTransitions( PageRoute route, BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { final curved = CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, ); return FadeTransition( opacity: curved, child: SlideTransition( position: Tween( begin: const Offset(0.06, 0), end: Offset.zero, ).animate(curved), child: child, ), ); } } /// What the app opens on. /// /// Three states, and the middle one is the reason this widget exists: until the /// stored session has been looked for we do not yet know whether this customer /// is signed in, and showing them a login form that vanishes a moment later is /// worse than showing them nothing. The Android 12 splash is still up for most /// of it. class _Launch extends StatelessWidget { const _Launch(); @override Widget build(BuildContext context) { final app = AppScope.of(context); if (app.restoringSession) { return const Scaffold(backgroundColor: DmColors.canvas, body: SizedBox()); } return app.isSignedIn ? const ShellScreen() : const LoginScreen(); } }