diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 766a0a83c..f1d4d65b7 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -21,7 +21,13 @@ import 'package:webtrit_phone/resolvers/resolvers.dart'; final _logger = Logger('AppWidget'); class App extends StatefulWidget { - const App({super.key}); + const App({super.key, this.embeddedPreview = false}); + + /// Whether the app runs embedded inside another Flutter host (realtime + /// preview). When true, Firebase-backed integrations such as the analytics + /// navigator observer are skipped to avoid touching a misconfigured/absent + /// Firebase app. + final bool embeddedPreview; @override State createState() => _AppState(); @@ -147,7 +153,7 @@ class _AppState extends State { deepLinkBuilder: isDeepLinkEnabled ? appRouter.deepLinkBuilder : null, navigatorObservers: () => [ AppRouterObserver(), - context.read().createObserver(), + if (!widget.embeddedPreview) context.read().createObserver(), AutoRouteObserver(), ], reevaluateListenable: ReevaluateListenable.stream( diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index ea28a88d2..897f90023 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -39,20 +39,32 @@ import 'services/services.dart'; // Dart isolates do not share memory -- each background isolate gets its own instance. IsolateContext? _isolateContext; -Future bootstrap() async { +/// Boots the application dependencies. +/// +/// When [embeddedPreview] is true the app is hosted inside another Flutter app +/// (e.g. the theme configurator's realtime preview) that already owns the +/// default Firebase app — which may be misconfigured for this app or absent. +/// In that mode all Firebase side effects are skipped and Firebase-backed +/// services fall back to local, offline-safe implementations so the embedded +/// app can render without a valid Firebase configuration. +Future bootstrap({bool embeddedPreview = false}) async { final registry = InstanceRegistry(); // External SDKs (Side effects only, don't need registration) - await _initFirebaseApp(); - await _initFirebaseMessaging(); - await _initLocalPushs(); + if (!embeddedPreview) { + await _initFirebaseApp(); + await _initFirebaseMessaging(); + await _initLocalPushs(); + } // Initialize Components // App Info & Device Data final packageInfo = await PackageInfoFactory.init(); - final appInfo = await AppInfo.init(FirebaseAppIdProvider()); + final appInfo = await AppInfo.init( + embeddedPreview ? const SharedPreferencesAppIdProvider() : FirebaseAppIdProvider(), + ); final deviceInfo = await DeviceInfoFactory.init(); // Storages @@ -99,7 +111,22 @@ Future bootstrap() async { // Remote configuration final remoteCacheConfigService = await DefaultRemoteCacheConfigService.init(); - final cachedRemoteConfigService = await CachedRemoteConfigService.init(remoteCacheConfigService); + // Firebase Remote Config (and its underlying Installations) may be unavailable + // when the app is embedded in another host (e.g. the theme configurator's + // realtime preview) that owns the default Firebase app, or when offline. Fall + // back to the local shared-preferences cache so bootstrap still completes; + // [DefaultRemoteCacheConfigService] also implements [RemoteConfigService]. + RemoteConfigService cachedRemoteConfigService; + if (embeddedPreview) { + cachedRemoteConfigService = remoteCacheConfigService; + } else { + try { + cachedRemoteConfigService = await CachedRemoteConfigService.init(remoteCacheConfigService); + } catch (e, s) { + Logger('bootstrap').warning('Firebase Remote Config init failed; using local cache fallback', e, s); + cachedRemoteConfigService = remoteCacheConfigService; + } + } final featureAccessStreamFactory = FeatureAccessStreamFactory( appThemes: appThemes, diff --git a/lib/main.dart b/lib/main.dart index b199fc7e7..c67b0ce8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,10 +66,15 @@ void _onRootLogRecord(LogRecord record) { } class RootApp extends StatelessWidget { - const RootApp({super.key, required this.instanceRegistry}); + const RootApp({super.key, required this.instanceRegistry, this.embeddedPreview = false}); final InstanceRegistry instanceRegistry; + /// Whether the app runs embedded inside another Flutter host (e.g. the theme + /// configurator's realtime preview). Forwarded to [App] so Firebase-backed + /// integrations such as the analytics navigator observer are skipped. + final bool embeddedPreview; + @override Widget build(BuildContext context) { return MultiProvider( @@ -147,7 +152,12 @@ class RootApp extends StatelessWidget { create: (_) => instanceRegistry.get(), dispose: disposeIfDisposable, ), - RepositoryProvider.value(value: AppAnalyticsRepository(instance: FirebaseAnalytics.instance)), + // Lazy so embedders that skip Firebase (e.g. the configurator's realtime + // preview) never touch FirebaseAnalytics.instance, which throws when the + // host's default Firebase app is misconfigured/absent. + RepositoryProvider( + create: (_) => AppAnalyticsRepository(instance: FirebaseAnalytics.instance), + ), RepositoryProvider.value(value: registerStatusRepository), RepositoryProvider.value(value: presenceSettingsRepository), RepositoryProvider.value(value: queuedTerminationRequestsRepository), @@ -176,7 +186,7 @@ class RootApp extends StatelessWidget { RepositoryProvider(create: (_) => instanceRegistry.get()), RepositoryProvider(create: (_) => instanceRegistry.get()), ], - child: const App(), + child: App(embeddedPreview: embeddedPreview), ); }, ),