Skip to content
Merged
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
44 changes: 44 additions & 0 deletions lib/app/firebase_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:webtrit_phone/common/common.dart';
import 'package:webtrit_phone/repositories/repositories.dart';
import 'package:webtrit_phone/services/services.dart';

/// Strategy for the Firebase-dependent integrations that [bootstrap] wires up.
///
/// Standalone builds use the default [FirebaseIntegrationEnabled] (defined in
/// bootstrap.dart, next to the Firebase init functions). An embedder that owns
/// the default Firebase app - the theme configurator's realtime preview - passes
/// [FirebaseIntegrationDisabled] so the app runs Firebase-free. The choice is a
/// single polymorphic decision at the composition root; bootstrap consumes the
/// interface uniformly, with no feature-flag branching.
abstract interface class FirebaseIntegration {
/// Side-effect platform initialisation (Firebase app, messaging, local pushes).
Future<void> initPlatform();

/// Source of the app-instance id.
AppIdProvider get appIdProvider;

/// Resolves the remote configuration service. [cache] is the always-available
/// local cache, used as the fallback (or as the sole source when disabled).
Future<RemoteConfigService> remoteConfig(DefaultRemoteCacheConfigService cache);

/// Analytics repository (navigator observer source).
AppAnalyticsRepository get analytics;
}

/// Firebase-free integration: no platform init, the local id provider, the local
/// cache as the remote config source, and no-op analytics.
class FirebaseIntegrationDisabled implements FirebaseIntegration {
const FirebaseIntegrationDisabled();

@override
Future<void> initPlatform() async {}

@override
AppIdProvider get appIdProvider => const SharedPreferencesAppIdProvider();

@override
Future<RemoteConfigService> remoteConfig(DefaultRemoteCacheConfigService cache) async => cache;

@override
AppAnalyticsRepository get analytics => const NoopAppAnalyticsRepository();
}
53 changes: 45 additions & 8 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import 'package:webtrit_phone/features/call/call.dart'

import 'package:drift/isolate.dart';

import 'app/firebase_integration.dart';
import 'app/session/session.dart';
import 'firebase_options.dart';
import 'services/services.dart';
Expand All @@ -39,20 +40,21 @@ import 'services/services.dart';
// Dart isolates do not share memory -- each background isolate gets its own instance.
IsolateContext? _isolateContext;

Future<InstanceRegistry> bootstrap() async {
Future<InstanceRegistry> bootstrap({FirebaseIntegration firebase = const FirebaseIntegrationEnabled()}) async {
final registry = InstanceRegistry();

// External SDKs (Side effects only, don't need registration)
await _initFirebaseApp();
await _initFirebaseMessaging();
await _initLocalPushs();
// External SDKs (side effects only, don't need registration). The [firebase]
// strategy decides whether these run: standalone wires Firebase, while an
// embedder that owns the default Firebase app (e.g. the theme configurator's
// realtime preview) passes a disabled strategy so the app runs Firebase-free.
await firebase.initPlatform();

// Initialize Components

// App Info & Device Data

final packageInfo = await PackageInfoFactory.init();
final appInfo = await AppInfo.init(FirebaseAppIdProvider());
final appInfo = await AppInfo.init(firebase.appIdProvider);
final deviceInfo = await DeviceInfoFactory.init();

// Storages
Expand Down Expand Up @@ -97,9 +99,12 @@ Future<InstanceRegistry> bootstrap() async {
apiClientFactory: apiClientFactory,
);

// Remote configuration
// Remote configuration. The Firebase-backed service needs the Firebase app, so
// the strategy resolves it (with a local-cache fallback); a disabled strategy
// just uses the local cache (DefaultRemoteCacheConfigService also implements
// RemoteConfigService).
final remoteCacheConfigService = await DefaultRemoteCacheConfigService.init();
final cachedRemoteConfigService = await CachedRemoteConfigService.init(remoteCacheConfigService);
final cachedRemoteConfigService = await firebase.remoteConfig(remoteCacheConfigService);

final featureAccessStreamFactory = FeatureAccessStreamFactory(
appThemes: appThemes,
Expand Down Expand Up @@ -221,6 +226,7 @@ Future<InstanceRegistry> bootstrap() async {
registry.register<WebtritApiClientFactory>(apiClientFactory);
registry.register<RemoteConfigService>(cachedRemoteConfigService);
registry.register<ConnectivityService>(connectivityService);
registry.register<AppAnalyticsRepository>(firebase.analytics);

// Final side-effect initializations that rely on registered components
await _initCallkeep(featureAccess);
Expand All @@ -229,6 +235,37 @@ Future<InstanceRegistry> bootstrap() async {
return registry;
}

/// Standalone integration: real Firebase platform init, the Firebase id provider,
/// Firebase Remote Config (with a local-cache fallback) and Firebase Analytics.
/// This is the default [bootstrap] strategy. Lives here so it can reuse the
/// private Firebase init functions below.
class FirebaseIntegrationEnabled implements FirebaseIntegration {
const FirebaseIntegrationEnabled();

@override
Future<void> initPlatform() async {
await _initFirebaseApp();
await _initFirebaseMessaging();
await _initLocalPushs();
}

@override
AppIdProvider get appIdProvider => FirebaseAppIdProvider();

@override
Future<RemoteConfigService> remoteConfig(DefaultRemoteCacheConfigService cache) async {
try {
return await CachedRemoteConfigService.init(cache);
} catch (e, s) {
Logger('bootstrap').warning('Firebase Remote Config init failed; using local cache fallback', e, s);
return cache;
}
}

@override
AppAnalyticsRepository get analytics => FirebaseAppAnalyticsRepository();
}

/// Creates the platform [ConnectivityChecker] used by [ConnectivityService]
/// for HTTP liveness probes. Picks the custom-URL implementation if a
/// dedicated check endpoint is provided via [EnvironmentConfig], otherwise
Expand Down
5 changes: 3 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:drift/drift.dart';
import 'package:drift/isolate.dart';

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -147,7 +146,9 @@ class RootApp extends StatelessWidget {
create: (_) => instanceRegistry.get(),
dispose: disposeIfDisposable,
),
RepositoryProvider.value(value: AppAnalyticsRepository(instance: FirebaseAnalytics.instance)),
// Built by bootstrap's Firebase integration strategy: the Firebase-backed
// repository standalone, a no-op one when Firebase is disabled.
RepositoryProvider<AppAnalyticsRepository>(create: (_) => instanceRegistry.get()),
RepositoryProvider<RegisterStatusRepository>.value(value: registerStatusRepository),
RepositoryProvider<PresenceSettingsRepository>.value(value: presenceSettingsRepository),
RepositoryProvider<QueuedTerminationRequestsRepository>.value(value: queuedTerminationRequestsRepository),
Expand Down
21 changes: 19 additions & 2 deletions lib/repositories/app_analytics/app_analytics_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import 'package:flutter/widgets.dart';

import 'package:firebase_analytics/firebase_analytics.dart';

class AppAnalyticsRepository {
AppAnalyticsRepository({required FirebaseAnalytics instance}) : _instance = instance;
/// Source of the navigator observer used to report screen views to analytics.
abstract interface class AppAnalyticsRepository {
NavigatorObserver createObserver();
}

/// Firebase Analytics-backed implementation.
class FirebaseAppAnalyticsRepository implements AppAnalyticsRepository {
FirebaseAppAnalyticsRepository({FirebaseAnalytics? instance}) : _instance = instance ?? FirebaseAnalytics.instance;

final FirebaseAnalytics _instance;

@override
NavigatorObserver createObserver() {
return FirebaseAnalyticsObserver(
analytics: _instance,
Expand All @@ -26,3 +33,13 @@ class AppAnalyticsRepository {
);
}
}

/// No-op implementation for builds that run without Firebase (e.g. embedded in a
/// host that owns the default Firebase app). Attaches a plain observer that
/// records nothing, so consumers need no Firebase-aware branching.
class NoopAppAnalyticsRepository implements AppAnalyticsRepository {
const NoopAppAnalyticsRepository();

@override
NavigatorObserver createObserver() => NavigatorObserver();
}
Loading