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
2 changes: 2 additions & 0 deletions dart_define.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"WEBTRIT_APP_FIREBASE_ENABLED": true,
"__WEBTRIT_APP_FIREBASE_ENABLED_DESCRIPTION": "Whether Firebase (app, messaging, local push, Remote Config, Installations, Analytics) is initialised. Default true. Disable when the app is embedded in a host that owns the default Firebase app (e.g. the theme configurator's realtime preview) so the embedded app runs Firebase-free.",
"WEBTRIT_APP_DATABASE_LOG_STATEMENTS": false,
"_WEBTRIT_APP_CORE_URL": "",
"WEBTRIT_APP_DEMO_CORE_URL": "http://192.168.10.100:4000",
Expand Down
2 changes: 2 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ maintainability and enabling tools to parse configuration reliably.
- `WEBTRIT_APP_DEMO_CORE_URL` – Demo core URL (_default: **http://localhost:4000**_).
- `WEBTRIT_APP_DATABASE_LOG_STATEMENTS` – Enables logging of database queries (
_default: **false**_).
- `WEBTRIT_APP_FIREBASE_ENABLED` – Whether Firebase is initialised (_default: **true**_). Set to
`false` to run embedded in a host that owns the default Firebase app (configurator realtime preview).
- `_WEBTRIT_APP_CORE_URL` – Custom core URL (optional override).
- `_WEBTRIT_APP_CORE_VERSION_CONSTRAINT` – Core compatibility range.
- `_WEBTRIT_APP_ABOUT_URL` – URL for "About" screen content.
Expand Down
22 changes: 21 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class _AppState extends State<App> {
late final AppBloc appBloc;
late final AppRouter appRouter;

ThemeSettings? _lastHostThemeSettings;
ThemeMode? _lastHostThemeMode;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -104,6 +107,20 @@ class _AppState extends State<App> {
initialTabResolver,
featureAccess.checker,
);

// A host (the configurator's realtime preview) supplies its theme through the
// tree; push it into the AppBloc so AppState stays the single source of truth.
final hostThemeSettings = context.watch<ThemeSettings?>();
if (hostThemeSettings != null && hostThemeSettings != _lastHostThemeSettings) {
_lastHostThemeSettings = hostThemeSettings;
appBloc.add(AppThemeSettingsChanged(hostThemeSettings));
}

final hostThemeMode = context.watch<ThemeMode?>();
if (hostThemeMode != null && hostThemeMode != _lastHostThemeMode) {
_lastHostThemeMode = hostThemeMode;
appBloc.add(AppThemeModeChanged(hostThemeMode));
}
}

@override
Expand Down Expand Up @@ -147,7 +164,10 @@ class _AppState extends State<App> {
deepLinkBuilder: isDeepLinkEnabled ? appRouter.deepLinkBuilder : null,
navigatorObservers: () => [
AppRouterObserver(),
context.read<AppAnalyticsRepository>().createObserver(),
// Skipped when Firebase is disabled (e.g. embedded in the configurator
// preview): the observer would touch FirebaseAnalytics.instance with no
// Firebase app. Lazy provider means it is then never constructed.
if (EnvironmentConfig.FIREBASE_ENABLED) context.read<AppAnalyticsRepository>().createObserver(),
AutoRouteObserver(),
],
reevaluateListenable: ReevaluateListenable.stream(
Expand Down
36 changes: 29 additions & 7 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,27 @@ IsolateContext? _isolateContext;
Future<InstanceRegistry> bootstrap() 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). Gated by the
// WEBTRIT_APP_FIREBASE_ENABLED config so a host that owns the default Firebase
// app (e.g. the theme configurator's realtime preview) can run this app
// Firebase-free by configuring that flag off.
final firebaseEnabled = EnvironmentConfig.FIREBASE_ENABLED;
if (firebaseEnabled) {
await _initFirebaseApp();
await _initFirebaseMessaging();
await _initLocalPushs();
}

// Initialize Components

// App Info & Device Data

final packageInfo = await PackageInfoFactory.init();
final appInfo = await AppInfo.init(FirebaseAppIdProvider());
// FirebaseAppIdProvider uses Firebase Installations; without Firebase use the
// local shared-preferences id provider instead.
final appInfo = await AppInfo.init(
firebaseEnabled ? FirebaseAppIdProvider() : const SharedPreferencesAppIdProvider(),
);
final deviceInfo = await DeviceInfoFactory.init();

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

// Remote configuration
// Remote configuration. Firebase Remote Config needs the Firebase app, so when
// Firebase is disabled fall back to the local shared-preferences cache
// (DefaultRemoteCacheConfigService also implements RemoteConfigService).
final remoteCacheConfigService = await DefaultRemoteCacheConfigService.init();
final cachedRemoteConfigService = await CachedRemoteConfigService.init(remoteCacheConfigService);
RemoteConfigService cachedRemoteConfigService;
if (firebaseEnabled) {
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;
}
} else {
cachedRemoteConfigService = remoteCacheConfigService;
}

final featureAccessStreamFactory = FeatureAccessStreamFactory(
appThemes: appThemes,
Expand Down
8 changes: 8 additions & 0 deletions lib/environment_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ class EnvironmentConfig {
return seconds > 0 ? seconds : compileTime;
}

// Whether Firebase (app, messaging, local push, Remote Config, Installations,
// Analytics) is initialised. Disable it when this app is embedded in a host
// that owns the default Firebase app (the theme configurator's realtime
// preview), so the embedded app runs Firebase-free.
static const FIREBASE_ENABLED__NAME = 'WEBTRIT_APP_FIREBASE_ENABLED';
static bool get FIREBASE_ENABLED =>
_env.boolean(FIREBASE_ENABLED__NAME, const bool.fromEnvironment(FIREBASE_ENABLED__NAME, defaultValue: true));

static const DATABASE_LOG_STATEMENTS__NAME = 'WEBTRIT_APP_DATABASE_LOG_STATEMENTS';
static bool get DATABASE_LOG_STATEMENTS => _env.boolean(
DATABASE_LOG_STATEMENTS__NAME,
Expand Down
35 changes: 28 additions & 7 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:webtrit_phone/environment_config.dart';
import 'package:webtrit_phone/models/models.dart';
import 'package:webtrit_phone/repositories/repositories.dart';
import 'package:webtrit_phone/services/services.dart';
import 'package:webtrit_phone/theme/theme.dart';
import 'package:webtrit_phone/utils/utils.dart';

void main() {
Expand Down Expand Up @@ -66,10 +67,17 @@ void _onRootLogRecord(LogRecord record) {
}

class RootApp extends StatelessWidget {
const RootApp({super.key, required this.instanceRegistry});
const RootApp({super.key, required this.instanceRegistry, this.themeSettings, this.themeMode, this.featureAccess});

final InstanceRegistry instanceRegistry;

/// Optional config supplied by an external host (the configurator's realtime
/// preview) and provided down the tree so the app renders it instead of its
/// own. Null in a normal run, where the app uses its bootstrap-built defaults.
final ThemeSettings? themeSettings;
final ThemeMode? themeMode;
final FeatureAccess? featureAccess;

@override
Widget build(BuildContext context) {
return MultiProvider(
Expand All @@ -85,11 +93,18 @@ class RootApp extends StatelessWidget {
// Provides reactive [FeatureAccess] configuration synchronized with [SystemInfoRepository] and [RemoteConfigService].
//
// Initializes with bootstrap data and updates whenever system information or remote configuration changes.
StreamProvider<FeatureAccess>(
initialData: instanceRegistry.get<FeatureAccess>(),
create: (_) => instanceRegistry.get<FeatureAccessStreamFactory>().create(),
updateShouldNotify: (previous, next) => previous != next,
),
// A host (the configurator's realtime preview) overrides it with a plain value it re-supplies on edits.
if (featureAccess != null)
Provider<FeatureAccess>.value(value: featureAccess!)
else
StreamProvider<FeatureAccess>(
initialData: instanceRegistry.get<FeatureAccess>(),
create: (_) => instanceRegistry.get<FeatureAccessStreamFactory>().create(),
updateShouldNotify: (previous, next) => previous != next,
),
// Optional host-supplied theme; null in a normal run (App falls back to its AppBloc theme).
Provider<ThemeSettings?>.value(value: themeSettings),
Provider<ThemeMode?>.value(value: themeMode),
Provider<SecureStorage>(create: (_) => instanceRegistry.get()),
Provider<AppPermissions>(create: (_) => instanceRegistry.get()),
Provider<AppLogger>(create: (_) => instanceRegistry.get()),
Expand Down Expand Up @@ -147,7 +162,13 @@ class RootApp extends StatelessWidget {
create: (_) => instanceRegistry.get(),
dispose: disposeIfDisposable,
),
RepositoryProvider.value(value: AppAnalyticsRepository(instance: FirebaseAnalytics.instance)),
// Lazy so a Firebase-free host (the configurator's realtime preview)
// never touches FirebaseAnalytics.instance, which throws when the
// host's default Firebase app is misconfigured/absent. The analytics
// observer is only attached when FIREBASE_ENABLED, so it is never read there.
RepositoryProvider<AppAnalyticsRepository>(
create: (_) => AppAnalyticsRepository(instance: FirebaseAnalytics.instance),
),
RepositoryProvider<RegisterStatusRepository>.value(value: registerStatusRepository),
RepositoryProvider<PresenceSettingsRepository>.value(value: presenceSettingsRepository),
RepositoryProvider<QueuedTerminationRequestsRepository>.value(value: queuedTerminationRequestsRepository),
Expand Down
10 changes: 10 additions & 0 deletions test/environment_config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,15 @@ void main() {
EnvironmentConfig.applyOverrides({name: '30'});
expect(EnvironmentConfig.USER_REPOSITORY_POLLING_INTERVAL_SECONDS, 30);
});

test('FIREBASE_ENABLED defaults to true and reflects a false override', () {
expect(EnvironmentConfig.FIREBASE_ENABLED, isTrue);

EnvironmentConfig.applyOverrides({EnvironmentConfig.FIREBASE_ENABLED__NAME: 'false'});
expect(EnvironmentConfig.FIREBASE_ENABLED, isFalse);

EnvironmentConfig.clearOverrides();
expect(EnvironmentConfig.FIREBASE_ENABLED, isTrue);
});
});
}
Loading