import 'dart:convert'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/services.dart' show rootBundle; import 'package:hmg_patient_app_new/services/logger_service.dart'; /// Firebase Remote Config wrapper. Fully generic — the app never hardcodes /// occasion-specific values here; it only knows about the `occasion_config` /// remote key and reads whatever JSON is published on the Firebase console. abstract class RemoteConfigService { /// Must be called once at startup, before the first frame if possible. Future initialize(); /// Fetches the latest values from Firebase (respecting the configured /// minimum fetch interval) and activates them. Future fetchAndActivate(); /// Raw JSON array string currently held for the `occasion_config` key — /// a list of scheduled occasions (bundled default `[]` until the first /// successful fetch/activate). String get occasionConfigRawJson; } class RemoteConfigServiceImp implements RemoteConfigService { static const String occasionConfigKey = 'occasion_config'; static const String _defaultConfigAssetPath = 'assets/json/default_occasion_config.json'; final LoggerService loggerService; final FirebaseRemoteConfig _remoteConfig; RemoteConfigServiceImp({ required this.loggerService, FirebaseRemoteConfig? remoteConfig, }) : _remoteConfig = remoteConfig ?? FirebaseRemoteConfig.instance; @override Future initialize() async { try { await _remoteConfig.setConfigSettings( RemoteConfigSettings( fetchTimeout: const Duration(seconds: 10), // In debug builds, always fetch fresh values so you can test // Remote Config changes instantly without waiting/throttling. // Release/profile builds keep a 1-hour minimum interval — // balances timely occasion-theme rollout against not hammering // Remote Config across the whole user base. minimumFetchInterval: kDebugMode ? Duration.zero : const Duration(hours: 1), ), ); // Bundled fallback so behaviour is deterministic (and identical to the // current app look) before the very first successful remote fetch. final defaultJson = await rootBundle.loadString(_defaultConfigAssetPath); await _remoteConfig.setDefaults({occasionConfigKey: defaultJson}); } catch (ex) { loggerService.logError('RemoteConfigService.initialize error: $ex'); } } @override Future fetchAndActivate() async { try { return await _remoteConfig.fetchAndActivate(); } catch (ex) { loggerService.logError('RemoteConfigService.fetchAndActivate error: $ex'); return false; } } @override String get occasionConfigRawJson { try { final value = _remoteConfig.getString(occasionConfigKey); if (value.isEmpty) return _fallbackDisabledJson; // Validate JSON shape early; if malformed, fall back safely. json.decode(value); return value; } catch (ex) { loggerService.logError('RemoteConfigService.occasionConfigRawJson parse error: $ex'); return _fallbackDisabledJson; } } /// Empty schedule — [OccasionConfigModel.resolveActive] treats this the /// same as "no occasion currently active". static const String _fallbackDisabledJson = '[]'; }