diff --git a/assets/json/default_occasion_config.json b/assets/json/default_occasion_config.json index 6b38a4cb..fe51488c 100644 --- a/assets/json/default_occasion_config.json +++ b/assets/json/default_occasion_config.json @@ -1,12 +1 @@ -{ - "key": "none", - "enabled": false, - "start_date": null, - "end_date": null, - "primary_color": null, - "secondary_color": null, - "background_opacity_image": null, - "background_image_opacity": 0.2, - "header_image": null -} - +[] diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 4145e78f..aa265931 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -317,10 +317,8 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - // static final double appVersionID = 21.9; - static final double appVersionID = 21.5; + static final double appVersionID = 21.6; - // static final double appVersionID = 50.7; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; static final String appGeneralId = "Cs2020@2016\$2958"; diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index 42d384c0..30e251e1 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -608,7 +608,6 @@ class PushNotificationHandler { // Permission.audio, // Permission.microphone, ].request(); - log("=-=-=-=-=-=-=-=-=-=-"); log(statuses[Permission.notification].toString()); } } catch (_) { diff --git a/lib/features/common/models/family_file_request.dart b/lib/features/common/models/family_file_request.dart index 047b9af0..e3802bd5 100644 --- a/lib/features/common/models/family_file_request.dart +++ b/lib/features/common/models/family_file_request.dart @@ -47,7 +47,7 @@ class FamilyFileRequest { "SharedPatientIdentificationID": sharedPatientIdentificationId, "SearchType": searchType, "SharedPatientMobileNumber": sharedPatientMobileNumber, - "zipCode": zipCode, + "ZipCode": zipCode, "isRegister": isRegister, "PatientStatus": patientStatus, "isDentalAllowedBackend": isDentalAllowedBackend, diff --git a/lib/features/occasion_theme/occasion_config_model.dart b/lib/features/occasion_theme/occasion_config_model.dart index bf9d713f..26ddf733 100644 --- a/lib/features/occasion_theme/occasion_config_model.dart +++ b/lib/features/occasion_theme/occasion_config_model.dart @@ -17,9 +17,12 @@ class OccasionConfigModel { /// default/current theme, regardless of any other field below. final bool enabled; - /// Optional activation window. When both are present, the theme is only - /// considered active if `DateTime.now()` falls within [startDate, endDate]. - /// When null, the [enabled] flag alone controls activation. + /// Activation window. Both must be present and `DateTime.now()` must fall + /// within [startDate, endDate] for this occasion to be eligible — a + /// missing date on either side makes the occasion inactive (dates are + /// mandatory, not an optional override of [enabled]). This lets several + /// occasions be scheduled in advance in the same list (see + /// [OccasionConfigModel.resolveActive]) without colliding. final DateTime? startDate; final DateTime? endDate; @@ -66,13 +69,9 @@ class OccasionConfigModel { return OccasionConfigModel( key: (json['key'] as String?) ?? 'none', enabled: (json['enabled'] as bool?) ?? false, + // enabled: true, startDate: _parseDate(json['start_date']), endDate: _parseDate(json['end_date']), - - // enabled: true, - // startDate: _parseDate("2026-09-30T00:00:00Z"), - // endDate: _parseDate("2026-10-07T00:00:00Z"), - primaryColorHex: json['primary_color'] as String?, backgroundOpacityImage: json['background_opacity_image'] as String?, backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0, @@ -80,6 +79,25 @@ class OccasionConfigModel { ); } + /// Parses the remote-config value as a JSON array of occasions — lets + /// several occasions be scheduled in advance (e.g. National Day, Founding + /// Day, Eid), each with its own non-overlapping [startDate]/[endDate] + /// window, all under the same `occasion_config` remote-config key. + static List listFromJson(List jsonList) { + return jsonList.map((e) => OccasionConfigModel.fromJson(e as Map)).toList(); + } + + /// Picks whichever scheduled occasion is enabled and whose + /// [startDate]/[endDate] window contains `now` — first match in list + /// order wins, so entries are expected not to overlap. Returns + /// [OccasionConfigModel.disabled] when nothing matches. + static OccasionConfigModel resolveActive(List configs, [DateTime? now]) { + for (final config in configs) { + if (config.enabled && config.isWithinActiveWindow(now)) return config; + } + return OccasionConfigModel.disabled(); + } + static DateTime? _parseDate(dynamic value) { if (value == null) return null; if (value is String && value.isNotEmpty) { @@ -89,11 +107,13 @@ class OccasionConfigModel { } /// Whether "now" falls inside the configured activation window. - /// If no window is configured, the [enabled] flag alone decides. + /// Both [startDate] and [endDate] are required — if either is missing, + /// this occasion is never considered active (see field doc above). bool isWithinActiveWindow([DateTime? now]) { + if (startDate == null || endDate == null) return false; final current = now ?? DateTime.now(); - if (startDate != null && current.isBefore(startDate!)) return false; - if (endDate != null && current.isAfter(endDate!)) return false; + if (current.isBefore(startDate!)) return false; + if (current.isAfter(endDate!)) return false; return true; } @@ -125,4 +145,3 @@ class OccasionConfigModel { 'header_image': headerImage, }; } - diff --git a/lib/features/occasion_theme/occasion_theme_view_model.dart b/lib/features/occasion_theme/occasion_theme_view_model.dart index 9dd22b09..73f94ec7 100644 --- a/lib/features/occasion_theme/occasion_theme_view_model.dart +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -35,8 +35,17 @@ class OccasionThemeViewModel extends ChangeNotifier { OccasionConfigModel _config = OccasionConfigModel.disabled(); OccasionConfigModel get config => _config; + /// Testing-only override for "now" when resolving which scheduled + /// occasion is active — set this instead of changing the device clock to + /// preview each occasion in the list. Only ever takes effect in debug + /// builds (see [_now]); always ignored in profile/release, so it can + /// never leak into production even if accidentally left set. + static DateTime? debugNowOverride; + + DateTime? get _now => kDebugMode ? debugNowOverride : null; + /// True only when enabled AND (if a window is configured) within it. - bool get isOccasionActive => _config.enabled && _config.isWithinActiveWindow(); + bool get isOccasionActive => _config.enabled && _config.isWithinActiveWindow(_now); String? get backgroundOpacityImage => isOccasionActive ? _config.backgroundOpacityImage : null; @@ -66,14 +75,20 @@ class OccasionThemeViewModel extends ChangeNotifier { } } - /// Fetches the latest occasion config from Firebase Remote Config and - /// applies it. Safe to call again later (e.g. on app resume) to pick up - /// changes without requiring an app update. + /// Fetches the latest occasion schedule from Firebase Remote Config and + /// applies whichever entry is currently active. Safe to call again later + /// (e.g. on app resume) to pick up changes without requiring an app update. Future refresh() async { try { await _remoteConfigService.fetchAndActivate(); final rawJson = _remoteConfigService.occasionConfigRawJson; - _config = OccasionConfigModel.fromJson(json.decode(rawJson) as Map); + final configs = OccasionConfigModel.listFromJson(json.decode(rawJson) as List); + _config = OccasionConfigModel.resolveActive(configs, _now); + _loggerService.logInfo('OccasionThemeViewModel: chosen theme = "${_config.key}" (enabled=${_config.enabled}, active=$isOccasionActive)'); + _loggerService.logInfo('OccasionThemeViewModel: chosen theme object = ${_config.toJson()}'); + // Cache the full schedule (not just the resolved entry) so offline + // restore at next startup re-resolves against that day's date instead + // of replaying whichever occasion happened to be active last time. await _cacheService.saveString(key: _cachedConfigKey, value: rawJson); _applyToAppColors(); notifyListeners(); @@ -91,7 +106,8 @@ class OccasionThemeViewModel extends ChangeNotifier { _config = OccasionConfigModel.disabled(); return; } - _config = OccasionConfigModel.fromJson(json.decode(cachedJson) as Map); + final configs = OccasionConfigModel.listFromJson(json.decode(cachedJson) as List); + _config = OccasionConfigModel.resolveActive(configs, _now); } catch (ex) { _loggerService.logError('OccasionThemeViewModel._restoreCachedConfig error: $ex'); _config = OccasionConfigModel.disabled(); diff --git a/lib/main.dart b/lib/main.dart index 5e527909..47416b49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -73,7 +73,6 @@ import 'core/utils/size_utils.dart'; import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; - @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -144,7 +143,11 @@ Future callInitializations() async { // Initialize Security Service early to catch threats before app logic runs if (kReleaseMode) { - await getIt.get().initialize(); + try { + await getIt.get().initialize(); + } catch (e) { + getIt.get().logError('SecurityService.initialize failed, continuing app startup: $e'); + } } // Set up critical threat callback to navigate to unsafe device page @@ -177,6 +180,8 @@ Future callInitializations() async { // disabled by default — the app looks exactly as it does today until // Remote Config enables an occasion (e.g. National Day) on the server side. final occasionThemeViewModel = getIt.get(); + OccasionThemeViewModel.debugNowOverride = DateTime.now(); + // OccasionThemeViewModel.debugNowOverride = DateTime.parse('2027-03-09T00:00:00Z'); unawaited(occasionThemeViewModel.initialize()); } diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index bef2df93..992f512e 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -858,7 +858,7 @@ class _AppointmentPaymentPageState extends State { print("onBrowserLoadStart"); print(url); - if (selectedPaymentMethod == "tamara") { + if (selectedPaymentMethod.toLowerCase() == "tamara") { if (Platform.isAndroid) { Uri uri = new Uri.dataFromString(url); tamaraPaymentStatus = uri.queryParameters['status']!; @@ -897,7 +897,7 @@ class _AppointmentPaymentPageState extends State { void checkPaymentStatus() async { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context)); - if (selectedPaymentMethod == "TAMARA") { + if (selectedPaymentMethod.toLowerCase() == "tamara") { await payfortViewModel.checkTamaraPaymentStatus( transactionID: transID, onSuccess: (apiResponse) async { diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index b205063c..b9df0744 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -781,7 +781,7 @@ class _ImmediateLiveCarePaymentPageState extends State { // inline with the rest of the content. final hasStickyBanner = context.watch().headerImage?.isNotEmpty ?? false; + log("hasStickyBanner: $hasStickyBanner"); return Stack( children: [ // Renders nothing unless a remote-config occasion theme (e.g. diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart index 617539aa..d113f06d 100644 --- a/lib/services/logger_service.dart +++ b/lib/services/logger_service.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:logger/logger.dart'; abstract class LoggerService { @@ -18,6 +20,6 @@ class LoggerServiceImp implements LoggerService { @override void logInfo(String message) { - logger.d(message); + log(message); } } diff --git a/lib/services/remote_config_service.dart b/lib/services/remote_config_service.dart index 568bd4e6..d39a7fbe 100644 --- a/lib/services/remote_config_service.dart +++ b/lib/services/remote_config_service.dart @@ -16,8 +16,9 @@ abstract class RemoteConfigService { /// minimum fetch interval) and activates them. Future fetchAndActivate(); - /// Raw JSON string currently held for the `occasion_config` key - /// (bundled default until the first successful fetch/activate). + /// 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; } @@ -41,7 +42,9 @@ class RemoteConfigServiceImp implements RemoteConfigService { fetchTimeout: const Duration(seconds: 10), // In debug builds, always fetch fresh values so you can test // Remote Config changes instantly without waiting/throttling. - // Release builds keep a sane 1-hour minimum interval. + // 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), ), ); @@ -79,5 +82,7 @@ class RemoteConfigServiceImp implements RemoteConfigService { } } - static const String _fallbackDisabledJson = '{"key":"none","enabled":false}'; + /// Empty schedule — [OccasionConfigModel.resolveActive] treats this the + /// same as "no occasion currently active". + static const String _fallbackDisabledJson = '[]'; } diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index 5eaa313c..1d3b6fa4 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -314,6 +314,8 @@ class MyInAppBrowser extends InAppBrowser { if (isLiveCareAppo) { form = form.replaceFirst('IS_SCHEDULE_VALUE', "true"); + } else { + form = form.replaceFirst('IS_SCHEDULE_VALUE', "false"); } var bytes = utf8.encode(form); diff --git a/pubspec.lock b/pubspec.lock index 6b0a4d35..29bc55a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: "direct main" description: name: clarity_flutter - sha256: b3a114fec6e873caf03cd59a32953f8cb4b1693bd3b4f749ef3030fea027ebce + sha256: "4d46afd76c31d3876f23384d44d6f5728a556daff6cec8b2c98d251dac14899e" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.10.0" clock: dependency: transitive description: @@ -165,18 +165,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "7.3.1" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" convert: dependency: transitive description: @@ -534,10 +534,9 @@ packages: flutter_amazonpaymentservices: dependency: "direct main" description: - name: flutter_amazonpaymentservices - sha256: "23f45fd3b1e12a0933da3704d66cebd19c4c41e28a50d4fd458d3e3e6311f332" - url: "https://pub.dev" - source: hosted + path: "package/flutter_amazonpaymentservices" + relative: true + source: path version: "0.0.13" flutter_cache_manager: dependency: transitive @@ -1213,10 +1212,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.20.3" + version: "0.20.2" jiffy: dependency: "direct main" description: @@ -1301,26 +1300,26 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + sha256: ecf24edf2283c509ecd217e3595f6f71034b68888d28ad1dae6bfa0857b816ac url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "3.0.2" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 + sha256: "76c389998fe95cff4352644776ab5775f24ddd58f179dbf3df1b9c268cbbadc4" url: "https://pub.dev" source: hosted - version: "1.0.56" + version: "2.2.0" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + sha256: b1b2d938cc3511512e552d92e782e055b8dc29c83e21c59e6ecfc6bd79ab0744 url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.4" local_auth_platform_interface: dependency: transitive description: @@ -1333,10 +1332,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + sha256: d72b8a441e5e6a679902f87c51529fcce65f442d77598484cf35454c3d733eda url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "2.0.2" location: dependency: "direct main" description: @@ -1405,10 +1404,10 @@ packages: dependency: transitive description: name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.20" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -1421,10 +1420,10 @@ packages: dependency: transitive description: name: meta - sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1797,10 +1796,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_android: dependency: transitive description: @@ -2018,10 +2017,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.11" time: dependency: transitive description: @@ -2154,10 +2153,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.2.0" video_player: dependency: transitive description: @@ -2303,5 +2302,5 @@ packages: source: hosted version: "1.2.0" sdks: - dart: ">=3.11.0-0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0"