Merge remote-tracking branch 'origin/master' into ai-analysis

# Conflicts:
#	pubspec.lock
ai-analysis
taha 1 week ago
commit d2e0eaf0ef

@ -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
}
[]

@ -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";

@ -608,7 +608,6 @@ class PushNotificationHandler {
// Permission.audio,
// Permission.microphone,
].request();
log("=-=-=-=-=-=-=-=-=-=-");
log(statuses[Permission.notification].toString());
}
} catch (_) {

@ -47,7 +47,7 @@ class FamilyFileRequest {
"SharedPatientIdentificationID": sharedPatientIdentificationId,
"SearchType": searchType,
"SharedPatientMobileNumber": sharedPatientMobileNumber,
"zipCode": zipCode,
"ZipCode": zipCode,
"isRegister": isRegister,
"PatientStatus": patientStatus,
"isDentalAllowedBackend": isDentalAllowedBackend,

@ -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<OccasionConfigModel> listFromJson(List<dynamic> jsonList) {
return jsonList.map((e) => OccasionConfigModel.fromJson(e as Map<String, dynamic>)).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<OccasionConfigModel> 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,
};
}

@ -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<void> refresh() async {
try {
await _remoteConfigService.fetchAndActivate();
final rawJson = _remoteConfigService.occasionConfigRawJson;
_config = OccasionConfigModel.fromJson(json.decode(rawJson) as Map<String, dynamic>);
final configs = OccasionConfigModel.listFromJson(json.decode(rawJson) as List<dynamic>);
_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<String, dynamic>);
final configs = OccasionConfigModel.listFromJson(json.decode(cachedJson) as List<dynamic>);
_config = OccasionConfigModel.resolveActive(configs, _now);
} catch (ex) {
_loggerService.logError('OccasionThemeViewModel._restoreCachedConfig error: $ex');
_config = OccasionConfigModel.disabled();

@ -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<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@ -144,7 +143,11 @@ Future<void> callInitializations() async {
// Initialize Security Service early to catch threats before app logic runs
if (kReleaseMode) {
await getIt.get<SecurityService>().initialize();
try {
await getIt.get<SecurityService>().initialize();
} catch (e) {
getIt.get<LoggerService>().logError('SecurityService.initialize failed, continuing app startup: $e');
}
}
// Set up critical threat callback to navigate to unsafe device page
@ -177,6 +180,8 @@ Future<void> 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>();
OccasionThemeViewModel.debugNowOverride = DateTime.now();
// OccasionThemeViewModel.debugNowOverride = DateTime.parse('2027-03-09T00:00:00Z');
unawaited(occasionThemeViewModel.initialize());
}

@ -858,7 +858,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
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<AppointmentPaymentPage> {
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 {

@ -781,7 +781,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
print("onBrowserLoadStart");
print(url);
if (selectedPaymentMethod == "TAMARA") {
if (selectedPaymentMethod.toLowerCase() == "tamara") {
if (Platform.isAndroid) {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['status'] ?? "";
@ -821,7 +821,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
void checkPaymentStatus() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
if (selectedPaymentMethod == "TAMARA") {
if (selectedPaymentMethod.toLowerCase() == "tamara") {
await payfortViewModel.checkTamaraPaymentStatus(
transactionID: transID,
onSuccess: (apiResponse) async {

@ -234,6 +234,7 @@ class _LandingPageState extends State<LandingPage> {
// inline with the rest of the content.
final hasStickyBanner = context.watch<OccasionThemeViewModel>().headerImage?.isNotEmpty ?? false;
log("hasStickyBanner: $hasStickyBanner");
return Stack(
children: [
// Renders nothing unless a remote-config occasion theme (e.g.

@ -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);
}
}

@ -16,8 +16,9 @@ abstract class RemoteConfigService {
/// minimum fetch interval) and activates them.
Future<bool> 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 = '[]';
}

@ -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);

@ -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"

Loading…
Cancel
Save