harmonyV

main.dart

Jul 13th, 2026 (edited)
49
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Dart 11.38 KB | None | 0 0
  1. void main() async {
  2.   final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
  3.  
  4.   await SystemChrome.setPreferredOrientations([
  5.     DeviceOrientation.portraitUp,
  6.     DeviceOrientation.portraitDown,
  7.   ]);
  8.  
  9.   FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
  10.   SignalsObserver.instance = null;
  11.  
  12.   final isOperational = await _initServices();
  13.   final initialDataSourceMode =
  14.       await DataSourceModeController.loadInitialMode();
  15.   final localDatabase = await initializeDatabase();
  16.  
  17.   runGutZenApp(
  18.     isOperational: isOperational,
  19.     initialDataSourceMode: initialDataSourceMode,
  20.     localDatabase: localDatabase,
  21.   );
  22. }
  23.  
  24. void runGutZenApp({
  25.   required bool isOperational,
  26.   required DataSourceMode initialDataSourceMode,
  27.   AppDatabase? localDatabase,
  28. }) {
  29.   final startupRoot = _StartupRoot(
  30.     isOperational: isOperational,
  31.     initialDataSourceMode: initialDataSourceMode,
  32.     localDatabase: localDatabase,
  33.   );
  34.  
  35.   runApp(
  36.     ProviderScope(
  37.       overrides: [
  38.         initialDataSourceModeProvider.overrideWithValue(initialDataSourceMode),
  39.         if (localDatabase != null)
  40.           appDatabaseProvider.overrideWithValue(localDatabase),
  41.       ],
  42.       child: Sentry.isEnabled ? SentryWidget(child: startupRoot) : startupRoot,
  43.     ),
  44.   );
  45. }
  46.  
  47. class _StartupRoot extends StatefulWidget {
  48.   const _StartupRoot({
  49.     required this.isOperational,
  50.     required this.initialDataSourceMode,
  51.     required this.localDatabase,
  52.   });
  53.  
  54.   final bool isOperational;
  55.   final DataSourceMode initialDataSourceMode;
  56.   final AppDatabase? localDatabase;
  57.  
  58.   @override
  59.   State<_StartupRoot> createState() => _StartupRootState();
  60. }
  61.  
  62. class _StartupRootState extends State<_StartupRoot> {
  63.   late bool _isOperational;
  64.   AppDatabase? _localDatabase;
  65.   var _isRetrying = false;
  66.  
  67.   @override
  68.   void initState() {
  69.     super.initState();
  70.     _isOperational = widget.isOperational;
  71.     _localDatabase = widget.localDatabase;
  72.   }
  73.  
  74.   Future<void> _retry() async {
  75.     setState(() => _isRetrying = true);
  76.  
  77.     if (!_isOperational) {
  78.       _isOperational = await _initServices();
  79.     }
  80.  
  81.     final localDatabase = _isOperational ? await initializeDatabase() : null;
  82.  
  83.     if (!mounted) return;
  84.  
  85.     if (_isOperational && localDatabase != null) {
  86.       runGutZenApp(
  87.         isOperational: true,
  88.         initialDataSourceMode: widget.initialDataSourceMode,
  89.         localDatabase: localDatabase,
  90.       );
  91.       return;
  92.     }
  93.  
  94.     setState(() {
  95.       _localDatabase = localDatabase;
  96.       _isRetrying = false;
  97.     });
  98.   }
  99.  
  100.   @override
  101.   Widget build(BuildContext context) {
  102.     return PostHogWidget(
  103.       child: _isOperational && _localDatabase != null
  104.           ? const MyApp()
  105.           : _ErrorScreen(
  106.               onRetry: _retry,
  107.               isRetrying: _isRetrying,
  108.             ),
  109.     );
  110.   }
  111. }
  112.  
  113. class MyApp extends ConsumerStatefulWidget {
  114.   const MyApp({super.key});
  115.  
  116.   @override
  117.   ConsumerState<MyApp> createState() => _MyAppState();
  118. }
  119.  
  120. class _MyAppState extends ConsumerState<MyApp> {
  121.   late final AuthStateHandler _authStateHandler;
  122.   late final AppLifecycleListener _listener;
  123.   var _useUpgrader = !Platform.isAndroid; // non-Android always uses upgrader
  124.   var _splashRemoved = false;
  125.  
  126.   Future<void> _checkAndroidUpdate() async {
  127.     if (!Platform.isAndroid || !kReleaseMode) {
  128.       developer.log("Skipping Android update check", name: 'smart_diet');
  129.       return;
  130.     }
  131.  
  132.     try {
  133.       final info = await InAppUpdate.checkForUpdate();
  134.       if (!mounted) return;
  135.  
  136.       if (info.updateAvailability != UpdateAvailability.updateAvailable) {
  137.         return;
  138.       }
  139.  
  140.       if (!info.flexibleUpdateAllowed) {
  141.         if (!_useUpgrader) {
  142.           setState(() => _useUpgrader = true);
  143.         }
  144.         return;
  145.       }
  146.  
  147.       final result = await InAppUpdate.startFlexibleUpdate();
  148.       if (!mounted) return;
  149.       if (result == AppUpdateResult.success) {
  150.         _showUpdateReadySnackbar();
  151.       }
  152.     } catch (error, stack) {
  153.       logError(error, stack: stack, context: 'Check Android update failed');
  154.       if (mounted && !_useUpgrader) {
  155.         setState(() => _useUpgrader = true);
  156.       }
  157.     }
  158.   }
  159.  
  160.   Future<void> _handleUpdateOnResume() async {
  161.     if (!Platform.isAndroid || !kReleaseMode) {
  162.       developer.log(
  163.         "Skipping Android update check on resume",
  164.         name: 'smart_diet',
  165.       );
  166.       return;
  167.     }
  168.  
  169.     try {
  170.       final info = await InAppUpdate.checkForUpdate();
  171.       if (!mounted) return;
  172.  
  173.       if (info.installStatus == InstallStatus.downloaded) {
  174.         _showUpdateReadySnackbar();
  175.         return;
  176.       }
  177.  
  178.       if (info.updateAvailability != UpdateAvailability.updateAvailable) {
  179.         return;
  180.       }
  181.  
  182.       if (!info.flexibleUpdateAllowed) {
  183.         if (!_useUpgrader) {
  184.           setState(() => _useUpgrader = true);
  185.         }
  186.         return;
  187.       }
  188.  
  189.       final result = await InAppUpdate.startFlexibleUpdate();
  190.       if (!mounted) return;
  191.       if (result == AppUpdateResult.success) {
  192.         _showUpdateReadySnackbar();
  193.       }
  194.     } catch (error, stack) {
  195.       logError(error, stack: stack, context: 'Check Android update failed');
  196.       if (mounted && !_useUpgrader) {
  197.         setState(() => _useUpgrader = true);
  198.       }
  199.     }
  200.   }
  201.  
  202.   void _showUpdateReadySnackbar() {
  203.     context.showSnackBar(
  204.       'An update has been downloaded and is ready to install.',
  205.       persist: true,
  206.       action: SnackBarAction(
  207.         label: 'Restart',
  208.         onPressed: () async {
  209.           await InAppUpdate.completeFlexibleUpdate();
  210.         },
  211.       ),
  212.     );
  213.   }
  214.  
  215.   @override
  216.   void initState() {
  217.     super.initState();
  218.  
  219.     unawaited(recordFirstAppOpenIfNeeded());
  220.  
  221.     _authStateHandler = AuthStateHandler(
  222.       onResetAuthState: () => invalidateAuthScopedProviders(ref),
  223.       onSignOut: () => routerConfig.goNamed(AppRoutes.landing),
  224.     );
  225.  
  226.     RevenueCatService.setupRevenueCatListener(
  227.       onCustomerInfoUpdate: (customerInfo) async {
  228.         await ref.read(subscriptionProvider.notifier).updateCustomerInfo();
  229.       },
  230.     );
  231.  
  232.     routerConfig.routerDelegate.addListener(_removeSplashWhenRouterReady);
  233.     WidgetsBinding.instance.addPostFrameCallback(
  234.       (_) => _removeSplashWhenRouterReady(),
  235.     );
  236.  
  237.     _checkAndroidUpdate();
  238.  
  239.     _listener = AppLifecycleListener(
  240.       onResume: () {
  241.         _handleUpdateOnResume();
  242.         AccountActivityService.touch();
  243.       },
  244.     );
  245.  
  246.     // Record activity on launch as well (covers users who rarely background
  247.     // the app between sessions).
  248.     AccountActivityService.touch();
  249.   }
  250.  
  251.   void _removeSplashWhenRouterReady() {
  252.     if (_splashRemoved || !mounted) return;
  253.  
  254.     if (routerConfig.routerDelegate.currentConfiguration.isNotEmpty) {
  255.       _splashRemoved = true;
  256.       FlutterNativeSplash.remove();
  257.       routerConfig.routerDelegate.removeListener(_removeSplashWhenRouterReady);
  258.     }
  259.   }
  260.  
  261.   @override
  262.   void dispose() {
  263.     routerConfig.routerDelegate.removeListener(_removeSplashWhenRouterReady);
  264.     _authStateHandler.dispose();
  265.     _listener.dispose();
  266.     super.dispose();
  267.   }
  268.  
  269.   @override
  270.   Widget build(BuildContext context) {
  271.     return MaterialApp.router(
  272.       debugShowCheckedModeBanner: false,
  273.       theme: AppTheme.light,
  274.       routerConfig: routerConfig,
  275.       builder: (context, child) => _useUpgrader
  276.           ? UpgradeAlert(
  277.               navigatorKey: routerConfig.routerDelegate.navigatorKey,
  278.               upgrader: Upgrader(
  279.                 debugDisplayAlways: kDebugMode,
  280.                 debugLogging: kDebugMode,
  281.               ),
  282.               showIgnore: false,
  283.               child: child,
  284.             )
  285.           : child ?? const _ErrorScreen(),
  286.     );
  287.   }
  288. }
  289.  
  290. class _ErrorScreen extends StatelessWidget {
  291.   const _ErrorScreen({this.onRetry, this.isRetrying = false});
  292.  
  293.   final Future<void> Function()? onRetry;
  294.   final bool isRetrying;
  295.  
  296.   @override
  297.   Widget build(BuildContext context) {
  298.     FlutterNativeSplash.remove();
  299.     final colorScheme = Theme.of(context).colorScheme;
  300.  
  301.     return MaterialApp(
  302.       debugShowCheckedModeBanner: false,
  303.       navigatorObservers: [PosthogObserver()],
  304.       theme: AppTheme.light,
  305.       home: Builder(
  306.         builder: (context) => Scaffold(
  307.           body: Padding(
  308.             padding: const EdgeInsets.all(24),
  309.             child: Column(
  310.               mainAxisAlignment: .center,
  311.               crossAxisAlignment: .stretch,
  312.               spacing: 16,
  313.               children: [
  314.                 MyCard(
  315.                   elevation: .5,
  316.                   padding: const EdgeInsets.all(12),
  317.                   child: Text(
  318.                     appInitializationErrorMessage,
  319.                     style: TextStyle(fontWeight: FontWeight.bold),
  320.                     textAlign: TextAlign.center,
  321.                   ),
  322.                 ),
  323.                 if (onRetry != null)
  324.                   ElevatedButton.icon(
  325.                     icon: Icon(Icons.refresh, color: colorScheme.onPrimary),
  326.                     label: Text(isRetrying ? 'Retrying…' : 'Try again'),
  327.                     onPressed: isRetrying ? null : () => unawaited(onRetry!()),
  328.                   ),
  329.                 if (isRetrying)
  330.                   const Center(child: CircularProgressIndicator()),
  331.                 ElevatedButton.icon(
  332.                   icon: Icon(Icons.error, color: colorScheme.onPrimary),
  333.                   label: const Text('Contact Support'),
  334.                   onPressed: () async {
  335.                     Sentry.isEnabled
  336.                         ? await Navigator.push<void>(
  337.                             context,
  338.                             MaterialPageRoute<void>(
  339.                               fullscreenDialog: true,
  340.                               builder: (context) => SentryFeedbackForm(
  341.                                 associatedEventId: Sentry.lastEventId,
  342.                               ),
  343.                             ),
  344.                           )
  345.                         : await sendEmail(
  346.                             supportEmail,
  347.                             subject: 'App startup issue',
  348.                             body:
  349.                                 'Please describe what you see on screen, the error message, and the actions you took before the error occurred:\n\n',
  350.                           );
  351.                   },
  352.                 ),
  353.               ],
  354.             ),
  355.           ),
  356.         ),
  357.       ),
  358.     );
  359.   }
  360. }
  361.  
  362. Future<bool> _initServices() async {
  363.   // No appRunner: non-web uses OnErrorIntegration (PlatformDispatcher.onError).
  364.   await SentryFlutter.init(
  365.     (options) {
  366.       options.dsn = Env.sentryDsn;
  367.       // Re-tune sampling as traffic/quotas change; see docs/pre-deployment.md.
  368.       options.tracesSampleRate = !kReleaseMode ? 0.0 : 1.0;
  369.  
  370.       // Disable user interaction tracking to avoid notch geometry errors
  371.       options.enableUserInteractionTracing = false;
  372.       options.enableUserInteractionBreadcrumbs = false;
  373.     },
  374.   ).onError((Object error, StackTrace stack) async {
  375.     await Sentry.close();
  376.     developer.log(
  377.       'SentryFlutter.init failed; continuing without Sentry.',
  378.       name: 'smart_diet',
  379.       error: error,
  380.       stackTrace: stack,
  381.     );
  382.   });
  383.  
  384.   if (!await SupabaseServices.init()) {
  385.     return false;
  386.   }
  387.  
  388.   await RevenueCatService.initPlatformState();
  389.   await AppsFlyerService.init();
  390.   await PosthogService.init();
  391.  
  392.   return true;
  393. }
  394.  
Advertisement
Add Comment
Please, Sign In to add comment