Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<App> createState() => _AppState();
Expand Down Expand Up @@ -147,7 +153,7 @@ class _AppState extends State<App> {
deepLinkBuilder: isDeepLinkEnabled ? appRouter.deepLinkBuilder : null,
navigatorObservers: () => [
AppRouterObserver(),
context.read<AppAnalyticsRepository>().createObserver(),
if (!widget.embeddedPreview) context.read<AppAnalyticsRepository>().createObserver(),
AutoRouteObserver(),
],
reevaluateListenable: ReevaluateListenable.stream(
Expand Down
39 changes: 33 additions & 6 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,32 @@ import 'services/services.dart';
// Dart isolates do not share memory -- each background isolate gets its own instance.
IsolateContext? _isolateContext;

Future<InstanceRegistry> 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<InstanceRegistry> 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
Expand Down Expand Up @@ -99,7 +111,22 @@ Future<InstanceRegistry> 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,
Expand Down
16 changes: 13 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<AppAnalyticsRepository>(
create: (_) => AppAnalyticsRepository(instance: FirebaseAnalytics.instance),
),
RepositoryProvider<RegisterStatusRepository>.value(value: registerStatusRepository),
RepositoryProvider<PresenceSettingsRepository>.value(value: presenceSettingsRepository),
RepositoryProvider<QueuedTerminationRequestsRepository>.value(value: queuedTerminationRequestsRepository),
Expand Down Expand Up @@ -176,7 +186,7 @@ class RootApp extends StatelessWidget {
RepositoryProvider<UserLocalDatasource>(create: (_) => instanceRegistry.get()),
RepositoryProvider<AuthRepository>(create: (_) => instanceRegistry.get()),
],
child: const App(),
child: App(embeddedPreview: embeddedPreview),
);
},
),
Expand Down
Loading