From 8b9f4d98f337101aa2654871675b65d476363053 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Tue, 22 Sep 2026 11:49:37 +0300 Subject: [PATCH] theme code updated. --- assets/json/default_occasion_config.json | 11 ++ ios/Runner.xcodeproj/project.pbxproj | 8 -- lib/core/api_consts.dart | 2 +- lib/core/dependencies.dart | 10 ++ .../occasion_theme/occasion_config_model.dart | 117 ++++++++++++++++++ .../occasion_theme_view_model.dart | 102 +++++++++++++++ lib/main.dart | 21 +++- .../appointments/appointment_queue_page.dart | 2 +- .../widgets/ask_doctor_appointment_card.dart | 2 +- .../blood_donation/blood_donation_page.dart | 2 +- .../contact_us/feedback_page.dart | 2 +- lib/presentation/contact_us/find_us_page.dart | 2 +- .../hmg_services/services_page.dart | 2 +- lib/presentation/home/landing_page.dart | 14 ++- .../home/widgets/occasion_banner_widget.dart | 61 +++++++++ .../insurance_approval_details_page.dart | 2 +- .../widgets/insurance_approval_card.dart | 4 +- .../insurance/widgets/insurance_history.dart | 4 +- .../widgets/patient_insurance_card.dart | 4 +- .../eye_measurement_details_page.dart | 2 +- .../medical_file/medical_file_page.dart | 2 +- .../my_family/widget/family_cards.dart | 6 +- .../widgets/invoice_list_card.dart | 6 +- .../widgets/pharmacy_invoice_card.dart | 6 +- .../prescriptions/prescription_item_view.dart | 4 +- .../profile_settings/profile_settings.dart | 2 +- .../widgets/family_card_widget.dart | 2 +- .../refund_request/refund_history_screen.dart | 4 +- .../symptoms_checker/risk_factors_screen.dart | 2 +- .../symptoms_checker/suggestions_screen.dart | 2 +- .../symptoms_selector_screen.dart | 2 +- .../symptoms_checker/triage_screen.dart | 4 +- .../user_info_flow_manager.dart | 8 +- .../user_info_selection_screen.dart | 2 +- .../widgets/pinch_zoom_tutorial_overlay.dart | 2 +- .../widgets/triage_level_card.dart | 2 +- .../widgets/cup_bottomsheet_widgets.dart | 6 +- .../weather/weather_details_page.dart | 4 +- lib/services/remote_config_service.dart | 84 +++++++++++++ lib/theme/colors.dart | 24 +++- .../bottom_navigation/bottom_navigation.dart | 11 +- .../date_range_calender.dart | 10 +- lib/widgets/map/HMSMap.dart | 2 +- lib/widgets/map/gms_map.dart | 2 +- pubspec.yaml | 1 + 45 files changed, 502 insertions(+), 72 deletions(-) create mode 100644 assets/json/default_occasion_config.json create mode 100644 lib/features/occasion_theme/occasion_config_model.dart create mode 100644 lib/features/occasion_theme/occasion_theme_view_model.dart create mode 100644 lib/presentation/home/widgets/occasion_banner_widget.dart create mode 100644 lib/services/remote_config_service.dart diff --git a/assets/json/default_occasion_config.json b/assets/json/default_occasion_config.json new file mode 100644 index 00000000..37740e39 --- /dev/null +++ b/assets/json/default_occasion_config.json @@ -0,0 +1,11 @@ +{ + "key": "none", + "enabled": false, + "start_date": null, + "end_date": null, + "primary_color": null, + "background_opacity_image": null, + "background_image_opacity": 0.2, + "header_image": null +} + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f52d12fc..21dccceb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -339,14 +339,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -360,14 +356,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 45d6f48c..68f2b21a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index b6c7fb38..b70de321 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -84,7 +84,9 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/services/remote_config_service.dart'; import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; @@ -383,5 +385,13 @@ class AppDependencies { getIt.registerLazySingleton( () => ClarityService(appState: getIt.get()), ); + + // Remote-config-driven "occasion" theming (National Day, Eid, etc.). + getIt.registerLazySingleton(() => RemoteConfigServiceImp(loggerService: getIt())); + getIt.registerLazySingleton(() => OccasionThemeViewModel( + remoteConfigService: getIt(), + cacheService: getIt(), + loggerService: getIt(), + )); } } diff --git a/lib/features/occasion_theme/occasion_config_model.dart b/lib/features/occasion_theme/occasion_config_model.dart new file mode 100644 index 00000000..8a46d62b --- /dev/null +++ b/lib/features/occasion_theme/occasion_config_model.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +/// Model representing a remotely-configurable "occasion" theme +/// (e.g. Saudi National Day, Founding Day, Eid, Ramadan, ...). +/// +/// Everything here is fully data-driven — nothing about a specific +/// occasion (name, color, dates, banner asset) is hardcoded in the app. +/// All values are supplied via Firebase Remote Config (or the bundled +/// default JSON fallback used before the first successful fetch). +@immutable +class OccasionConfigModel { + /// Unique identifier of the occasion (e.g. "national_day"). Used only + /// for logging/analytics — no app logic branches on this value. + final String key; + + /// Master kill-switch. When false, the app must look exactly like the + /// 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. + final DateTime? startDate; + final DateTime? endDate; + + /// Hex string (e.g. "#00894A") for the occasion's primary brand color. + /// When null, the app's default primary color is preserved. + final String? primaryColorHex; + + /// Remote (or bundled asset path) image rendered as a decorative + /// background/watermark, drawn using [backgroundImageOpacity]. + final String? backgroundOpacityImage; + + /// Opacity (0.0 - 1.0) applied to [backgroundOpacityImage]. + final double backgroundImageOpacity; + + /// Remote (or bundled asset path) image rendered as the occasion's main + /// header/banner (e.g. the National Day greeting banner), drawn at full + /// opacity on top of [backgroundOpacityImage]. + final String? headerImage; + + const OccasionConfigModel({ + required this.key, + required this.enabled, + this.startDate, + this.endDate, + this.primaryColorHex, + this.backgroundOpacityImage, + this.backgroundImageOpacity = 1.0, + this.headerImage, + }); + + /// Fully inert config — identical to the app's current/default look. + factory OccasionConfigModel.disabled() => const OccasionConfigModel( + key: 'none', + enabled: false, + startDate: null, + endDate: null, + primaryColorHex: null, + backgroundOpacityImage: null, + backgroundImageOpacity: 1.0, + headerImage: null, + ); + + factory OccasionConfigModel.fromJson(Map json) { + return OccasionConfigModel( + key: (json['key'] as String?) ?? 'none', + enabled: (json['enabled'] as bool?) ?? false, + startDate: _parseDate(json['start_date']), + endDate: _parseDate(json['end_date']), + primaryColorHex: json['primary_color'] as String?, + backgroundOpacityImage: json['background_opacity_image'] as String?, + backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0, + headerImage: json['header_image'] as String?, + ); + } + + static DateTime? _parseDate(dynamic value) { + if (value == null) return null; + if (value is String && value.isNotEmpty) { + return DateTime.tryParse(value); + } + return null; + } + + /// Whether "now" falls inside the configured activation window. + /// If no window is configured, the [enabled] flag alone decides. + bool isWithinActiveWindow([DateTime? now]) { + final current = now ?? DateTime.now(); + if (startDate != null && current.isBefore(startDate!)) return false; + if (endDate != null && current.isAfter(endDate!)) return false; + return true; + } + + /// Parsed [Color] from [primaryColorHex], or null if not configured. + Color? get primaryColor => _hexToColor(primaryColorHex); + + static Color? _hexToColor(String? hex) { + if (hex == null || hex.isEmpty) return null; + var value = hex.trim().replaceFirst('#', ''); + if (value.length == 6) value = 'FF$value'; + final parsed = int.tryParse(value, radix: 16); + return parsed != null ? Color(parsed) : null; + } + + Map toJson() => { + 'key': key, + 'enabled': enabled, + 'start_date': startDate?.toIso8601String(), + 'end_date': endDate?.toIso8601String(), + 'primary_color': primaryColorHex, + 'background_opacity_image': backgroundOpacityImage, + 'background_image_opacity': backgroundImageOpacity, + '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 new file mode 100644 index 00000000..b1016589 --- /dev/null +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_config_model.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'package:hmg_patient_app_new/services/remote_config_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Drives the fully remote-config-based "occasion" theming (National Day, +/// Founding Day, Eid, Ramadan, ...). Nothing about a specific occasion is +/// hardcoded here — every color/asset/date comes from [OccasionConfigModel], +/// which is parsed from Firebase Remote Config. +/// +/// Mirrors the existing dark-mode pattern in ProfileSettingsViewModel: +/// a static flag on [AppColors] is kept in sync so every widget using +/// AppColors.primaryRedColor (even ones with no BuildContext/Provider +/// access) reflects the active occasion instantly. +class OccasionThemeViewModel extends ChangeNotifier { + static const String _cachedConfigKey = 'cached_occasion_config_json'; + + final RemoteConfigService _remoteConfigService; + final CacheService _cacheService; + final LoggerService _loggerService; + + OccasionThemeViewModel({ + required RemoteConfigService remoteConfigService, + required CacheService cacheService, + required LoggerService loggerService, + }) : _remoteConfigService = remoteConfigService, + _cacheService = cacheService, + _loggerService = loggerService; + + OccasionConfigModel _config = OccasionConfigModel.disabled(); + OccasionConfigModel get config => _config; + + /// True only when enabled AND (if a window is configured) within it. + bool get isOccasionActive => _config.enabled && _config.isWithinActiveWindow(); + + String? get backgroundOpacityImage => isOccasionActive ? _config.backgroundOpacityImage : null; + + double get backgroundImageOpacity => _config.backgroundImageOpacity; + + /// The occasion's main header/banner image (e.g. National Day greeting + /// banner), rendered at full opacity. Null unless an occasion is active. + String? get headerImage => isOccasionActive ? _config.headerImage : null; + + /// Call once at app startup (before the first frame) to restore the last + /// known-good config instantly (offline-safe), then kick off a remote + /// fetch in the background. + Future initialize() async { + _restoreCachedConfig(); + _applyToAppColors(); + + try { + await _remoteConfigService.initialize(); + await refresh(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.initialize error: $ex'); + } + } + + /// 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. + Future refresh() async { + try { + await _remoteConfigService.fetchAndActivate(); + final rawJson = _remoteConfigService.occasionConfigRawJson; + _config = OccasionConfigModel.fromJson(json.decode(rawJson) as Map); + await _cacheService.saveString(key: _cachedConfigKey, value: rawJson); + _applyToAppColors(); + notifyListeners(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.refresh error: $ex'); + // Keep whatever config was already applied (cached/disabled) — never + // let a remote-config failure change the app's appearance unexpectedly. + } + } + + void _restoreCachedConfig() { + try { + final cachedJson = _cacheService.getString(key: _cachedConfigKey); + if (cachedJson == null || cachedJson.isEmpty) { + _config = OccasionConfigModel.disabled(); + return; + } + _config = OccasionConfigModel.fromJson(json.decode(cachedJson) as Map); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel._restoreCachedConfig error: $ex'); + _config = OccasionConfigModel.disabled(); + } + } + + /// Keeps AppColors in sync so non-Provider call sites (AppColors.primaryRedColor) + /// pick up the occasion color exactly like the existing dark-mode flag does. + void _applyToAppColors() { + AppColors.isOccasionThemeActive = isOccasionActive; + AppColors.occasionPrimaryColor = _config.primaryColor; + } +} + diff --git a/lib/main.dart b/lib/main.dart index 4e60c2a0..e5b538e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -36,6 +37,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_reg import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; @@ -166,6 +168,14 @@ Future callInitializations() async { // Restore persisted accessibility settings before the first frame. getIt.get().loadAccessibilitySettings(); + + // Restore last-known occasion theme instantly (offline-safe, synchronous) + // so the very first frame is already correct, then let the Remote Config + // fetch happen in the background without blocking app startup. Fully + // 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(); + unawaited(occasionThemeViewModel.initialize()); } void main() async { @@ -305,7 +315,10 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ], child: MyApp()), ), // Wrap your app ); @@ -373,11 +386,11 @@ class MyApp extends StatelessWidget { return Sizer( builder: (context, orientation, deviceType) { // SizeConfig().init(constraints, orientation); - return Consumer( - builder: (context, profileVm, _) { + return Consumer2( + builder: (context, profileVm, occasionThemeVm, _) { final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; return MaterialApp( - key: ValueKey('app_theme_${profileVm.isDarkMode}'), + key: ValueKey('app_theme_${profileVm.isDarkMode}_${occasionThemeVm.isOccasionActive}_${occasionThemeVm.config.key}'), // title: 'Dr. AlHabib', title: 'Dr. AlHabib Beta', builder: (context, mchild) { diff --git a/lib/presentation/appointments/appointment_queue_page.dart b/lib/presentation/appointments/appointment_queue_page.dart index f2bd1d18..031295e5 100644 --- a/lib/presentation/appointments/appointment_queue_page.dart +++ b/lib/presentation/appointments/appointment_queue_page.dart @@ -150,7 +150,7 @@ class AppointmentQueuePage extends StatelessWidget { textColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, backgroundColor: myAppointmentsVM.patientQueueDetailsList[index].callType == 1 - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: 8.h, end: -2.h), ), diff --git a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart index e7d7c187..bd9fdef9 100644 --- a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart +++ b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart @@ -68,7 +68,7 @@ class AskDoctorAppointmentCard extends StatelessWidget { ), AppCustomChipWidget( labelText: LocaleKeys.outPatient.tr(context: context), - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), textColor: AppColors.primaryRedColor, ), AppCustomChipWidget( diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart index f3f0782d..62c409c6 100644 --- a/lib/presentation/blood_donation/blood_donation_page.dart +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -244,7 +244,7 @@ class _BloodDonationPageState extends State { Container( padding: EdgeInsets.all(8.h), decoration: BoxDecoration( - color: AppColors.primaryRedColor.withValues(alpha: 0.1), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8.r), ), child: Utils.buildSvgWithAssets( diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 91fa3b52..a5d68614 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -59,7 +59,7 @@ class FeedbackPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.send.tr(context: context)), CustomTabBarModel(null, LocaleKeys.status.tr(context: context)), //TODO: Need to be Disabled This Tab For Now diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart index 379540a3..38f0f70b 100644 --- a/lib/presentation/contact_us/find_us_page.dart +++ b/lib/presentation/contact_us/find_us_page.dart @@ -44,7 +44,7 @@ class FindUsPage extends StatelessWidget { ? SizedBox.shrink() : CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.hmgHospitals.tr()), CustomTabBarModel(null, LocaleKeys.hmgPharmacy.tr()), diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 582fd6fc..42669a71 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -667,7 +667,7 @@ class _ServicesPageState extends State { borderWidth: 0.w, isBold: true, borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.08), padding: EdgeInsets.all(8.w), fontSize: 14.f, onPressed: () { diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index fdb8afcf..b9c9c245 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; @@ -49,6 +50,7 @@ import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dar import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/lakum_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; +import 'package:hmg_patient_app_new/presentation/home/widgets/occasion_banner_widget.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; @@ -232,6 +234,11 @@ class _LandingPageState extends State { RefreshIndicator( color: AppColors.primaryRedColor, onRefresh: () async { + // Re-check Remote Config for occasion theme changes on + // pull-to-refresh (independent of auth state) — handy for + // testing without needing to restart the app. + unawaited(GetIt.instance().refresh()); + if (appState.isAuthenticated) { // Refresh Appointments Data myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); @@ -282,6 +289,9 @@ class _LandingPageState extends State { child: Column( spacing: 16.h, children: [ + // Renders nothing (zero height) unless a remote-config + // occasion theme (e.g. National Day) is currently active. + const OccasionBannerWidget(), Row( spacing: 8.h, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -715,7 +725,7 @@ class _LandingPageState extends State { // // children: [ // // AppCustomChipWidget( // // labelText: LocaleKeys.erOnlineCheckInRequest.tr(context: context), - // // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), + // // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.10), // // textColor: AppColors.primaryRedColor, // // ), // // Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), @@ -1235,7 +1245,7 @@ class _LandingPageState extends State { iconColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, textColor: isVitalCall ? AppColors.primaryRedColor : AppColors.successColor, iconSize: 14.w, - backgroundColor: isVitalCall ? AppColors.primaryRedColor.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), + backgroundColor: isVitalCall ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : AppColors.successColor.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only( start: 8.h, end: -2.h, diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart new file mode 100644 index 00000000..38958426 --- /dev/null +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -0,0 +1,61 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Renders the remotely-configured occasion imagery: +/// - `background_opacity_image`: a decorative backdrop/watermark drawn at +/// [OccasionThemeViewModel.backgroundImageOpacity]. +/// - `header_image`: the occasion's main header/banner (e.g. the National +/// Day greeting banner), drawn on top at full opacity. +/// +/// Fully data-driven: when no occasion is active (the default), this widget +/// renders nothing and takes zero space — the existing layout/UI is +/// completely unaffected. +class OccasionBannerWidget extends StatelessWidget { + const OccasionBannerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, occasionThemeVm, _) { + final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; + final headerImageUrl = occasionThemeVm.headerImage; + final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); + final hasHeaderImage = occasionThemeVm.isOccasionActive && (headerImageUrl?.isNotEmpty ?? false); + + if (!hasBackgroundImage && !hasHeaderImage) { + return const SizedBox.shrink(); + } + + return Stack( + alignment: Alignment.center, + children: [ + if (hasBackgroundImage) + Opacity( + opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), + child: CachedNetworkImage( + imageUrl: backgroundImageUrl!, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ), + if (hasHeaderImage) + CachedNetworkImage( + imageUrl: headerImageUrl!, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, ___) => const SizedBox.shrink(), + ), + ], + ); + }, + ); + } +} + + + diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 1a5fe039..5b23bab2 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -58,7 +58,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { ? AppColors.successColor.withValues(alpha: 0.1) : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) ? AppColors.warningColorYellow.withValues(alpha: 0.1) - : AppColors.primaryRedColor.withValues(alpha: 0.1), + : AppColors.primaryRedColorBase.withValues(alpha: 0.1), textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : (insuranceApprovalResponseModel.approvalStatusDescption?.toLowerCase().contains('partial') == true || insuranceApprovalResponseModel.status == 8) diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index 4bc98627..23b14db9 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -37,7 +37,7 @@ class InsuranceApprovalCard extends StatelessWidget { } else if (_isPartiallyApproved()) { return AppColors.warningColorYellow.withValues(alpha: 0.1); } else { - return AppColors.primaryRedColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } } @@ -126,7 +126,7 @@ class InsuranceApprovalCard extends StatelessWidget { runSpacing: 4.h, children: [ // AppCustomChipWidget(labelText: isLoading ? "Approved" : insuranceApprovalResponseModel.approvalStatusDescption!, - // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColor.withValues(alpha: 0.1), + // backgroundColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.primaryRedColorBase.withValues(alpha: 0.1), // textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, // ).toShimmer2(isShow: isLoading), AppCustomChipWidget(labelText: isLoading ? "Cardiology" : insuranceApprovalResponseModel.clinicName ?? "").toShimmer2(isShow: isLoading), diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 0f5bde1e..2393e875 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -69,8 +69,8 @@ class InsuranceHistory extends StatelessWidget { CustomButton( text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription ?? "", onPressed: () {}, - backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - borderColor: AppColors.primaryRedColor.withOpacity(0.0), + backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), + borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), textColor: AppColors.primaryRedColor, fontSize: 10, isBold: true, diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 6fab990c..4b352474 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -117,7 +117,7 @@ class PatientInsuranceCard extends StatelessWidget { } }, backgroundColor: isCurrentPatientInsuranceExpired(insuranceCardDetailsModel.cardValidTo!) - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : insuranceViewModel.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), @@ -129,7 +129,7 @@ class PatientInsuranceCard extends StatelessWidget { // iconColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // textColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // iconSize: 12, - // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), // labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.h), // ), ], diff --git a/lib/presentation/medical_file/eye_measurement_details_page.dart b/lib/presentation/medical_file/eye_measurement_details_page.dart index 68f261cd..90dbba10 100644 --- a/lib/presentation/medical_file/eye_measurement_details_page.dart +++ b/lib/presentation/medical_file/eye_measurement_details_page.dart @@ -47,7 +47,7 @@ class EyeMeasurementDetailsPage extends StatelessWidget { SizedBox(height: 16.h), CustomTabBar( activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), + activeBackgroundColor: AppColors.primaryRedColorBase.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.classes.tr()), CustomTabBarModel(null, LocaleKeys.contactLens.tr()), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 27978723..99a6db10 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -340,7 +340,7 @@ class _MedicalFilePageState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 8feb8434..16130d0c 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -198,7 +198,7 @@ class _FamilyCardsState extends State { backgroundColor: profile.status == FamilyFileEnum.pending.toInt ? AppColors.alertLightColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.active.toInt ? AppColors.lightGreenColor : AppColors.lightGrayBGColor, @@ -410,7 +410,7 @@ class _FamilyCardsState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.warningColorYellow.withValues(alpha: 0.1), @@ -575,7 +575,7 @@ class _FamilyCardsState extends State { backgroundColor: profile.status == FamilyFileEnum.pending.toInt ? AppColors.alertLightColor.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.20) : profile.status == FamilyFileEnum.active.toInt ? AppColors.lightGreenColor : AppColors.lightGrayBGColor, diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 8fea72fc..5f1b3d3a 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -201,8 +201,8 @@ class InvoiceListCard extends StatelessWidget { onTap!(); } }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, @@ -223,7 +223,7 @@ class InvoiceListCard extends StatelessWidget { // icon: AppAssets.refundIcon, // iconColor: AppColors.caloriesCalculatorColor, // backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), - // borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + // borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), // textColor: AppColors.caloriesCalculatorColor, // fontSize: 14.f, // fontWeight: FontWeight.w600, diff --git a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart index 2aaefcac..4c8be01e 100644 --- a/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart +++ b/lib/presentation/my_invoices/widgets/pharmacy_invoice_card.dart @@ -57,7 +57,7 @@ class PharmacyInvoiceCard extends StatelessWidget { // if (pharmacyInvoice.totalAmount != null) // AppCustomChipWidget( // labelText: '${pharmacyInvoice.totalAmount!.toStringAsFixed(2)} ${LocaleKeys.sar.tr(context: context)}', - // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + // backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), // textColor: AppColors.primaryRedColor, // isEnglishOnly: true, // ), @@ -158,8 +158,8 @@ class PharmacyInvoiceCard extends StatelessWidget { ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context), onPressed: onTap, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index b0e11af0..59b10620 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -177,8 +177,8 @@ class PrescriptionItemView extends StatelessWidget { // child: CustomButton( // text: LocaleKeys.checkAvailability.tr(context: context), // onPressed: () {}, - // backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - // borderColor: AppColors.primaryRedColor.withOpacity(0.0), + // backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), + // borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), // textColor: AppColors.primaryRedColor, // fontSize: 14.f, // isBold: true, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 2e9bb3b7..af6623c8 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -701,7 +701,7 @@ class _FamilyCardWidgetState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withValues(alpha: 0.1) + ? AppColors.primaryRedColorBase.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive ? AppColors.successColor.withValues(alpha: 0.1) : AppColors.warningColorYellow.withValues(alpha: 0.1), diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index fbd1d58b..92729d8a 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -123,7 +123,7 @@ class FamilyCardWidget extends StatelessWidget { icon = AppAssets.cancel_circle_icon; labelText = LocaleKeys.insuranceExpired.tr(context: context); iconColor = AppColors.primaryRedColor; - backgroundColor = AppColors.primaryRedColor.withValues(alpha: 0.15); + backgroundColor = AppColors.primaryRedColorBase.withValues(alpha: 0.15); } else { icon = AppAssets.insurance_active_icon; labelText = LocaleKeys.insuranceActive.tr(context: context); diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 630c5dc0..d68b7494 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -173,8 +173,8 @@ class _RefundHistoryScreenState extends State { CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))), - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColorBase.withValues(alpha: 0.01), textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 0e150733..c9f07c96 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -289,7 +289,7 @@ class _RiskFactorsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index 5e102581..e49f0521 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -268,7 +268,7 @@ class _SuggestionsScreenState extends State { child: CustomButton( text: LocaleKeys.previous.tr(), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index 6a83316b..2e57664a 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -360,7 +360,7 @@ class _SymptomsSelectorPageState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 95de9464..24a8ddcd 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -750,7 +750,7 @@ class _TriagePageState extends State { percentage: probability.toInt(), paddingBetween: 5.h, color: AppColors.primaryRedColor, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.17), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.17), height: 8.h, titleWidget: RichText( text: TextSpan( @@ -785,7 +785,7 @@ class _TriagePageState extends State { text: LocaleKeys.previous.tr(context: context), onPressed: _onPreviousPressed, isDisabled: viewModel.isTriageDiagnosisLoading, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart index 6672a968..3f9c5747 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -148,7 +148,7 @@ class _UserInfoFlowManagerState extends State { boxShadow: isCurrentStep && value > 0 ? [ BoxShadow( - color: AppColors.primaryRedColor.withValues(alpha: 0.4), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.4), blurRadius: 8.r, spreadRadius: 1.r, ), @@ -191,8 +191,8 @@ class _UserInfoFlowManagerState extends State { isDisabled: !(viewModel.isCurrentUserInfoPageValid), onPressed: () => viewModel.isCurrentUserInfoPageValid ? _onNext() : () {}, backgroundColor: - viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), - borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColor.withValues(alpha: 0.5), + viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), + borderColor: viewModel.isCurrentUserInfoPageValid ? AppColors.primaryRedColor : AppColors.primaryRedColorBase.withValues(alpha: 0.5), textColor: AppColors.whiteColor, fontSize: 16.f, ) @@ -204,7 +204,7 @@ class _UserInfoFlowManagerState extends State { child: CustomButton( text: LocaleKeys.previous.tr(context: context), onPressed: _onPrevious, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/user_info_selection_screen.dart b/lib/presentation/symptoms_checker/user_info_selection_screen.dart index 589771ec..fef576dc 100644 --- a/lib/presentation/symptoms_checker/user_info_selection_screen.dart +++ b/lib/presentation/symptoms_checker/user_info_selection_screen.dart @@ -296,7 +296,7 @@ class _UserInfoSelectionScreenState extends State { context.read().setUserInfoPage(0, isSinglePageEdit: false); context.navigateWithName(AppRoutes.userInfoFlowManager); }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + backgroundColor: AppColors.primaryRedColorBase.withValues(alpha: 0.11), borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 16.f, diff --git a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart index 307445a4..073aa110 100644 --- a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart +++ b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart @@ -188,7 +188,7 @@ class _PinchZoomTutorialOverlayState extends State wit width: 48.w, height: 48.h, decoration: BoxDecoration( - color: AppColors.primaryRedColor.withValues(alpha: 0.1), + color: AppColors.primaryRedColorBase.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), ), child: Icon( diff --git a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart index 19ad3935..b1e8beea 100644 --- a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart +++ b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart @@ -94,7 +94,7 @@ class _TriageLevelCardState extends State with SingleTickerProv switch (triageLevel) { case 'emergency_ambulance': case 'emergency': - return AppColors.chipColorEmergency.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); case 'consultation_24': case 'consultation': return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1); diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart index 058f14e8..2d61378b 100644 --- a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -91,7 +91,7 @@ class SwitchCupBottomSheet extends StatelessWidget { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent, + color: isSelected ? AppColors.primaryRedColorBase.withOpacity(0.08) : Colors.transparent, borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, @@ -271,9 +271,9 @@ class _CustomizeCupBottomSheetState extends State { SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.primaryRedColor, - inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2), + inactiveTrackColor: AppColors.primaryRedColorBase.withOpacity(0.2), thumbColor: AppColors.primaryRedColor, - overlayColor: AppColors.primaryRedColor.withOpacity(0.2), + overlayColor: AppColors.primaryRedColorBase.withOpacity(0.2), trackHeight: 4.h, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), ), diff --git a/lib/presentation/weather/weather_details_page.dart b/lib/presentation/weather/weather_details_page.dart index 8762a55a..7e93b9b6 100644 --- a/lib/presentation/weather/weather_details_page.dart +++ b/lib/presentation/weather/weather_details_page.dart @@ -62,7 +62,7 @@ class _WeatherDetailsPageState extends State { case ColorName.ORANGE: return AppColors.warningLightColor.withValues(alpha: 0.1); case ColorName.RED: - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } } @@ -120,7 +120,7 @@ class _WeatherDetailsPageState extends State { } // Red: Very High, At High Risk else if (category.contains('very high') || category.contains('at high risk')) { - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColorBase.withValues(alpha: 0.1); } // Orange: Risk, High (but not "very high" or "at high risk") else if (category.contains('risk') || category.contains('high')) { diff --git a/lib/services/remote_config_service.dart b/lib/services/remote_config_service.dart new file mode 100644 index 00000000..e8a7ec16 --- /dev/null +++ b/lib/services/remote_config_service.dart @@ -0,0 +1,84 @@ +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 string currently held for the `occasion_config` key + /// (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 builds keep a sane 1-hour minimum interval. + 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; + } + } + + static const String _fallbackDisabledJson = '{"key":"none","enabled":false}'; +} + diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 1ecbcb61..2be811d8 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -5,6 +5,15 @@ class AppColors { /// When true, every getter below returns the dark palette value. static bool isDarkMode = false; + /// Global occasion-theme flag — toggled by OccasionThemeViewModel based on + /// Firebase Remote Config. When false (the default), the app looks exactly + /// like it does today — no colors are affected. + static bool isOccasionThemeActive = false; + + /// Remotely-configured primary color for the currently active occasion + /// (e.g. National Day). Null unless an occasion theme is active. + static Color? occasionPrimaryColor; + static const transparent = Colors.transparent; // ── Scaffold / Background ───────────────────────────────────────────────── @@ -20,7 +29,20 @@ class AppColors { // ── Core Colors ─────────────────────────────────────────────────────────── static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFffffff); // static const Color primaryRedColor = Color(0xFFED1C2B); - static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : Color(0xFFED1C2B); + /// Occasion-aware primary brand color. Resolves to the remotely-configured + /// occasion color when active (e.g. National Day green), otherwise falls + /// back to the original dark/light red — identical to current behavior. + /// + /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived + /// (translucent) colors — use [primaryRedColorBase] for those instead, so + /// occasion theming never affects translucent overlays/backgrounds. + static Color get primaryRedColor => + (isOccasionThemeActive && occasionPrimaryColor != null) ? occasionPrimaryColor! : primaryRedColorBase; + + /// The original, non-themed brand red (or its dark-mode variant). Always + /// stable regardless of any active occasion theme — intended for opacity + /// derived usages (e.g. `AppColors.primaryRedColorBase.withOpacity(0.1)`). + static Color get primaryRedColorBase => isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B); static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); static const Color successColor = Color(0xFF18C273); static Color get successLightBgColor => isDarkMode ? dark.successLightBgColor : const Color(0xffDDF6EA); diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index e999842f..a5b8b239 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -28,7 +28,13 @@ class BottomNavigation extends StatelessWidget { icon: AppAssets.bookAppoBottom, fillIcon: AppAssets.bookAppoBottom, label: LocaleKeys.appointments.tr(context: context), - iconSize: 32, isSpecial: true), + iconSize: 32, + isSpecial: true, + // Explicit dynamic color — the SVG asset itself is a fixed red, so + // without this override the Appointments icon would never react to + // dark mode or the active occasion theme (e.g. National Day green). + color: AppColors.primaryRedColor, + ), appState.isAuthenticated // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, fillIcon: AppAssets.symptomCheckerBottomFillIcon, label: LocaleKeys.symptoms.tr(context: context)) @@ -75,7 +81,8 @@ class BottomNavigation extends StatelessWidget { icon: isSelected ? item.fillIcon : item.icon, height: item.iconSize.h, width: item.iconSize.w, - applyThemeColor: !item.isSpecial + applyThemeColor: !item.isSpecial, + iconColor: item.color, // iconColor: isSelected ? Colors.black87 : Colors.black87, ), // const SizedBox(height: 10), diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index a544f010..60178939 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -163,7 +163,7 @@ class _DateRangeSelectorState extends State { ), startRangeSelectionColor: AppColors.primaryRedColor, endRangeSelectionColor: AppColors.primaryRedColor, - rangeSelectionColor: AppColors.primaryRedColor.withOpacity(0.1), + rangeSelectionColor: AppColors.primaryRedColorBase.withOpacity(0.1), rangeTextStyle: TextStyle( fontFamily: "Poppins", ), @@ -963,7 +963,7 @@ class _DateRangeSelectorState extends State { children: [ AppCustomChipWidget( labelText: LocaleKeys.thisWeek.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -977,7 +977,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastMonth.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -991,7 +991,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastSixMonths.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -1012,7 +1012,7 @@ class _DateRangeSelectorState extends State { ], ), labelText: "${LocaleKeys.year.tr(context: context)} ${model.getCurrentYear}", - backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index 50da5f3a..2994db57 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -220,7 +220,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/lib/widgets/map/gms_map.dart b/lib/widgets/map/gms_map.dart index 33e0befb..1fe7f442 100644 --- a/lib/widgets/map/gms_map.dart +++ b/lib/widgets/map/gms_map.dart @@ -372,7 +372,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColorBase.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/pubspec.yaml b/pubspec.yaml index 82ae76ac..9e9977c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: google_api_availability: ^5.0.1 firebase_analytics: ^11.5.1 firebase_crashlytics: ^4.3.8 + firebase_remote_config: ^5.5.0 jiffy: ^6.4.3 # hijri_gregorian_calendar: # path: /Users/aamir/StudioProjects/hijri_gregorian_calendar