Compare commits

..

99 Commits

Author SHA1 Message Date
Haroon Amjad 439140dba2 Update to stores, VersionID 10.2 4 days ago
haroon amjad 67802bb021 Updates 4 days ago
Sultan khan 59efdf382b no message 4 days ago
khansultan1 d7b88e2e73 Merge pull request 'wala_plus_with_dialog_removal' (#22) from wala_plus_with_dialog_removal into master_merge
Reviewed-on: #22
4 days ago
Sultan khan f9332d29ae no message 4 days ago
Sultan khan 95888fdb64 no message 4 days ago
Sultan khan 548aa501a1 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Security changes
  Update dependencies and remove unused keys from localization
  changes for Latest AGP
  Update dependencies and improve code structure
  chat history maintained
  translation added.
  Chat Bot Completed
  Chat Bot Completed
  chat bot & Speech to Text
5 days ago
haroon amjad 38906e24b7 Merge branch 'master' into wala_plus_with_dialog_removal
# Conflicts:
#	assets/langs/ar-SA.json
#	assets/langs/en-US.json
#	ios/Flutter/AppFrameworkInfo.plist
#	ios/Runner.xcodeproj/project.pbxproj
#	ios/Runner/Info.plist
#	lib/classes/consts.dart
#	lib/config/routes.dart
#	lib/generated/codegen_loader.g.dart
#	lib/generated/locale_keys.g.dart
#	lib/models/generic_response_model.dart
#	lib/models/get_mobile_login_info_list_model.dart
#	lib/ui/landing/dashboard_screen.dart
#	lib/ui/login/verify_last_login_screen.dart
#	pubspec.yaml
5 days ago
Sultan khan c32b155718 ui updates 5 days ago
haroon amjad efee685178 Security changes 5 days ago
Sultan khan f483c858ef Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  made the city search functional
2 weeks ago
FaizHashmi 3dcb332a04 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 2 weeks ago
FaizHashmi 48e5d70ada made the city search functional 2 weeks ago
Sultan khan 3cf2c2a4c8 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  made the city search functional
2 weeks ago
FaizHashmi 06295821aa made the city search functional 2 weeks ago
Sultan khan ae1bd7f0c5 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Fav Tab Changes and SearchBar Fix
  icons
2 weeks ago
FaizHashmi 6ba06065b3 Fav Tab Changes and SearchBar Fix 2 weeks ago
FaizHashmi d75f9a9cc3 icons 2 weeks ago
Sultan khan 63f678b666 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal 2 weeks ago
FaizHashmi ef7078c152 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 2 weeks ago
FaizHashmi 252b9b471c Toast fixed 2 weeks ago
Sultan khan bd6e9577e3 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Toast fixed

# Conflicts:
#	lib/generated/codegen_loader.g.dart
#	lib/generated/locale_keys.g.dart
2 weeks ago
Sultan khan e6cc2491b4 no message 2 weeks ago
Sultan khan 5194f8b1fd no message 2 weeks ago
FaizHashmi 2fb4a99a42 Toast fixed 2 weeks ago
Sultan khan fe36263ac0 design changed 2 weeks ago
Sultan khan 99c2d658e1 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Toast fixed
2 weeks ago
Sultan khan 1d2ed863ae no message 2 weeks ago
FaizHashmi b931ed8138 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 2 weeks ago
FaizHashmi b25b398695 Toast fixed 2 weeks ago
Sultan khan c957d547a8 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Toast fixed
3 weeks ago
Sultan khan a517e4e48b last login token updated. 3 weeks ago
FaizHashmi a0c5ac7e6c Toast fixed 3 weeks ago
Sultan khan 7043f0f91c Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  FIXING
  fixed toast color
3 weeks ago
FaizHashmi 99646c90c5 FIXING 3 weeks ago
Sultan khan 1a4dd29a9b no message 3 weeks ago
FaizHashmi 9bcb6c6b8a fixed toast color 3 weeks ago
Sultan khan e66154d476 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Fav Fix
3 weeks ago
FaizHashmi 2a4cd36ba9 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 3 weeks ago
FaizHashmi a152fe1da2 Fav Fix 3 weeks ago
Sultan khan 6b364b4109 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Filters Fix
  Filters Fix
3 weeks ago
FaizHashmi 367218702b Filters Fix 3 weeks ago
FaizHashmi c976b9c9db Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 3 weeks ago
FaizHashmi ca9c7f7e5d Filters Fix 3 weeks ago
Sultan khan d23910351f bottomsheet added 3 weeks ago
Sultan khan 49d46dada2 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  chip color change
3 weeks ago
FaizHashmi fc04ff13bc chip color change 3 weeks ago
Sultan khan a0967051e8 Arabic text updated. 3 weeks ago
Sultan khan bdf3337af4 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  chip color change
3 weeks ago
FaizHashmi 9aa368dbf7 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 3 weeks ago
FaizHashmi d6fdcb214d chip color change 3 weeks ago
Sultan khan a8f9d275f5 no message 3 weeks ago
Sultan khan ecf1c7c948 no message 3 weeks ago
Sultan khan 5a96d64d9c Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  chip set

# Conflicts:
#	lib/ui/landing/dashboard_screen_for_dependet.dart
3 weeks ago
Sultan khan bd6e6c6f59 no message 3 weeks ago
Sultan khan 7c9599ff6e no message 3 weeks ago
FaizHashmi 62058d6dd9 chip set 3 weeks ago
Sultan khan 37303458bf Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Added filters

# Conflicts:
#	assets/langs/ar-SA.json
#	assets/langs/en-US.json
#	lib/generated/locale_keys.g.dart
#	lib/ui/landing/dashboard_screen_for_dependet.dart
3 weeks ago
Sultan khan cd2703cbc5 favorite offers done 3 weeks ago
FaizHashmi 6d4bddd42d Added filters 3 weeks ago
Sultan khan 3a73ec1c2e Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  added shimmer

# Conflicts:
#	lib/generated/locale_keys.g.dart
3 weeks ago
FaizHashmi 59bba20e20 added shimmer 3 weeks ago
Sultan khan 4b1589f7df Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Added sub categories

# Conflicts:
#	lib/generated/locale_keys.g.dart
3 weeks ago
Sultan khan 975f6622b4 no message 3 weeks ago
FaizHashmi 154b62bbad Added sub categories 3 weeks ago
Sultan khan 821cdf474d ui updates 3 weeks ago
Sultan khan f6865959e6 no message 3 weeks ago
Sultan khan e21738cf5f no message 3 weeks ago
Sultan khan 518c766756 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  fixed suggestions
3 weeks ago
FaizHashmi ec37e74bc0 fixed suggestions 3 weeks ago
Sultan khan 58908df424 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  minor changes
  refactoriåtion
3 weeks ago
Sultan khan 7065a4fb23 no message 3 weeks ago
FaizHashmi cc03ab6a80 minor changes 3 weeks ago
FaizHashmi 287afc6ff1 Merge remote-tracking branch 'origin/wala_plus_with_dialog_removal' into wala_plus_with_dialog_removal 3 weeks ago
FaizHashmi 6aec5b1a8c refactoriåtion 3 weeks ago
Sultan khan 4cd4766ab8 no message 3 weeks ago
Sultan khan 798584431e Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  refactoriåtion
3 weeks ago
FaizHashmi e134e8d4e5 refactoriåtion 3 weeks ago
Sultan khan 825845b4c3 Merge branch 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus_with_dialog_removal
* 'wala_plus_with_dialog_removal' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  changhes
  toast gravity change

# Conflicts:
#	lib/ui/landing/dashboard_screen_for_dependet.dart
3 weeks ago
Sultan khan 0a270bd323 login flow updated 3 weeks ago
FaizHashmi fa1cbf866b changhes 3 weeks ago
FaizHashmi ae816a1f8f toast gravity change 3 weeks ago
FaizHashmi 371a3ecb74 toast gravity change 3 weeks ago
FaizHashmi f19a1441b3 url change 3 weeks ago
FaizHashmi 857d1e74ac Merge branch 'wala_plus' into wala_plus_with_dialog_removal 3 weeks ago
FaizHashmi e05e97a536 Updated the dialogs to toast 3 weeks ago
haroon amjad 6344d78302 Merge branch 'master_merge' 1 month ago
Haroon Amjad 4baf5f9389 Update dependencies and remove unused keys from localization 1 month ago
Haroon6138 cd9f8e0b48 Merge pull request 'flutter_upgrade_with_timesheet_changes' (#21) from flutter_upgrade_with_timesheet_changes into master
Reviewed-on: https://34.17.182.140/Haroon6138/mohemm-flutter-app/pulls/21
1 month ago
Haroon6138 c49872f798 Merge branch 'master' into flutter_upgrade_with_timesheet_changes 1 month ago
Haroon Amjad f503dca074 Merge branch 'apilevel_raise' into master_merge
# Conflicts:
#	android/app/build.gradle
#	android/gradle/wrapper/gradle-wrapper.properties
#	android/settings.gradle
#	assets/langs/ar-SA.json
#	assets/langs/en-US.json
#	lib/api/dashboard_api_client.dart
#	lib/classes/consts.dart
#	lib/config/routes.dart
#	lib/generated/codegen_loader.g.dart
#	lib/generated/locale_keys.g.dart
#	lib/models/generic_response_model.dart
#	lib/ui/landing/dashboard_screen.dart
#	lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart
#	pubspec.yaml
1 month ago
haroon amjad 9dcbb0d855 changes for Latest AGP 1 month ago
taha a6be594b3b Update dependencies and improve code structure 1 month ago
taha 9284d0e100 chat history maintained 1 month ago
Sultan khan 0175c958ef translation added. 1 month ago
Sultan khan 3cfe5355ef Merge branch 'master' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into flutter_upgrade_with_timesheet_changes
* 'master' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  Update to stores VersionID 10.1
  updates
2 months ago
aamir-csol 4f4db082fa Chat Bot Completed 10 months ago
aamir-csol f253a5e99e Chat Bot Completed 10 months ago
aamir-csol 234dce65e5 chat bot & Speech to Text 10 months ago

@ -19,6 +19,11 @@ analyzer:
unnecessary_null_checks: error
unnecessary_brace_in_string_interps: error
unnecessary_final: error
exclude:
- build/**
- android/**
- ios/**
- web/**
linter:

@ -18,7 +18,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace 'hmg.cloudSolutions.mohem'
compileSdk 36
compileSdk 37
ndkVersion '28.2.13676358'
@ -34,7 +34,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "hmg.cloudSolutions.mohem"
minSdkVersion 30
targetSdk 35
targetSdk 37
targetSdk = flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
@ -60,12 +60,14 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.debug
minifyEnabled false
shrinkResources false
}
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles 'proguard-rules.pro'
}
}
}

@ -31,11 +31,19 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="MoheM"
android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round">
<provider

@ -4,5 +4,5 @@
<external-files-path name="external-files-path" path="."/>
<files-path name="files_path" path="."/>
<cache-path name="cache-path" path="."/>
<root-path name="root" path="."/>
<!-- <root-path name="root" path="."/>-->
</paths>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>

@ -66,11 +66,34 @@ allprojects {
jvmTarget = "21"
}
}
if (project.name == 'nfc_manager') {
project.tasks.matching { it.name.startsWith("compile") && it.name.endsWith("Kotlin") }.configureEach {
doFirst {
def translatorFile = project.file("src/main/kotlin/io/flutter/plugins/nfcmanager/Translator.kt")
if (translatorFile.exists()) {
def content = translatorFile.text
if (content.contains("toLowerCase(Locale.ROOT)")) {
translatorFile.text = content.replace("toLowerCase(Locale.ROOT)", "lowercase(Locale.ROOT)")
}
}
}
}
}
}
}
}
subprojects {
afterEvaluate { project ->
if (project.name == "huawei_push") {
project.android {
compileSdk 36
}
}
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"

@ -1,4 +1,5 @@
org.gradle.jvmargs=-Xmx4096m
#org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1024m
org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Xmx4096m -Dfile.encoding=UTF-8
kotlin.daemon.jvmargs=-Xmx4096m
android.useAndroidX=true
android.enableJetifier=true

@ -1,6 +1,6 @@
#Tue Jul 08 11:08:34 AST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -30,8 +30,10 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.11.0' apply false
id("org.jetbrains.kotlin.android") version "2.2.0" apply false
// id "com.android.application" version '8.11.0' apply false
// id("org.jetbrains.kotlin.android") version "2.2.0" apply false
id "com.android.application" version '9.0.1' apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.3" apply false
id("com.google.firebase.crashlytics") version "3.0.4" apply false
id('org.gradle.toolchains.foojay-resolver-convention') version '0.9.0' apply false

@ -0,0 +1,46 @@
<svg width="74" height="74" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_119_438)">
<rect x="17" y="17" width="40" height="40" rx="20" fill="url(#paint0_linear_119_438)"/>
</g>
<g filter="url(#filter1_d_119_438)">
<rect x="13" y="13" width="48" height="48" rx="24" fill="url(#paint1_linear_119_438)" fill-opacity="0.4" shape-rendering="crispEdges"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.9992 37.5291C40.7637 38.0552 39.4301 38.3796 38.0352 38.4637C37.7946 38.4782 37.5523 38.4856 37.3082 38.4856C37.0642 38.4856 36.8219 38.4782 36.5813 38.4637C35.1861 38.3796 33.8522 38.055 32.6165 37.5287C30.0232 36.4243 27.8623 34.4316 26.4749 31.9094C28.6373 27.9782 32.6791 25.3333 37.3082 25.3333C41.9374 25.3333 45.9792 27.9782 48.1416 31.9094C46.7541 34.4319 44.5928 36.4247 41.9992 37.5291Z" fill="white"/>
<path d="M32.6163 38.5525C32.3175 38.4253 32.0245 38.2863 31.7378 38.136C30.8058 38.8264 30.018 39.6862 29.4248 40.6673C30.4331 42.3347 32.0037 43.6522 33.8885 44.3824C34.7866 44.7303 35.7561 44.9449 36.7701 45.0005H37.8268C38.8406 44.9449 40.6224 46.0011 38.035 48.6666C39.9201 47.9364 44.1637 42.3349 45.1721 40.6673C44.5806 39.689 43.7956 38.8313 42.8671 38.1419C42.5837 38.29 42.2941 38.4272 41.999 38.5529C40.7635 39.079 39.4299 39.4034 38.035 39.4875C37.7945 39.502 37.5521 39.5093 37.3081 39.5093C37.0641 39.5093 36.8217 39.502 36.5811 39.4875C35.1859 39.4034 33.852 39.0788 32.6163 38.5525Z" fill="white"/>
<rect x="32.0844" y="30.2271" width="10.4705" height="3.93258" rx="1.96629" fill="#162550"/>
<ellipse cx="40.2071" cy="32.1717" rx="0.685571" ry="0.720974" fill="#04FED1"/>
<ellipse cx="37.2984" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
<ellipse cx="34.5566" cy="32.1717" rx="0.685571" ry="0.720974" fill="#04FED1"/>
<ellipse cx="34.5566" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
<ellipse cx="40.0411" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
<defs>
<filter id="filter0_d_119_438" x="4" y="4" width="66" height="66" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="6.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0784314 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_119_438"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_119_438" result="shape"/>
</filter>
<filter id="filter1_d_119_438" x="0" y="0" width="74" height="74" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="6.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0784314 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_119_438"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_119_438" result="shape"/>
</filter>
<linearGradient id="paint0_linear_119_438" x1="-12.3178" y1="86.2302" x2="-12.3178" y2="18.3971" gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#259CB8"/>
<stop stop-color="#32D892"/>
</linearGradient>
<linearGradient id="paint1_linear_119_438" x1="-22.1814" y1="96.0762" x2="-22.1814" y2="14.6765" gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#259CB8"/>
<stop stop-color="#32D892"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.8331 11.6668C5.83305 11.7752 5.86321 11.8815 5.9202 11.9737C5.97718 12.066 6.05874 12.1405 6.15572 12.1889L7.3225 12.7723C7.41146 12.8167 7.51031 12.8377 7.60967 12.8332C7.70902 12.8287 7.80557 12.7989 7.89015 12.7466C7.97473 12.6943 8.04454 12.6212 8.09293 12.5343C8.14133 12.4474 8.16671 12.3496 8.16666 12.2502V8.16678C8.16679 7.87766 8.27427 7.5989 8.46827 7.38452L12.6821 2.72418C12.7576 2.64051 12.8073 2.53675 12.8251 2.42544C12.8429 2.31414 12.828 2.20007 12.7824 2.09702C12.7367 1.99396 12.6622 1.90635 12.5678 1.84477C12.4733 1.78318 12.3631 1.75027 12.2504 1.75H1.74938C1.63655 1.75004 1.52616 1.78279 1.43158 1.84429C1.33699 1.90579 1.26226 1.99339 1.21645 2.09648C1.17064 2.19958 1.15571 2.31375 1.17347 2.42516C1.19123 2.53656 1.24091 2.64043 1.3165 2.72418L5.53149 7.38452C5.72549 7.5989 5.83297 7.87766 5.8331 8.16678V11.6668Z" stroke-linecap="round" stroke="#202B33"/>
</svg>

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 559 B

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 6C11 7.65685 9.65685 9 8 9C6.34315 9 5 7.65685 5 6C5 4.34315 6.34315 3 8 3C9.65685 3 11 4.34315 11 6Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="#26343D"/>
<path d="M2 20V16C1.9946 14.8394 2.32594 13.7021 2.95386 12.726C3.58179 11.7499 4.47934 10.9768 5.53775 10.5006C6.59616 10.0243 7.76999 9.86518 8.917 10.0426C10.064 10.22 11.1349 10.7262 12 11.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="#26343D"/>
<path d="M19.5 11C19.5 12.3807 18.3807 13.5 17 13.5C15.6193 13.5 14.5 12.3807 14.5 11C14.5 9.61929 15.6193 8.5 17 8.5C18.3807 8.5 19.5 9.61929 19.5 11Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="#26343D"/>
<path d="M12 21V18.5C12 17.1739 12.5268 15.9021 13.4645 14.9645C14.4021 14.0268 15.6739 13.5 17 13.5C18.3261 13.5 19.5979 14.0268 20.5355 14.9645C21.4732 15.9021 22 17.1739 22 18.5V21H12Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="#26343D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow-right">
<path id="Vector" d="M3.1245 7.5H11.8755M7.5 11.8755L11.8755 7.5L7.5 3.1245" stroke-width="1.7" stroke-linecap="round" stroke="#147D6B"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow-up-right">
<path id="Vector" d="M14.166 14.166V5.834H5.834M14.166 5.834L5.834 14.166" stroke-width="1.7" stroke-linecap="round" stroke="#143F36"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 338 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="chevron-down">
<path id="Vector" d="M3 4.5L6 7.5L9 4.5" stroke-width="1.7" stroke-linecap="round" stroke="#147D6B"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 302 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="chevron-left">
<path id="Vector" d="M12.5 15L7.5 10L12.5 5" stroke-width="1.7" stroke-linecap="round" stroke="#202F32"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 306 B

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="compass" clip-path="url(#clip0_0_24)">
<path id="Vector" d="M20.1674 11C20.1674 16.063 16.063 20.1674 11 20.1674C5.93698 20.1674 1.8326 16.063 1.8326 11C1.8326 5.93698 5.93698 1.8326 11 1.8326C16.063 1.8326 20.1674 5.93698 20.1674 11ZM14.8868 7.11281L13.233 12.0733C13.143 12.3433 12.9913 12.5887 12.79 12.79C12.5887 12.9913 12.3433 13.143 12.0733 13.233L7.11281 14.8868L8.76661 9.92628C8.85661 9.65623 9.00827 9.41084 9.20955 9.20955C9.41084 9.00827 9.65623 8.85661 9.92628 8.76661L14.8868 7.11281Z" stroke-width="1.7" stroke-linecap="round" stroke="#147D6B"/>
</g>
<defs>
<clipPath id="clip0_0_24">
<rect width="22" height="22" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 845 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="heart">
<path id="Vector" d="M1.2495 5.93763C1.24951 5.24212 1.46051 4.56298 1.85463 3.9899C2.24875 3.41682 2.80744 2.97676 3.45693 2.72785C4.10641 2.47893 4.81612 2.43287 5.49233 2.59575C6.16855 2.75863 6.77944 3.12278 7.24434 3.64011C7.27708 3.67512 7.31667 3.70303 7.36064 3.72211C7.40462 3.74119 7.45204 3.75104 7.49998 3.75104C7.54792 3.75104 7.59534 3.74119 7.63932 3.72211C7.68329 3.70303 7.72288 3.67512 7.75563 3.64011C8.21906 3.11941 8.8301 2.7522 9.50741 2.58735C10.1847 2.42249 10.8962 2.46781 11.5471 2.71728C12.198 2.96674 12.7575 3.40851 13.1511 3.9838C13.5447 4.55909 13.7538 5.2406 13.7505 5.93763C13.7505 7.36889 12.8129 8.43765 11.8753 9.37516L8.44255 12.6958C8.32609 12.8296 8.18249 12.937 8.0213 13.011C7.86011 13.085 7.68502 13.1239 7.50766 13.125C7.3303 13.1261 7.15473 13.0895 6.99262 13.0175C6.83051 12.9456 6.68556 12.84 6.56741 12.7077L3.12464 9.37516C2.18707 8.43765 1.2495 7.37514 1.2495 5.93763Z" stroke-width="1.7" stroke-linecap="round" stroke="#202F32"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="heart">
<path id="Vector" d="M1.8326 8.70852C1.83262 7.68845 2.14208 6.69237 2.72012 5.85185C3.29816 5.01134 4.11758 4.36592 5.07016 4.00084C6.02273 3.63577 7.06365 3.56821 8.05542 3.8071C9.0472 4.04598 9.94318 4.58007 10.625 5.33882C10.6731 5.39017 10.7311 5.43111 10.7956 5.45909C10.8601 5.48708 10.9297 5.50152 11 5.50152C11.0703 5.50152 11.1398 5.48708 11.2043 5.45909C11.2688 5.43111 11.3269 5.39017 11.3749 5.33882C12.0546 4.57514 12.9508 4.03656 13.9442 3.79478C14.9376 3.55299 15.9811 3.61946 16.9357 3.98534C17.8904 4.35122 18.711 4.99915 19.2883 5.8429C19.8656 6.68666 20.1722 7.68621 20.1673 8.70852C20.1673 10.8077 18.7922 12.3752 17.4171 13.7502L12.3824 18.6205C12.2116 18.8167 12.001 18.9743 11.7646 19.0828C11.5282 19.1913 11.2714 19.2483 11.0112 19.25C10.7511 19.2516 10.4936 19.1979 10.2558 19.0924C10.0181 18.9869 9.80549 18.8319 9.6322 18.638L4.58281 13.7502C3.20771 12.3752 1.8326 10.8169 1.8326 8.70852Z" stroke-width="1.7" stroke-linecap="round" stroke="#78817F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 114.3 52">
<defs>
<style>
.cls-1 {
fill: #5e5f5f;
}
.cls-2 {
fill: #fff;
}
.cls-3 {
fill: #ec1f2d;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<path fill="#5e5f5f" d="M15.74,25.46H3.99c-2.53,0-3.99-1.42-3.99-3.78v-11.01h3.31v9.33c0,1.56.4,2.39,2.84,2.39h9.59v3.07h0Z"/>
<path fill="#5e5f5f" d="M17.2,29.26h-2.93v-2.66h2.93v2.66ZM21.88,21.03c0,1.21-.35,2.22-1.04,3.03-.78.94-1.87,1.4-3.24,1.4h-2.81v-3.07h2.1c1.11,0,1.68-.76,1.68-2.25v-5.07h3.31v5.96ZM21.67,29.26h-2.95v-2.66h2.95v2.66Z"/>
<path fill="#5e5f5f" d="M27.6,25.46h-3.31v-14.79h3.31v14.79Z"/>
<path fill="#5e5f5f" d="M39.98,25.46h-3.66c-.54,2.52-2.67,3.78-6.43,3.78v-2.59c2.17,0,3.26-1.01,3.26-3.03v-8.55h3.33v7.32h3.5v3.07h0ZM36.27,13.32h-2.93v-2.64h2.93v2.64Z"/>
<path fill="#5e5f5f" d="M53.17,22.2c0,3.07-2.13,5.23-5.44,5.23-2.01,0-3.52-.67-4.49-1.97h-4.25v-3.07h3.28c0-3.28,1.94-5.37,5.34-5.37s5.55,2.04,5.55,5.18ZM49.89,22.31c0-1.81-.71-2.73-2.1-2.73h-.24c-1.21,0-2.03,1.08-2.03,2.59s.8,2.64,2.22,2.64c1.3,0,2.15-1.01,2.15-2.5h0Z"/>
<path fill="#ec1f2d" d="M104.69,0h-34.38c-5.31,0-9.61,4.17-9.61,9.32v33.36c0,5.15,4.3,9.32,9.61,9.32h34.38c5.31,0,9.61-4.17,9.61-9.32V9.32c0-5.15-4.3-9.32-9.61-9.32Z"/>
<path fill="#ffffff" d="M107.74,34.57l-.03,10.61-14.28-.04c-1.28,0-2.16-1.23-2.22-3.11,0-2.95-.9-5.86-2.69-8.25-3.37-4.51-8.12-8.12-14.29-10.8-1.52-.66-3.18-1-4.85-.99-1.32-.08-2.2-1.54-2.2-3.37v-10.57s14.09,0,14.09,0c1.27,0,2.28,1.27,2.34,3.15,0,2.05.58,4.07,1.72,5.79,3.85,5.77,8.76,10.4,15.33,13.26,1.52.66,3.18.99,4.85,1,1.32.08,2.22,1.5,2.22,3.32Z"/>
<path fill="#ffffff" d="M83.66,32.91c-.01.15-.02.3-.02.46v7.34s-.01,1.27-.01,1.27c-.07,1.88-1.12,3.19-2.39,3.19l-10.64-.04c-1.4,0-3.39.01-3.39.01l-.02-10.65c0-1.82.89-3.27,2.21-3.36h3.95s1.51,0,1.51,0h6.83c.92,0,1.7.75,1.98,1.78h0Z"/>
<path fill="#ffffff" d="M107.81,5.37l-.02,13.27c0,1.83-.85,3.33-2.17,3.41h-4s-1.34-.01-1.34-.01c-.09-.02-.19-.02-.28-.02h-6.81c-.9,0-1.67-.72-1.96-1.72.02-.16.03-.33.04-.51v-8.51c.06-1.89,1.12-3.22,2.39-3.22h9.73c2.94-.14,4.43-2.68,4.43-2.68h0Z"/>
<path fill="#5e5f5f" d="M8.84,38.19c-.04-.99-.08-2.18-.08-3.37h-.04c-.26,1.05-.61,2.22-.94,3.18l-1.03,3.2h-1.49l-.9-3.18c-.28-.96-.57-2.13-.77-3.2h-.03c-.05,1.11-.09,2.38-.15,3.4l-.15,3.12h-1.77l.54-8.21h2.55l.83,2.74c.26.95.53,1.97.72,2.94h.04c.24-.95.53-2.03.8-2.95l.9-2.73h2.5l.46,8.21h-1.87l-.12-3.14h0Z"/>
<path fill="#5e5f5f" d="M14.49,39.23l-.6,2.11h-1.98l2.59-8.21h2.51l2.62,8.21h-2.06l-.65-2.11h-2.42ZM16.63,37.84l-.53-1.74c-.15-.49-.3-1.1-.43-1.58h-.03c-.13.49-.25,1.11-.39,1.58l-.5,1.74h1.87Z"/>
<path fill="#5e5f5f" d="M21.45,40.34l4.13-5.63v-.05h-4.13v-1.53h6.67v1.07l-4.04,5.55v.05h4.1v1.53h-6.74v-1h0Z"/>
<path fill="#5e5f5f" d="M32.58,39.23l-.6,2.11h-1.98l2.59-8.21h2.51l2.62,8.21h-2.06l-.65-2.11h-2.42ZM34.73,37.84l-.53-1.74c-.15-.49-.3-1.1-.43-1.58h-.03c-.13.49-.25,1.11-.39,1.58l-.5,1.74h1.87Z"/>
<path fill="#5e5f5f" d="M39.86,41.34v-3.36l-2.67-4.85h2.18l.87,1.97c.26.6.44,1.04.65,1.57h.03c.19-.51.39-.99.64-1.57l.85-1.97h2.15l-2.79,4.79v3.42h-1.91,0Z"/>
<path fill="#5e5f5f" d="M46.54,39.23l-.6,2.11h-1.98l2.59-8.21h2.51l2.62,8.21h-2.06l-.65-2.11h-2.42ZM48.69,37.84l-.53-1.74c-.15-.49-.3-1.1-.43-1.58h-.03c-.13.49-.25,1.11-.39,1.58l-.5,1.74h1.87Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.02 94.61">
<defs>
<style>
.cls-1 {
fill: #5e5f5f;
}
.cls-2 {
fill: #ec222e;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<g>
<path fill="#5e5f5f" d="M16.59,77.8H4.53c-2.6,0-4.1-1.5-4.1-4v-11.65h3.4v9.88c0,1.65.41,2.52,2.91,2.52h9.85v3.25Z"/>
<path fill="#5e5f5f" d="M18.09,81.83h-3.01v-2.81h3.01v2.81ZM22.89,73.12c0,1.29-.36,2.35-1.07,3.2-.8.99-1.92,1.48-3.32,1.48h-2.89v-3.25h2.16c1.14,0,1.72-.8,1.72-2.38v-5.36h3.4v6.31ZM22.67,81.83h-3.03v-2.81h3.03v2.81Z"/>
<path fill="#5e5f5f" d="M28.76,77.8h-3.4v-15.65h3.4v15.65Z"/>
<path fill="#5e5f5f" d="M41.48,77.8h-3.76c-.56,2.67-2.74,4-6.6,4v-2.74c2.23,0,3.35-1.07,3.35-3.2v-9.05h3.42v7.74h3.59v3.25ZM37.67,64.97h-3.01v-2.79h3.01v2.79Z"/>
<path fill="#5e5f5f" d="M55.02,74.36c0,3.25-2.18,5.53-5.58,5.53-2.06,0-3.62-.7-4.61-2.09h-4.37v-3.25h3.37c0-3.47,1.99-5.68,5.48-5.68s5.7,2.16,5.7,5.48ZM51.64,74.48c0-1.92-.73-2.89-2.16-2.89h-.24c-1.24,0-2.09,1.14-2.09,2.74s.82,2.79,2.28,2.79c1.33,0,2.21-1.07,2.21-2.64Z"/>
</g>
<g>
<rect fill="#ffffff" y="0" width="55.02" height="55.02" rx="9.86" ry="9.86"/>
<g>
<path fill="#ec1f2d" d="M48.28,36.58l-.03,11.22-14.66-.04c-1.31,0-2.21-1.3-2.27-3.29h0c0-3.13-.93-6.2-2.76-8.73-3.46-4.77-8.33-8.59-14.67-11.43-1.56-.7-3.26-1.06-4.98-1.05h0c-1.35-.09-2.26-1.63-2.26-3.56v-11.19s14.46,0,14.46,0c1.31,0,2.34,1.34,2.4,3.33h0c0,2.17.59,4.31,1.77,6.13,3.95,6.11,9,11,15.74,14.03,1.56.7,3.26,1.05,4.98,1.06h0c1.35.09,2.28,1.58,2.28,3.52Z"/>
<path fill="#ec1f2d" d="M23.56,34.82c-.01.16-.02.32-.02.48v7.77s-.02,1.34-.02,1.34c-.07,1.99-1.15,3.37-2.45,3.37l-10.92-.05c-1.44,0-3.48.01-3.48.01l-.02-11.27c0-1.93.91-3.46,2.26-3.55h4.05s.26,0,.26,0h1.3s7.01,0,7.01,0c.94,0,1.75.79,2.03,1.88Z"/>
<path fill="#ec1f2d" d="M48.36,5.69l-.02,14.04c0,1.93-.88,3.52-2.23,3.61h-4.1s-1.38-.01-1.38-.01c-.1-.02-.19-.02-.29-.02h-6.99c-.93,0-1.72-.76-2.01-1.82.02-.17.03-.35.04-.54v-9.01c.06-2,1.15-3.41,2.45-3.41h9.99c3.02-.15,4.55-2.83,4.55-2.83Z"/>
</g>
</g>
<g>
<path fill="#5e5f5f" d="M9.5,91.28c-.04-1.04-.08-2.31-.08-3.57h-.04c-.27,1.11-.63,2.35-.97,3.36l-1.06,3.39h-1.53l-.93-3.36c-.28-1.02-.58-2.26-.79-3.39h-.03c-.05,1.17-.09,2.51-.15,3.6l-.15,3.3h-1.82l.55-8.69h2.62l.85,2.9c.27,1.01.54,2.09.73,3.11h.04c.24-1.01.54-2.15.82-3.12l.93-2.89h2.56l.48,8.69h-1.92l-.13-3.32Z"/>
<path fill="#5e5f5f" d="M15.3,92.38l-.62,2.23h-2.04l2.66-8.69h2.58l2.69,8.69h-2.11l-.67-2.23h-2.49ZM17.51,90.91l-.54-1.84c-.15-.52-.31-1.16-.44-1.68h-.03c-.13.52-.26,1.17-.4,1.68l-.52,1.84h1.92Z"/>
<path fill="#5e5f5f" d="M22.45,93.55l4.24-5.95v-.05h-4.24v-1.62h6.85v1.13l-4.15,5.88v.05h4.21v1.62h-6.92v-1.06Z"/>
<path fill="#5e5f5f" d="M33.88,92.38l-.62,2.23h-2.04l2.66-8.69h2.58l2.69,8.69h-2.11l-.67-2.23h-2.49ZM36.08,90.91l-.54-1.84c-.15-.52-.31-1.16-.44-1.68h-.03c-.13.52-.26,1.17-.4,1.68l-.52,1.84h1.92Z"/>
<path fill="#5e5f5f" d="M41.35,94.61v-3.56l-2.75-5.13h2.24l.89,2.09c.27.63.45,1.1.67,1.66h.03c.19-.54.4-1.04.66-1.66l.88-2.09h2.2l-2.86,5.07v3.62h-1.96Z"/>
<path fill="#5e5f5f" d="M48.21,92.38l-.62,2.23h-2.04l2.66-8.69h2.58l2.69,8.69h-2.11l-.67-2.23h-2.49ZM50.41,90.91l-.54-1.84c-.15-.52-.31-1.16-.44-1.68h-.03c-.13.52-.26,1.17-.4,1.68l-.52,1.84h1.92Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="map-pin">
<path id="Vector" d="M9.9996 4.99997C9.9996 7.4967 7.23038 10.0969 6.30047 10.9C6.21384 10.9652 6.10839 11.0004 6 11.0004C5.89161 11.0004 5.78616 10.9652 5.69953 10.9C4.76962 10.0969 2.0004 7.4967 2.0004 4.99997C2.0004 3.93901 2.42179 2.9215 3.17186 2.17128C3.92193 1.42107 4.93924 0.9996 6 0.9996C7.06076 0.9996 8.07807 1.42107 8.82814 2.17128C9.57822 2.9215 9.9996 3.93901 9.9996 4.99997ZM7.49985 4.99997C7.49985 5.82847 6.82834 6.50011 6 6.50011C5.17166 6.50011 4.50015 5.82847 4.50015 4.99997C4.50015 4.17147 5.17166 3.49983 6 3.49983C6.82834 3.49983 7.49985 4.17147 7.49985 4.99997Z" stroke-linecap="round" stroke="#147D6B"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 826 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="search">
<path id="Vector" d="M15.7501 15.7501L12.4951 12.4951M14.25 8.25C14.25 11.5637 11.5637 14.25 8.25 14.25C4.93629 14.25 2.25 11.5637 2.25 8.25C2.25 4.93629 4.93629 2.25 8.25 2.25C11.5637 2.25 14.25 4.93629 14.25 8.25Z" stroke-width="1.7" stroke-linecap="round" stroke="#78817F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="user-round">
<path id="Vector" d="M11 11.9167C13.5311 11.9167 15.5829 9.86464 15.5829 7.33333C15.5829 4.80203 13.5311 2.75 11 2.75C8.46895 2.75 6.41712 4.80203 6.41712 7.33333C6.41712 9.86464 8.46895 11.9167 11 11.9167ZM11 11.9167C12.9447 11.9167 14.8098 12.6893 16.1849 14.0646C17.5601 15.4398 18.3326 17.3051 18.3326 19.25M11 11.9167C9.05527 11.9167 7.1902 12.6893 5.81507 14.0646C4.43994 15.4398 3.6674 17.3051 3.6674 19.25" stroke-width="1.7" stroke-linecap="round" stroke="#78817F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 674 B

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_120_1007)">
<path d="M13.125 5C13.125 3.27411 11.7259 1.875 10 1.875C8.27411 1.875 6.875 3.27411 6.875 5V10C6.875 11.7259 8.27411 13.125 10 13.125C11.7259 13.125 13.125 11.7259 13.125 10V5Z" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 15.625V18.75" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.625 10C15.625 11.4918 15.0324 12.9226 13.9775 13.9775C12.9226 15.0324 11.4918 15.625 10 15.625C8.50816 15.625 7.07742 15.0324 6.02252 13.9775C4.96763 12.9226 4.375 11.4918 4.375 10" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_120_1007">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 903 B

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8744 6.13098L7.58175 10.4694L2.69958 7.41575C2.00006 6.97811 2.14558 5.91558 2.93679 5.6842L14.5284 2.28958C15.253 2.07722 15.9244 2.75462 15.7092 3.4815L12.2798 15.0651C12.0448 15.8574 10.9884 15.999 10.5549 15.2965L7.5795 10.4701" stroke="#0D121C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because it is too large Load Diff

@ -553,7 +553,7 @@
"confirmRevokeMazayaAccess": "Are you sure you want to revoke Mazaya access to \"{}\"?",
"yes": "Yes",
"no": "No",
"mazayaInviteInstructions": "Please download the Mohemm app from the above link and use the following credentials to login",
"mazayaInviteInstructions": "Please download the MoheM app from the above link and use the following credentials to login",
"cameraPermissionRequiredToScan": "Please grant camera permission to scan the QR code",
"cameraPermissionPermanentlyDenied": "Please allow camera permission from the app settings > Permissions",
"openSettings": "Open Settings",
@ -703,8 +703,8 @@
"copyLink": "Copy Link",
"done": "Done",
"linkCopied": "Link copied to clipboard",
"mazayaAccessRevoked": "Mazaya access revoked",
"mazayaAccessRevokedDesc": "Your sponsor has revoked your dependent access. You can no longer open offers or sign in with biometrics.",
"mazayaAccessRevoked": "Mazaya Access",
"mazayaAccessRevokedDesc": "You are not authorized to access the Mazaya Program.",
"contactYourSponsor": "Contact your sponsor",
"contactYourSponsorDesc": "Ask your sponsor to allow Mazaya access again from Dependent Details. Only the sponsor can restore access.",
"dependentAccessBlocked": "Dependent access blocked",
@ -734,7 +734,7 @@
"searchDependents": "Search dependents",
"searchFamilyMembers": "Search family members",
"addAnotherUser": "Add another user",
"joinMazaya": "Join Mazaya",
"joinMazaya": "Join Mazaya - Coming Soon",
"joinMazayaSubtitle": "Get the best offers and exclusive deals across dining, shopping, and lifestyle rewards.",
"continueAsEmployee": "Continue as an employee",
"continueAsEmployeeSubtitle": "Sign in with your employee username and password.",
@ -786,6 +786,7 @@
"showAllLocations": "Show all {} locations",
"searchBranches": "Search branches or cities...",
"allCities": "All",
"allCitiesOption": "All Cities",
"noBranchesFound": "No branches found",
"browseAllBranches": "Browse all {} branches across cities",
"branchesCount": "{} Branches",
@ -802,7 +803,93 @@
"loginMethod": "Login method",
"nationalIdAndDob": "National ID & Date of Birth",
"sponsorOtp": "Sponsor OTP",
"revokingPreventsAccessDesc": "Revoking prevents all future dependent access until you allow it again."
"revokingPreventsAccessDesc": "Revoking prevents all future dependent access until you allow it again.",
"locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance",
"locationServicesRequiredForAttendance": "You need to enable location services to mark attendance",
"pleaseProvideLocationPermission": "Please provide location permission",
"microphoneAccessRequired": "The app needs microphone access to be able to record audio.",
"shareAccessGuide": "Share Access Guide",
"nationalIdOrIqamaNumber": "National ID or Iqama Number",
"multipleCities": "Multi-city",
"filters": "Filters",
"availability": "Availability",
"chooseOneOrBoth": "Choose one or both",
"sortBy": "Sort by",
"recommended": "Recommended",
"nearest": "Nearest",
"highestDiscount": "Highest discount",
"newest": "Newest",
"distance": "Distance",
"fromYourCurrentLocation": "From your current location",
"any": "Any",
"minimumDiscount": "Minimum discount",
"reset": "Reset",
"applyFilters": "Apply filters",
"distanceKm": "{} km",
"discountPercentPlus": "{}%+",
"visitMerchantWebsite": "Visit Merchant Website",
"merchantWebsiteNotAvailable": "Merchant website not available",
"locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers",
"locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers",
"mazayaDeals": "Mazaya Deals",
"helloName": "Hello, {}",
"findABrandOrSomething": "Find a brand or a little something",
"exploreYourBenefits": "Explore your benefits",
"theMazayaEdit": "The Mazaya Edit",
"exclusivelyYours": "Exclusively HMG",
"aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.",
"seeAll": "See all",
"latestOnMazaya": "Latest on Mazaya",
"goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.",
"travelEssentials": "Travel Essentials",
"carServices": "Car Services",
"fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.",
"homeAndKitchen": "Home & Kitchen",
"curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.",
"discover": "Discover",
"saved": "Saved",
"myFamily": "My Family",
"educationAndTraining": "Education & Training",
"foodsAndDrinks": "Foods & Drinks",
"travelAndEntertainment": "Travel & Entertainment",
"beautyAndFitness": "Beauty & Fitness",
"healthcare": "Healthcare",
"favoriteAddedSuccess": "Added to favorites successfully",
"favoriteRemovedSuccess": "Removed from favorites successfully",
"favoriteError": "An error occurred while updating favorites",
"favoriteMerchant": "Favorite Offers",
"favoriteOffers": "Favorite Offers",
"goodToKnow": "Good to know",
"goodToKnowDesc": "Keep your Mazaya membership handy. Check the merchant's availability, eligible activities and full terms before you book.",
"termsAndConditions": "Terms & conditions",
"haveAQuestionAboutOffer": "Have a question about this offer?",
"getHelp": "Get help",
"redeemOffer": "Redeem offer",
"merchantContact": "Merchant contact",
"website": "Website",
"phone": "Phone",
"viewAll": "View all",
"moreCitiesCount": "+ {} more cities",
"mazayaDeals": "Mazaya Deals",
"exploreYourBenefits": "Explore your benefits",
"findAnEscapeOrAnExperience": "Find an escape or an experience",
"allOffers": "All offers",
"sampleAvailability": "sample availability",
"offersCount": "{} offers",
"mazayaExclusive": "MAZAYA EXCLUSIVE",
"youAreAllCaughtUp": "You're all caught up. More little extras soon.",
"selectCity": "Select City",
"searchCities": "Search cities",
"amuLastThreeMonths": "AMU (Last 3 months)",
"buyerDetails": "Buyer Details",
"chatBot": "ChatBot",
"helloAskMe": "Hello, ask me",
"anything": "anything...",
"thinking": "Thinking",
"waitingForResponse": "Waiting for response...",
"typeAMessage": "Type a message..",
"initializingChatPleaseWait": "Please wait, initializing chat...",
"am": "AM",
"pm": "PM"
}

@ -0,0 +1,699 @@
#!/bin/bash
# Enhanced ELF Alignment Checker for Android 16KB Page Size Compatibility
# This script checks if your app's native libraries are compatible with 16KB page size devices
# as required by Google Play starting November 1st, 2025 for apps targeting Android 15+
progname="${0##*/}"
progname="${progname%.sh}"
# Color codes and formatting - detect if colors are supported
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && tput colors >/dev/null 2>&1 && [[ $(tput colors) -ge 8 ]]; then
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
CYAN="\033[36m"
PURPLE="\033[35m"
BOLD="\033[1m"
DIM="\033[2m"
ENDCOLOR="\033[0m"
USE_COLORS=true
else
# No color support - use plain text
RED=""
GREEN=""
YELLOW=""
BLUE=""
CYAN=""
PURPLE=""
BOLD=""
DIM=""
ENDCOLOR=""
USE_COLORS=false
fi
# Unicode symbols with fallbacks
if [[ $USE_COLORS == true ]]; then
CHECK_MARK="✓"
CROSS_MARK="✗"
WARNING_MARK="⚠"
INFO_MARK="ℹ"
ROCKET="🚀"
GEAR="⚙"
PACKAGE="📦"
DOCUMENT="📄"
ALERT="🚨"
PARTY="🎉"
TOOLS="🔧"
BOOKS="📚"
ARROW="▶"
else
CHECK_MARK="[OK]"
CROSS_MARK="[X]"
WARNING_MARK="[!]"
INFO_MARK="[i]"
ROCKET="[>]"
GEAR="[*]"
PACKAGE="[P]"
DOCUMENT="[D]"
ALERT="[!]"
PARTY="[*]"
TOOLS="[T]"
BOOKS="[B]"
ARROW=">"
fi
# Enhanced formatting functions
print_banner() {
local title="$1"
local subtitle="$2"
local width=88
echo
printf "%*s\n" $width | tr ' ' '═'
# Main title
local title_len=${#title}
local title_padding=$(( (width - title_len - 4) / 2 ))
printf "║${BOLD}${BLUE}%*s %s %*s${ENDCOLOR}║\n" \
$title_padding "" "$title" $title_padding ""
# Subtitle if provided
if [[ -n "$subtitle" ]]; then
local subtitle_len=${#subtitle}
local subtitle_padding=$(( (width - subtitle_len - 4) / 2 ))
printf "║${DIM}%*s %s %*s${ENDCOLOR}║\n" \
$subtitle_padding "" "$subtitle" $subtitle_padding ""
fi
printf "%*s\n" $width | tr ' ' '═'
echo
}
print_section() {
local icon="$1"
local title="$2"
local description="$3"
echo
echo -e "${BOLD}${BLUE}$icon $title${ENDCOLOR}"
if [[ -n "$description" ]]; then
echo -e "${DIM}$description${ENDCOLOR}"
fi
printf "${BLUE}%*s${ENDCOLOR}\n" 80 | tr ' ' '─'
}
print_status() {
local status="$1"
local message="$2"
local detail="$3"
case $status in
"success")
printf " ${GREEN}${CHECK_MARK}${ENDCOLOR} %s" "$message"
;;
"error")
printf " ${RED}${CROSS_MARK}${ENDCOLOR} %s" "$message"
;;
"warning")
printf " ${YELLOW}${WARNING_MARK}${ENDCOLOR} %s" "$message"
;;
"info")
printf " ${CYAN}${INFO_MARK}${ENDCOLOR} %s" "$message"
;;
"processing")
printf " ${PURPLE}${GEAR}${ENDCOLOR} %s" "$message"
;;
esac
if [[ -n "$detail" ]]; then
printf "\n ${DIM}%s${ENDCOLOR}" "$detail"
fi
echo
}
print_subsection() {
local title="$1"
echo
echo -e " ${BOLD}${PURPLE}${ARROW} $title${ENDCOLOR}"
printf " ${PURPLE}%*s${ENDCOLOR}\n" 50 | tr ' ' '┈'
}
print_table_header() {
echo
local line1="┌$(printf '─%.0s' {1..29})┬$(printf '─%.0s' {1..18})┬$(printf '─%.0s' {1..15})┬$(printf '─%.0s' {1..13})┐"
local line2="│ $(printf '%-27s' 'Library') │ $(printf '%-16s' 'Architecture') │ $(printf '%-13s' 'Alignment') │ $(printf '%-11s' 'Status') │"
local line3="├$(printf '─%.0s' {1..29})┼$(printf '─%.0s' {1..18})┼$(printf '─%.0s' {1..15})┼$(printf '─%.0s' {1..13})┤"
echo -e "${CYAN}$line1${ENDCOLOR}"
echo -e "${BOLD}${CYAN}$line2${ENDCOLOR}"
echo -e "${CYAN}$line3${ENDCOLOR}"
}
print_table_row() {
local lib="$1"
local arch="$2"
local alignment="$3"
local status="$4"
local is_critical="$5"
local status_symbol
local status_color
local status_text
if [[ $status == "ALIGNED" ]]; then
status_symbol="$CHECK_MARK"
status_color="$GREEN"
status_text="PASS"
else
status_symbol="$CROSS_MARK"
if [[ $is_critical == "true" ]]; then
status_color="$RED"
status_text="FAIL"
else
status_color="$YELLOW"
status_text="WARN"
fi
fi
# Truncate library name if too long
local display_lib="$lib"
if [[ ${#lib} -gt 27 ]]; then
display_lib="${lib:0:24}..."
fi
printf "${CYAN}│${ENDCOLOR} %-27s ${CYAN}│${ENDCOLOR} %-16s ${CYAN}│${ENDCOLOR} %-13s ${CYAN}│${ENDCOLOR} ${status_color}%s %-7s${ENDCOLOR} ${CYAN}│${ENDCOLOR}\n" \
"$display_lib" "$arch" "$alignment" "$status_symbol" "$status_text"
if [[ $is_critical == "true" && $status == "UNALIGNED" ]]; then
printf "${CYAN}│${ENDCOLOR} ${RED}%-75s${ENDCOLOR} ${CYAN}│${ENDCOLOR}\n" " ${ALERT} CRITICAL: Required for Google Play compliance!"
fi
}
print_table_footer() {
local line="└$(printf '─%.0s' {1..29})┴$(printf '─%.0s' {1..18})┴$(printf '─%.0s' {1..15})┴$(printf '─%.0s' {1..13})┘"
echo -e "${CYAN}$line${ENDCOLOR}"
echo
}
print_summary_box() {
local title="$1"
local status="$2" # success, warning, error
shift 2
local lines=("$@")
local box_color
local title_color
case $status in
"success") box_color="$GREEN"; title_color="$GREEN" ;;
"warning") box_color="$YELLOW"; title_color="$YELLOW" ;;
"error") box_color="$RED"; title_color="$RED" ;;
*) box_color="$BLUE"; title_color="$BLUE" ;;
esac
local width=78
echo
printf "${box_color}┌%*s┐${ENDCOLOR}\n" $((width-2)) | tr ' ' '─'
# Title
local title_len=${#title}
local title_padding=$(( (width - title_len - 4) / 2 ))
printf "${box_color}│${ENDCOLOR}${BOLD}${title_color}%*s %s %*s${ENDCOLOR}${box_color}│${ENDCOLOR}\n" \
$title_padding "" "$title" $title_padding ""
printf "${box_color}├%*s┤${ENDCOLOR}\n" $((width-2)) | tr ' ' '─'
# Content lines
for line in "${lines[@]}"; do
printf "${box_color}│${ENDCOLOR} %-*s ${box_color}│${ENDCOLOR}\n" $((width-4)) "$line"
done
printf "${box_color}└%*s┘${ENDCOLOR}\n" $((width-2)) | tr ' ' '─'
echo
}
cleanup_trap() {
if [ -n "${tmp}" -a -d "${tmp}" ]; then
print_status "processing" "Cleaning up temporary files..."
rm -rf "${tmp}"
fi
exit $1
}
usage() {
print_banner "Android 16KB Page Size Compatibility Checker" "Google Play Compliance Tool"
echo -e "${BOLD}DESCRIPTION:${ENDCOLOR}"
echo " This tool verifies that your Android app's native libraries are compatible"
echo " with 16KB page size devices, as required by Google Play starting November 1st, 2025."
echo
echo -e "${BOLD}USAGE:${ENDCOLOR}"
echo -e " ${GREEN}$progname${ENDCOLOR} ${CYAN}[input-path|input-APK|input-APEX]${ENDCOLOR}"
echo
echo -e "${BOLD}EXAMPLES:${ENDCOLOR}"
echo -e " ${DIM}# Check an APK file${ENDCOLOR}"
echo -e " $progname ${CYAN}app/build/outputs/apk/release/app-release.apk${ENDCOLOR}"
echo
echo -e " ${DIM}# Check a directory of native libraries${ENDCOLOR}"
echo -e " $progname ${CYAN}/path/to/native/libs/${ENDCOLOR}"
echo
echo -e "${BOLD}WHAT THIS TOOL CHECKS:${ENDCOLOR}"
echo -e " ${CHECK_MARK} APK zip-alignment for 16KB boundaries (requires build-tools 35.0.0+)"
echo -e " ${CHECK_MARK} ELF segment alignment in native libraries (arm64-v8a and x86_64)"
echo -e " ${CHECK_MARK} Compliance with Android 16KB page size requirements"
echo
echo -e "${BOLD}RESULT MEANINGS:${ENDCOLOR}"
echo -e " ${GREEN}${CHECK_MARK} PASS${ENDCOLOR} - Library is compatible with 16KB page sizes (2**14 or higher)"
echo -e " ${RED}${CROSS_MARK} FAIL${ENDCOLOR} - Library needs recompilation with 16KB ELF alignment"
echo -e " ${YELLOW}${WARNING_MARK} WARN${ENDCOLOR} - Non-critical architecture but should be fixed"
echo
}
# Enhanced dependency checking
check_dependencies() {
print_section "$GEAR" "Dependency Check" "Verifying required tools are available"
local missing_tools=()
local available_tools=()
# Check each tool
local tools=("objdump" "unzip" "file")
for tool in "${tools[@]}"; do
if command -v "$tool" >/dev/null 2>&1; then
available_tools+=("$tool")
print_status "success" "$tool" "$(which "$tool")"
else
missing_tools+=("$tool")
print_status "error" "$tool not found"
fi
done
if [ ${#missing_tools[@]} -gt 0 ]; then
echo
print_subsection "Installation Instructions"
for tool in "${missing_tools[@]}"; do
case $tool in
objdump)
print_status "info" "Install objdump:" "macOS: Xcode Command Line Tools or Android NDK's llvm-objdump"
print_status "info" "" "Linux: sudo apt-get install binutils (Ubuntu/Debian)"
;;
unzip)
print_status "info" "Install unzip:" "Usually pre-installed on most systems"
print_status "info" "" "Linux: sudo apt-get install unzip"
;;
file)
print_status "info" "Install file:" "Usually pre-installed on most systems"
print_status "info" "" "Linux: sudo apt-get install file"
;;
esac
echo
done
exit 1
fi
print_status "success" "All dependencies satisfied" "${#available_tools[@]} tools available"
}
# Validate input arguments
if [ ${#} -ne 1 ]; then
usage
exit 1
fi
case ${1} in
--help | -h | -\?)
usage
exit 0
;;
*)
dir="${1}"
;;
esac
# Validate input file/directory
if ! [ -f "${dir}" -o -d "${dir}" ]; then
print_status "error" "Invalid input: ${dir}"
echo " Please provide a valid APK file, APEX file, or directory containing native libraries."
exit 1
fi
# Check dependencies before proceeding
check_dependencies
print_banner "ANALYSIS IN PROGRESS" "Checking 16KB Page Size Compatibility"
print_status "processing" "Target: $(basename "${dir}")"
print_status "info" "Compliance Deadline: November 1st, 2025"
print_status "info" "Requirement: Apps targeting Android 15+ must support 16KB pages"
# APK Processing
if [[ "${dir}" == *.apk ]]; then
trap 'cleanup_trap' EXIT
print_section "$PACKAGE" "APK Analysis" "Processing Android Package file"
# Enhanced zipalign check
if command -v zipalign >/dev/null 2>&1; then
if { zipalign --help 2>&1 | grep -q "\-P <pagesize_kb>"; }; then
print_status "processing" "Checking APK zip-alignment for 16KB boundaries..."
zip_result=$(zipalign -v -c -P 16 4 "${dir}" 2>&1)
zip_exit_code=$?
if [ $zip_exit_code -eq 0 ]; then
print_status "success" "APK zip-alignment verification passed"
else
print_status "error" "APK zip-alignment verification failed"
echo " ${DIM}Details:${ENDCOLOR}"
echo "$zip_result" | grep -E 'lib/arm64-v8a|lib/x86_64|Verification|would be' | sed 's/^/ /' || echo " $zip_result"
fi
else
print_status "warning" "zipalign version doesn't support 16KB alignment checks"
print_status "info" "Solution: Update to Android SDK build-tools 35.0.0+"
echo " ${DIM}Update via Android Studio → SDK Manager → SDK Tools${ENDCOLOR}"
echo " ${DIM}Or run: sdkmanager \"build-tools;35.0.0\"${ENDCOLOR}"
fi
else
print_status "warning" "zipalign not found in PATH"
print_status "info" "Ensure Android SDK build-tools are installed and in PATH"
fi
# Extract APK
dir_filename=$(basename "${dir}")
tmp=$(mktemp -d -t "${dir_filename%.apk}_out_XXXXX")
if [ ! -d "${tmp}" ]; then
print_status "error" "Failed to create temporary directory"
exit 1
fi
print_status "processing" "Extracting native libraries from APK..."
if ! unzip -q "${dir}" "lib/*" -d "${tmp}" 2>/dev/null; then
print_summary_box "NO NATIVE LIBRARIES FOUND" "success" \
"${PARTY} Your app contains only Java/Kotlin code!" \
"" \
"${CHECK_MARK} Apps without native libraries automatically support 16KB devices" \
"${CHECK_MARK} No additional changes required for Google Play compliance" \
"${CHECK_MARK} You're all set for the November 1st, 2025 deadline!"
cleanup_trap 0
fi
dir="${tmp}"
fi
# APEX Processing
if [[ "${dir}" == *.apex ]]; then
trap 'cleanup_trap' EXIT
print_section "$DOCUMENT" "APEX Analysis" "Processing Android Pony EXpress file"
if ! command -v deapexer >/dev/null 2>&1; then
print_status "error" "deapexer tool not found"
echo " Please ensure Android SDK tools are properly installed and in your PATH."
exit 1
fi
dir_filename=$(basename "${dir}")
tmp=$(mktemp -d -t "${dir_filename%.apex}_out_XXXXX")
if [ ! -d "${tmp}" ]; then
print_status "error" "Failed to create temporary directory"
exit 1
fi
print_status "processing" "Extracting APEX contents..."
if ! deapexer extract "${dir}" "${tmp}"; then
print_status "error" "Failed to extract APEX file"
cleanup_trap 1
fi
dir="${tmp}"
fi
# Track libraries for enhanced summary
unaligned_libs=()
aligned_libs=()
critical_unaligned_libs=()
non_critical_unaligned_libs=()
total_libs=0
critical_libs=0
print_section "$GEAR" "ELF Segment Analysis" "Scanning native libraries for 16KB alignment compliance"
# Find all native libraries
matches="$(find "${dir}" -type f -name '*.so' 2>/dev/null)"
if [ -z "$matches" ]; then
# Also check for ELF files without .so extension
matches="$(find "${dir}" -type f -exec file {} \; 2>/dev/null | grep 'ELF' | cut -d: -f1)"
fi
if [ -z "$matches" ]; then
print_summary_box "NO NATIVE LIBRARIES DETECTED" "success" \
"${PARTY} Your app uses only Java/Kotlin code!" \
"" \
"${CHECK_MARK} No native library alignment issues to worry about" \
"${CHECK_MARK} Already compatible with 16KB page size devices" \
"${CHECK_MARK} Ready for Google Play's November 1st, 2025 requirement!"
cleanup_trap 0
fi
print_status "info" "Found native libraries - analyzing ELF segment alignment..."
print_table_header
IFS=$'\n'
for match in $matches; do
# Skip non-ELF files and nested packages
if [[ "${match}" == *".apk" ]]; then
continue
fi
if [[ "${match}" == *".apex" ]]; then
continue
fi
# Verify it's actually an ELF file
if ! [[ $(file "${match}" 2>/dev/null) == *"ELF"* ]]; then
continue
fi
total_libs=$((total_libs + 1))
# Determine architecture and criticality
arch="unknown"
is_critical="false"
if [[ "${match}" == *"arm64-v8a"* ]]; then
arch="arm64-v8a"
is_critical="true"
critical_libs=$((critical_libs + 1))
elif [[ "${match}" == *"x86_64"* ]]; then
arch="x86_64"
is_critical="true"
critical_libs=$((critical_libs + 1))
elif [[ "${match}" == *"armeabi-v7a"* ]]; then
arch="armeabi-v7a"
elif [[ "${match}" == *"x86"* ]]; then
arch="x86"
fi
# Check ELF segment alignment
res="$(objdump -p "${match}" 2>/dev/null | grep LOAD | awk '{ print $NF }' | head -1)"
if [ -z "$res" ]; then
print_table_row "$(basename "${match}")" "$arch" "UNKNOWN" "FAILED" "$is_critical"
continue
fi
# Check if alignment meets 16KB requirement (2**14 or higher)
if [[ $res =~ 2\*\*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,}) ]]; then
print_table_row "$(basename "${match}")" "$arch" "$res" "ALIGNED" "$is_critical"
aligned_libs+=("${match}")
else
print_table_row "$(basename "${match}")" "$arch" "$res" "UNALIGNED" "$is_critical"
unaligned_libs+=("${match}")
if [[ "$is_critical" == "true" ]]; then
critical_unaligned_libs+=("${match}")
else
non_critical_unaligned_libs+=("${match}")
fi
fi
done
print_table_footer
# Enhanced Summary Section
if [ ${#unaligned_libs[@]} -gt 0 ]; then
# Failure case - unaligned libraries found
summary_lines=(
"${ALERT} ACTION REQUIRED: Unaligned native libraries detected!"
""
"Analysis Results:"
" • Total libraries scanned: $total_libs"
" • Critical architectures (64-bit): $critical_libs"
" • Libraries aligned: ${#aligned_libs[@]}"
" • Libraries UNALIGNED: ${#unaligned_libs[@]}"
)
if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then
summary_lines+=(" • Critical failures: ${#critical_unaligned_libs[@]} (MUST FIX)")
fi
if [ ${#non_critical_unaligned_libs[@]} -gt 0 ]; then
summary_lines+=(" • Non-critical warnings: ${#non_critical_unaligned_libs[@]} (SHOULD FIX)")
fi
summary_lines+=("")
summary_lines+=("Google Play Compliance: FAILED")
summary_lines+=("Deadline: November 1st, 2025")
print_summary_box "COMPATIBILITY CHECK FAILED" "error" "${summary_lines[@]}"
# Detailed library breakdown
if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then
print_subsection "Critical Libraries Requiring Immediate Attention"
for lib in "${critical_unaligned_libs[@]}"; do
arch_type="64-bit"
[[ "${lib}" == *"arm64-v8a"* ]] && arch_type="ARM64"
[[ "${lib}" == *"x86_64"* ]] && arch_type="x86_64"
print_status "error" "$(basename "$lib")" "$arch_type architecture - Required for Google Play"
done
fi
if [ ${#non_critical_unaligned_libs[@]} -gt 0 ]; then
print_subsection "Non-Critical Libraries (Recommended to Fix)"
for lib in "${non_critical_unaligned_libs[@]}"; do
arch_type="32-bit"
[[ "${lib}" == *"armeabi-v7a"* ]] && arch_type="ARMv7"
[[ "${lib}" == *"x86"* ]] && arch_type="x86"
print_status "warning" "$(basename "$lib")" "$arch_type architecture"
done
fi
# Comprehensive fix instructions
print_section "$TOOLS" "How to Fix Unaligned Libraries" "Step-by-step guide to achieve 16KB compatibility"
print_subsection "Step 1: Update Your Build Environment"
print_status "info" "Android Gradle Plugin (AGP)" "Upgrade to version 8.5.1 or higher"
print_status "info" "Android NDK" "Update to NDK r27 or higher (r28+ recommended)"
print_status "info" "Build Tools" "Ensure Android SDK build-tools 35.0.0+ for zipalign"
print_subsection "Step 2: Configure 16KB ELF Alignment"
echo
echo -e "${BOLD}For NDK r28 and newer:${ENDCOLOR}"
print_status "success" "Automatic Support" "16KB alignment enabled by default - no changes needed!"
echo
echo -e "${BOLD}For NDK r27:${ENDCOLOR}"
print_status "info" "Gradle Configuration" "Add to your app/build.gradle:"
echo -e "${DIM} android {"
echo -e " defaultConfig {"
echo -e " externalNativeBuild {"
echo -e " cmake {"
echo -e " arguments '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'"
echo -e " }"
echo -e " }"
echo -e " }"
echo -e " }${ENDCOLOR}"
print_status "info" "NDK-Build Configuration" "Add to Application.mk:"
echo -e "${DIM} APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true${ENDCOLOR}"
echo
echo -e "${BOLD}For NDK r26 and older:${ENDCOLOR}"
print_status "info" "Manual Linker Flags" "Add to your native build configuration:"
echo -e "${DIM} # For Android.mk:"
echo -e " LOCAL_LDFLAGS += \"-Wl,-z,max-page-size=16384\""
echo
echo -e " # For CMakeLists.txt:"
echo -e " target_link_options(\${CMAKE_PROJECT_NAME} PRIVATE \"-Wl,-z,max-page-size=16384\")${ENDCOLOR}"
print_subsection "Step 3: Update Library Packaging"
print_status "info" "AGP 8.5.1+" "Uncompressed libraries used by default (no changes needed)"
print_status "info" "Older AGP versions" "Add to app/build.gradle:"
echo -e "${DIM} android {"
echo -e " packagingOptions {"
echo -e " jniLibs {"
echo -e " useLegacyPackaging = true"
echo -e " }"
echo -e " }"
echo -e " }${ENDCOLOR}"
print_subsection "Step 4: Build and Verify"
print_status "processing" "Clean Build" "Run ./gradlew clean"
print_status "processing" "Rebuild Project" "Run ./gradlew assembleRelease"
print_status "processing" "Re-run This Script" "Verify all libraries are now aligned"
print_status "processing" "Test on Device" "Use Android 15 emulator with 16KB system image"
print_section "$BOOKS" "Additional Resources" "Learn more about 16KB page size support"
print_status "info" "Official Guide" "https://developer.android.com/guide/practices/page-sizes"
print_status "info" "NDK Documentation" "https://developer.android.com/ndk/guides/"
print_status "info" "Testing Guide" "Use 'adb shell getconf PAGE_SIZE' (should return 16384)"
print_status "info" "Emulator Setup" "Android 15 system images with 16KB page size"
final_message=""
if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then
final_message="CRITICAL: Fix required for Google Play compliance by November 1st, 2025!"
else
final_message="Warning: Non-critical issues found - recommended to fix for complete compatibility"
fi
print_summary_box "NEXT STEPS" "error" \
"$final_message" \
"" \
"1. Update your build tools and NDK version" \
"2. Apply the configuration changes above" \
"3. Clean and rebuild your project" \
"4. Run this script again to verify fixes" \
"5. Test thoroughly on 16KB devices/emulator"
cleanup_trap 1
else
# Success case - all libraries aligned
summary_lines=(
"${PARTY} All native libraries are properly aligned!"
""
"Analysis Results:"
" • Total libraries scanned: $total_libs"
" • Critical architectures: $critical_libs"
" • All libraries: ${#aligned_libs[@]}/${total_libs} ALIGNED"
""
"Google Play Compliance: PASSED ${CHECK_MARK}"
"Ready for November 1st, 2025 deadline!"
)
print_summary_box "COMPATIBILITY CHECK PASSED" "success" "${summary_lines[@]}"
print_section "$ROCKET" "Next Steps" "Ensure complete 16KB compatibility"
print_status "success" "Library Alignment" "All ELF segments properly aligned for 16KB pages"
print_status "info" "Runtime Testing" "Test on Android 15 emulator with 16KB system image"
print_status "info" "Code Review" "Check for hardcoded PAGE_SIZE dependencies in your code"
print_status "info" "Third-party SDKs" "Verify all dependencies are 16KB compatible"
print_status "info" "Zipalign Verification" "Run: zipalign -c -P 16 -v 4 your-app.apk"
print_subsection "Testing Commands"
echo -e "${DIM} # Verify page size on device/emulator${ENDCOLOR}"
echo -e "${DIM} adb shell getconf PAGE_SIZE # Should return: 16384${ENDCOLOR}"
echo
echo -e "${DIM} # Verify APK alignment${ENDCOLOR}"
echo -e "${DIM} zipalign -c -P 16 -v 4 app-release.apk${ENDCOLOR}"
print_summary_box "CONGRATULATIONS!" "success" \
"${PARTY} Your app is 16KB page size compatible!" \
"" \
"${CHECK_MARK} All native libraries meet Google Play requirements" \
"${CHECK_MARK} Ready for devices with 16KB page sizes" \
"${CHECK_MARK} Compliant with November 1st, 2025 deadline" \
"" \
"Recommendation: Test thoroughly on 16KB environment to ensure" \
"no runtime issues exist in your application code."
fi
echo
print_status "info" "Analysis completed at $(date)"
echo

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

@ -3,15 +3,16 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
76AE113C28E078EA00513112 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 76AE113B28E078EA00513112 /* GoogleService-Info.plist */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -39,6 +40,7 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7627313328E45A2400F354F7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
76AE113B28E078EA00513112 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@ -57,7 +59,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */,
0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -75,6 +77,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@ -151,6 +154,9 @@
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@ -179,6 +185,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -242,23 +251,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
6C6E76E4FB19FAF11C24005D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -291,6 +283,23 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
D9FFC0F21D4146458615A992 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -367,7 +376,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -454,7 +463,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -504,7 +513,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -600,6 +609,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

@ -3,9 +3,41 @@
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B74B458B450D74B75744B87BD747314"
BuildableName = "Pods_Runner.framework"
BlueprintName = "Pods-Runner"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"

@ -3,9 +3,8 @@ import Flutter
import Firebase
import flutter_local_notifications
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@ -17,7 +16,11 @@ import flutter_local_notifications
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)
// GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

@ -2,6 +2,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@ -31,6 +52,11 @@
<string>sms</string>
<string>tel</string>
<string>mailto</string>
<string>https</string>
<string>http</string>
<string>comgooglemaps</string>
<string>googlechromes</string>
<string>waze</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
@ -46,19 +72,23 @@
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>This app requires camera access to capture &amp; upload picture as profile image.</string>
<string>This app requires camera access to capture &amp; upload picture as profile image, and scan QR codes.</string>
<key>NSFaceIDUsageDescription</key>
<string>This app requires Face ID to allow biometric authentication for app login.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>This app requires access to local network to connect to company Wi-Fi networks for attendance.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This App requires access to your location to mark your attendance.</string>
<string>This app requires access to your location to mark your attendance and discover nearby offers and merchants.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This App requires access to your location to mark your attendance.</string>
<string>This app requires access to your location to mark your attendance and discover nearby offers and merchants.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This App requires access to your location to mark your attendance.</string>
<string>This app requires access to your location to mark your attendance and discover nearby offers and merchants.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to for call.</string>
<string>This app requires microphone access for calls and speech-to-text input.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires photo library access to select image as document &amp; upload it.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app requires speech recognition to convert your voice to text.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSSpeechRecognitionUsageDescription</key>

@ -20,6 +20,7 @@
<true/>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>

@ -6,7 +6,6 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart' as http;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:mobile_device_identifier/mobile_device_identifier.dart';
import 'package:mohem_flutter_app/api/api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
@ -37,7 +36,7 @@ class DashboardApiClient {
Future<GetAttendanceTracking?> getAttendanceTracking() async {
String url = "${ApiConsts.erpRest}GET_Attendance_Tracking";
Map<String, dynamic> postParams = <String, dynamic>{};
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -51,7 +50,7 @@ class DashboardApiClient {
Future<GenericResponseModel?> getOpenNotifications() async {
String url = "${ApiConsts.erpRest}GET_OPEN_NOTIFICATIONS";
Map<String, dynamic> postParams = <String, dynamic>{};
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -65,7 +64,7 @@ class DashboardApiClient {
Future<GenericResponseModel?> getCOCNotifications() async {
String url = "${ApiConsts.cocRest}Mohemm_ITG_ReviewerAdmin_Pending_Tasks";
Map<String, dynamic> postParams = <String, dynamic>{"Date": DateUtil.getISODateFormat(DateTime.now()), "EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER};
Map<String, dynamic> postParams = {"Date": DateUtil.getISODateFormat(DateTime.now()), "EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -79,7 +78,7 @@ class DashboardApiClient {
Future<ItgFormsModel?> getItgFormsPendingTask() async {
String url = "${ApiConsts.cocRest}ITGFormsPendingTasks";
Map<String, dynamic> postParams = <String, dynamic>{};
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -93,13 +92,13 @@ class DashboardApiClient {
Future<List<GetAccrualBalancesList>> getAccrualBalances(String effectiveDate, {String? empID}) async {
String url = "${ApiConsts.erpRest}GET_ACCRUAL_BALANCES";
Map<String, dynamic> postParams = <String, dynamic>{"P_EFFECTIVE_DATE": effectiveDate};
Map<String, dynamic> postParams = {"P_EFFECTIVE_DATE": effectiveDate};
postParams.addAll(AppState().postParamsJson);
if (empID != null) postParams["P_SELECTED_EMPLOYEE_NUMBER"] = empID;
return await ApiClient().postJsonForObject(
(json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
return responseData.getAccrualBalancesList ?? <GetAccrualBalancesList>[];
return responseData.getAccrualBalancesList ?? [];
},
url,
postParams,
@ -108,13 +107,13 @@ class DashboardApiClient {
Future<List<GetTicketAccuralBalanceModel>> getTicketAccuralBalance(String effectiveDate, {String? empID}) async {
String url = "${ApiConsts.erpRest}GET_TICKET_ACCRUAL_BALANCES";
Map<String, dynamic> postParams = <String, dynamic>{"P_EFFECTIVE_DATE": effectiveDate};
Map<String, dynamic> postParams = {"P_EFFECTIVE_DATE": effectiveDate};
postParams.addAll(AppState().postParamsJson);
if (empID != null) postParams["P_SELECTED_EMPLOYEE_NUMBER"] = empID;
return await ApiClient().postJsonForObject(
(json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
return responseData.getAccrualBalancesTicketList ?? <GetTicketAccuralBalanceModel>[];
return responseData.getAccrualBalancesTicketList ?? [];
},
url,
postParams,
@ -123,7 +122,7 @@ class DashboardApiClient {
Future<GenericResponseModel?> getOpenMissingSwipes() async {
String url = "${ApiConsts.erpRest}GET_OPEN_MISSING_SWIPES";
Map<String, dynamic> postParams = <String, dynamic>{};
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -138,7 +137,7 @@ class DashboardApiClient {
//Menus List
Future<List<ListMenu>> getListMenu() async {
String url = "${ApiConsts.erpRest}GET_MENU";
Map<String, dynamic> postParams = <String, dynamic>{};
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
@ -276,7 +275,7 @@ class DashboardApiClient {
String QRValue = "",
String payrollCode = "",
}) async {
var _mobileDeviceIdentifierPlugin = MobileDeviceIdentifier();
Codec<String, String> stringToBase64 = utf8.fuse(base64);
String url = "${ApiConsts.swpRest}AuthenticateAndSwipeUserSupportNFC";
Uuid uuid = Uuid();
@ -284,7 +283,7 @@ class DashboardApiClient {
// Generate a v4 (random) id
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo;
deviceId = await _mobileDeviceIdentifierPlugin.getDeviceId() ?? androidInfo.id;
deviceId = await androidInfo.id;
deviceId = stringToBase64.encode(deviceId);
deviceId = deviceId.length >= 50 ? deviceId.substring(0, 48) : deviceId;
} else if (Platform.isIOS) {
@ -502,4 +501,32 @@ class DashboardApiClient {
postParams,
);
}
Future<GenericResponseModel?> getChatBotSession() async {
String url = "${ApiConsts.erpRest}Atlas_InitiateChat";
Map<String, dynamic> postParams = {};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
return responseData;
},
url,
postParams,
);
}
Future<GenericResponseModel?> sendChatBotMessage({required AtlasInitiateChatResponse atlasChatTokenResponse, required String atlasChatText}) async {
String url = "${ApiConsts.erpRest}Atlas_ContinueChat";
Map<String, dynamic> postParams = <String, dynamic>{"Atlas_AccessToken": atlasChatTokenResponse.atlasAccessToken, "Atlas_ChatSessionID": atlasChatTokenResponse.atlasChatSessionId, "Atlas_ChatText": atlasChatText};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
(json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
return responseData;
},
url,
postParams,
);
}
}

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mohem_flutter_app/api/api_client.dart';
@ -49,6 +50,22 @@ class LoginApiClient {
postParams["MemberType"] = memberType ?? ApiConsts.selectedMemberType.value;
return await ApiClient().postJsonForObject((json) {
GenericResponseModel? responseData = GenericResponseModel.fromJson(json);
if (responseData != null) {
bool? isEmp = responseData.isEmpLastLoginToDevice;
if (isEmp == null && json is Map) {
dynamic raw = json["IsEmpLastLoginToDevice"] ?? json["isEmpLastLoginToDevice"];
if (raw is bool) {
isEmp = raw;
} else if (raw is num) {
isEmp = raw == 1;
} else if (raw is String) {
isEmp = raw.toLowerCase() == "true" || raw == "1";
}
}
AppState().setIsEmpLastLoginToDevice = isEmp;
}
return (responseData.mohemmGetMobileLoginInfoList?.length ?? 0) > 0 ? (responseData.mohemmGetMobileLoginInfoList!.first) : null;
}, url, postParams);
}
@ -98,23 +115,50 @@ class LoginApiClient {
}
Future<GenericResponseModel?> insertMobileLoginInfoNEW(
String email, int sessionId, String employeeName, int loginType, String mobileNumber, String userName, String deviceToken, String deviceType,
String email, int sessionId, String employeeName, int loginType, String mobileNumber, String userName, String? deviceToken, String deviceType,
{int? memberType}) async {
String url = "${ApiConsts.erpRest}Mohemm_InsertMobileLoginInfo";
String devType = deviceType.isNotEmpty ? deviceType : (Platform.isAndroid ? "android" : "ios");
String devToken = (deviceToken != null && deviceToken.isNotEmpty && deviceToken != "null")
? deviceToken
: (AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? ""));
if (devToken.isEmpty || devToken == "null") {
try {
String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
if (savedToken.isNotEmpty && savedToken != "null") {
devToken = savedToken;
}
} catch (_) {}
}
if (devToken.isEmpty || devToken == "null") {
try {
if (!AppState().getIsHuawei) {
String? tok = await FirebaseMessaging.instance.getToken();
if (tok != null && tok.isNotEmpty) {
devToken = tok;
AppState().setDeviceToken = tok;
Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok);
}
}
} catch (_) {}
}
Map<String, dynamic> postParams = {
"MobileNumber": mobileNumber,
"P_USER_NAME": userName,
"UserName": userName,
"CompanyID": 1, // todo 'sikander' @discuss umer for companyID
"DeviceToken": deviceToken,
"DeviceToken": devToken,
"LoginType": loginType,
"EmployeeName": employeeName,
"P_SESSION_ID": sessionId,
"P_EMAIL_ADDRESS": email,
"MemberType": memberType ?? ApiConsts.selectedMemberType.value,
};
postParams["DeviceToken"] = deviceToken;
postParams["DeviceType"] = deviceType;
postParams["DeviceToken"] = devToken;
postParams["DeviceType"] = devType;
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject((json) {
GenericResponseModel? responseData = GenericResponseModel.fromJson(json);
@ -143,17 +187,39 @@ class LoginApiClient {
}) async {
String url = "${ApiConsts.erpRest}Mohemm_InsertMobileLoginInfo_Dependent";
String devType = deviceType ?? (Platform.isAndroid ? "android" : "ios");
String devToken = (deviceToken != null && deviceToken.isNotEmpty)
String devToken = (deviceToken != null && deviceToken.isNotEmpty && deviceToken != "null")
? deviceToken
: (AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? ""));
if (devToken.isEmpty || devToken == "null") {
try {
String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
if (savedToken.isNotEmpty && savedToken != "null") {
devToken = savedToken;
}
} catch (_) {}
}
if (devToken.isEmpty || devToken == "null") {
try {
if (!AppState().getIsHuawei) {
String? tok = await FirebaseMessaging.instance.getToken();
if (tok != null && tok.isNotEmpty) {
devToken = tok;
AppState().setDeviceToken = tok;
Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok);
}
}
} catch (_) {}
}
String effectiveToken = (tokenID != null && tokenID.isNotEmpty)
? tokenID
: (AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID ?? "");
Map<String, dynamic> postParams = {
"VersionID": versionId ?? 5,
"Channel": channel ?? 31,
"Channel": channel,
"LanguageID": context != null ? AppState().getLanguageID(context) : 2,
"MemberType": memberType ?? 1,
"MobileType": devType,
@ -225,13 +291,12 @@ class LoginApiClient {
};
postParams.addAll(AppState().postParamsJson);
postParams["smsSignature"] = sig ?? "";
postParams["SMSSignature"] = sig ?? "";
return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams);
}
Future<GenericResponseModel?> checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName) async {
Future<GenericResponseModel?> checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName, String loginType) async {
String url = "${ApiConsts.erpRest}CheckActivationCode";
Map<String, dynamic> postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName};
Map<String, dynamic> postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName, "LoginType":loginType};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject((json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
@ -246,6 +311,7 @@ class LoginApiClient {
AppState().postParamsInitConfig.setPLegislationCode = responseData.basicMemberInformation!.pLEGISLATIONCODE;
AppState().postParamsObject?.setPayrollCodeStr = responseData.memberInformationList!.first.pAYROLLCODE;
AppState().setBusinessCardPrivilege = responseData.businessCardPrivilege ?? false;
AppState().setPrivilegeListModel = responseData.privilegeList ?? [];
return responseData;
}, url, postParams);
}
@ -345,8 +411,6 @@ class LoginApiClient {
}
Map<String, dynamic> postParams = {
"VersionID": 20,
"Channel": 31,
"LanguageID": context != null ? AppState().getLanguageID(context) : 2,
"DeviceToken": devToken,
"DeviceType": devType,
@ -382,8 +446,6 @@ class LoginApiClient {
AppState().setUserName = nationalId;
AppState().postParamsObject?.setUserName = nationalId;
AppState().postParamsObject?.setPUserName = nationalId;
AppState().postParamsObject?.channel = 31;
AppState().postParamsObject?.versionID = 20;
return responseData;
}, url, postParams);
}
@ -399,7 +461,6 @@ class LoginApiClient {
String? sig = smsSignature ?? await getSignature();
Map<String, dynamic> postParams = {
"VersionID": 20,
"Channel": 31,
"LanguageID": context != null ? AppState().getLanguageID(context) : 2,
"MobileType": Platform.isAndroid ? "android" : "ios",
"OTP_SendType": 1,
@ -408,8 +469,9 @@ class LoginApiClient {
"P_DEP_NATIONAL_ID": nationalId,
"P_USER_NAME": nationalId,
"LogInTokenID": logInTokenID,
"smsSignature": sig ?? "",
"SMSSignature": sig ?? "",
"NotificationCenterAppGUID": ApiConsts.notificationCenterAppGUID,
"NotificationCenterAppSeviceGUID": ApiConsts.notificationCenterLoginServiceGUID,
"smsSignature": (sig != null && sig.isNotEmpty) ? sig : "",
};
return await ApiClient().postJsonForObject((json) {
GenericResponseModel responseData = GenericResponseModel.fromJson(json);
@ -431,8 +493,6 @@ class LoginApiClient {
"P_USER_NAME": nationalId,
"P_DEP_NATIONAL_ID": nationalId,
"MobileNumber": mobileNumber,
"VersionID": 20,
"Channel": 31,
"LanguageID": context != null ? AppState().getLanguageID(context) : 2,
"MobileType": Platform.isAndroid ? "android" : "ios",
};
@ -450,8 +510,6 @@ class LoginApiClient {
AppState().postParamsObject?.setUserName = nationalId;
AppState().postParamsObject?.setPUserName = nationalId;
AppState().postParamsObject?.setMobileNumer = mobileNumber;
AppState().postParamsObject?.channel = 31;
AppState().postParamsObject?.versionID = 20;
AppState().setUserName = nationalId;
AppState().isAuthenticated = true;
AppState().setLogged = true;

@ -104,24 +104,18 @@ class MarathonApiClient {
}
Future<QuestionModel> getNextQuestion({required String? questionId, required String marathonId}) async {
Map<String, String?> jsonObject = <String, String?>{
"previousQuestionId": questionId,
"marathonId": marathonId,
};
Map<String, String?> jsonObject = <String, String?>{"previousQuestionId": questionId, "marathonId": marathonId};
return await ApiClient().postJsonForObject(
(json) {
MarathonGenericModel marathonModel = MarathonGenericModel.fromJson(json);
if (marathonModel.statusCode == 404 || marathonModel.statusCode == 208 || marathonModel.statusCode == 204 || marathonModel.statusCode == 500) {
Utils.confirmDialog(
AppRoutes.navigatorKey.currentContext,
Utils.showToast(
marathonModel.message!,
onTap: () {
onComplete: () {
AppRoutes.navigatorKey.currentContext!.read<MarathonProvider>().resetValues();
Navigator.of(
AppRoutes.navigatorKey.currentContext!,
).popUntil(ModalRoute.withName(AppRoutes.dashboard));
Navigator.of(AppRoutes.navigatorKey.currentContext!).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
}
@ -214,99 +208,99 @@ class MarathonApiClient {
return null;
}
// late HubConnection hubConnection;
// Future<void> buildHubConnection(BuildContext context, String prizeId) async {
// HttpConnectionOptions httpOptions = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true);
// hubConnection = HubConnectionBuilder()
// .withUrl(
// ApiConsts.marathonHubConnectionUrl + "?employeeNumber=${AppState().memberInformationList!.eMPLOYEENUMBER ?? ""}&employeeName=${AppState().memberInformationList!.eMPLOYEENAME ?? ""}",
// options: httpOptions,
// )
// .withAutomaticReconnect(
// retryDelays: <int>[2000, 5000, 10000, 20000],
// )
// .configureLogging(
// Logger("Logging"),
// )
// .build();
// hubConnection.onclose(
// ({Exception? error}) {
// logger.i("onclose");
// },
// );
// hubConnection.onreconnecting(
// ({Exception? error}) {
// logger.i("onreconnecting");
// },
// );
// hubConnection.onreconnected(
// ({String? connectionId}) {
// logger.i("onreconnected");
// },
// );
// if (hubConnection.state != HubConnectionState.Connected) {
// await hubConnection.start();
// logger.i("Started HubConnection");
//
// await hubConnection.invoke(
// "AddParticipant",
// args: <Object>[
// <String, dynamic>{
// "employeeNumber": AppState().memberInformationList!.eMPLOYEENUMBER ?? "",
// "employeeName": AppState().memberInformationList!.eMPLOYEENAME ?? "",
// "marathonId": AppState().getMarathonProjectId,
// "prizeId": "8577B2E8-5DD7-43F0-10DD-08DACB0AC064",
// }
// ],
// ).catchError((e) {
// logger.i("Error in AddParticipant: $e");
// });
//
// context.read<MarathonProvider>().addItemToList(ApiConsts.dummyQuestion);
//
// await hubConnection.invoke(
// "SendQuestionToParticipant",
// args: <Object>[
// <String, dynamic>{
// "marathonId": "${AppState().getMarathonProjectId}",
// }
// ],
// ).catchError((e) {
// Utils.confirmDialog(context, e.toString());
// logger.i("Error in SendQuestionToParticipant: $e");
// });
//
// try {
// hubConnection.on("OnSendQuestionToParticipant", (List<Object?>? arguments) {
// onSendQuestionToParticipant(arguments, context);
// });
// } catch (e, s) {
// logger.i("Error in OnSendQuestionToParticipant");
// }
//
// try {
// hubConnection.on("OnParticipantJoin", (List<Object?>? arguments) {
// onParticipantJoin(arguments, context);
// });
// } catch (e, s) {
// logger.i("Error in OnParticipantJoin");
// }
// }
// }
//
// Future<void> onSendQuestionToParticipant(List<Object?>? arguments, BuildContext context) async {
// logger.i("onSendQuestionToParticipant arguments: $arguments");
//
// if (arguments != null) {
// Map<dynamic, dynamic> data = arguments.first! as Map<dynamic, dynamic>;
// var json = data["data"];
// QuestionModel newQuestion = QuestionModel.fromJson(json);
// AppRoutes.navigatorKey.currentContext!.read<MarathonProvider>().onNewQuestionReceived(newQuestion);
// }
// }
//
// Future<void> onParticipantJoin(List<Object?>? arguments, BuildContext context) async {
// logger.i("OnParticipantJoin arguments: $arguments");
// context.watch<MarathonProvider>().totalMarathoners++;
// }
// late HubConnection hubConnection;
// Future<void> buildHubConnection(BuildContext context, String prizeId) async {
// HttpConnectionOptions httpOptions = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true);
// hubConnection = HubConnectionBuilder()
// .withUrl(
// ApiConsts.marathonHubConnectionUrl + "?employeeNumber=${AppState().memberInformationList!.eMPLOYEENUMBER ?? ""}&employeeName=${AppState().memberInformationList!.eMPLOYEENAME ?? ""}",
// options: httpOptions,
// )
// .withAutomaticReconnect(
// retryDelays: <int>[2000, 5000, 10000, 20000],
// )
// .configureLogging(
// Logger("Logging"),
// )
// .build();
// hubConnection.onclose(
// ({Exception? error}) {
// logger.i("onclose");
// },
// );
// hubConnection.onreconnecting(
// ({Exception? error}) {
// logger.i("onreconnecting");
// },
// );
// hubConnection.onreconnected(
// ({String? connectionId}) {
// logger.i("onreconnected");
// },
// );
// if (hubConnection.state != HubConnectionState.Connected) {
// await hubConnection.start();
// logger.i("Started HubConnection");
//
// await hubConnection.invoke(
// "AddParticipant",
// args: <Object>[
// <String, dynamic>{
// "employeeNumber": AppState().memberInformationList!.eMPLOYEENUMBER ?? "",
// "employeeName": AppState().memberInformationList!.eMPLOYEENAME ?? "",
// "marathonId": AppState().getMarathonProjectId,
// "prizeId": "8577B2E8-5DD7-43F0-10DD-08DACB0AC064",
// }
// ],
// ).catchError((e) {
// logger.i("Error in AddParticipant: $e");
// });
//
// context.read<MarathonProvider>().addItemToList(ApiConsts.dummyQuestion);
//
// await hubConnection.invoke(
// "SendQuestionToParticipant",
// args: <Object>[
// <String, dynamic>{
// "marathonId": "${AppState().getMarathonProjectId}",
// }
// ],
// ).catchError((e) {
// Utils.confirmDialog(context, e.toString());
// logger.i("Error in SendQuestionToParticipant: $e");
// });
//
// try {
// hubConnection.on("OnSendQuestionToParticipant", (List<Object?>? arguments) {
// onSendQuestionToParticipant(arguments, context);
// });
// } catch (e, s) {
// logger.i("Error in OnSendQuestionToParticipant");
// }
//
// try {
// hubConnection.on("OnParticipantJoin", (List<Object?>? arguments) {
// onParticipantJoin(arguments, context);
// });
// } catch (e, s) {
// logger.i("Error in OnParticipantJoin");
// }
// }
// }
//
// Future<void> onSendQuestionToParticipant(List<Object?>? arguments, BuildContext context) async {
// logger.i("onSendQuestionToParticipant arguments: $arguments");
//
// if (arguments != null) {
// Map<dynamic, dynamic> data = arguments.first! as Map<dynamic, dynamic>;
// var json = data["data"];
// QuestionModel newQuestion = QuestionModel.fromJson(json);
// AppRoutes.navigatorKey.currentContext!.read<MarathonProvider>().onNewQuestionReceived(newQuestion);
// }
// }
//
// Future<void> onParticipantJoin(List<Object?>? arguments, BuildContext context) async {
// logger.i("OnParticipantJoin arguments: $arguments");
// context.watch<MarathonProvider>().totalMarathoners++;
// }
}

@ -7,14 +7,18 @@ import 'package:geolocator/geolocator.dart';
import 'package:mohem_flutter_app/api/api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/widgets/location/Location.dart' as location;
class OffersAndDiscountsApiClient {
static final OffersAndDiscountsApiClient _instance = OffersAndDiscountsApiClient._internal();
static final OffersAndDiscountsApiClient _instance =
OffersAndDiscountsApiClient._internal();
static List<WalaPlusCategoryModel>? cachedCategories;
OffersAndDiscountsApiClient._internal();
@ -24,7 +28,12 @@ class OffersAndDiscountsApiClient {
List<GetCategoriesList> getSaleCategoriesList = [];
String url = "${ApiConsts.cocRest}Mohemm_ITG_GetCategories";
Map<String, dynamic> postParams = {"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, "ItgPageSize": 100, "ItgPageNo": 1, "ItgIsActive": true};
Map<String, dynamic> postParams = {
"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER,
"ItgPageSize": 100,
"ItgPageNo": 1,
"ItgIsActive": true,
};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
@ -51,11 +60,19 @@ class OffersAndDiscountsApiClient {
);
}
Future<List<OffersListModel>> getOffersList(int categoryID, int pageSize) async {
Future<List<OffersListModel>> getOffersList(
int categoryID,
int pageSize,
) async {
List<OffersListModel> getSaleCategoriesList = [];
String url = "${ApiConsts.cocRest}GetOfferDiscountsConfigData";
Map<String, dynamic> postParams = {"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, "ItgPageSize": pageSize, "ItgPageNo": 1, "ItgCategoryID": categoryID};
Map<String, dynamic> postParams = {
"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER,
"ItgPageSize": pageSize,
"ItgPageNo": 1,
"ItgCategoryID": categoryID,
};
postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject(
@ -79,7 +96,10 @@ class OffersAndDiscountsApiClient {
String _resolveUserName() {
String? pUserName = AppState().getUserName;
if (pUserName == null || pUserName.isEmpty) {
pUserName = AppState().postParamsObject?.userName ?? AppState().postParamsObject?.pUserName ?? AppState().memberInformationList?.eMPLOYEENUMBER;
pUserName =
AppState().postParamsObject?.userName ??
AppState().postParamsObject?.pUserName ??
AppState().memberInformationList?.eMPLOYEENUMBER;
}
return pUserName ?? "";
}
@ -95,12 +115,15 @@ class OffersAndDiscountsApiClient {
if (!enabledCompleter.isCompleted) enabledCompleter.complete(isEnabled);
});
bool isEnabled = await enabledCompleter.future;
debugPrint("_getCurrentPositionSilently: location service isEnabled=$isEnabled");
debugPrint(
"_getCurrentPositionSilently: location service isEnabled=$isEnabled",
);
if (!isEnabled) return null;
Completer<bool> permissionCompleter = Completer<bool>();
location.Location.havePermission((bool hasPermission) {
if (!permissionCompleter.isCompleted) permissionCompleter.complete(hasPermission);
if (!permissionCompleter.isCompleted)
permissionCompleter.complete(hasPermission);
});
bool hasPermission = await permissionCompleter.future;
debugPrint("_getCurrentPositionSilently: havePermission=$hasPermission");
@ -109,11 +132,16 @@ class OffersAndDiscountsApiClient {
Completer<Position?> positionCompleter = Completer<Position?>();
location.Location.getCurrentLocation(
(Position position, bool isMocked) {
debugPrint("_getCurrentPositionSilently: got position lat=${position.latitude}, lng=${position.longitude}, isMocked=$isMocked");
if (!positionCompleter.isCompleted) positionCompleter.complete(position);
debugPrint(
"_getCurrentPositionSilently: got position lat=${position.latitude}, lng=${position.longitude}, isMocked=$isMocked",
);
if (!positionCompleter.isCompleted)
positionCompleter.complete(position);
},
() {
debugPrint("_getCurrentPositionSilently: getCurrentLocation error callback fired");
debugPrint(
"_getCurrentPositionSilently: getCurrentLocation error callback fired",
);
if (!positionCompleter.isCompleted) positionCompleter.complete(null);
},
context,
@ -132,6 +160,19 @@ class OffersAndDiscountsApiClient {
if (overrideMobile != null && overrideMobile.trim().isNotEmpty) {
return overrideMobile.trim();
}
// postParamsObject.mobileNumber/memberInformationList/memberLoginList are only ever set by
// the employee login flow and never cleared on logout - for a dependent session, the only
// legitimate source is the sponsor's mobile captured at dependent login (walaplusDepInfoModel),
// same reasoning as _resolveUserReference above.
if (ApiConsts.isDependetLogin) {
String? depMobile = AppState().walaplusDepInfoModel?.pEmpMobileNumber;
if (depMobile != null && depMobile.trim().isNotEmpty) {
return depMobile.trim();
}
return "";
}
String? mobile = AppState().postParamsObject?.mobileNumber;
if (mobile != null && mobile.trim().isNotEmpty) {
return mobile.trim();
@ -159,7 +200,9 @@ class OffersAndDiscountsApiClient {
Map<String, dynamic> base = {};
base.addAll(AppState().postParamsJson);
if (ApiConsts.isDependetLogin) {
String? token = AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID;
String? token =
AppState().postParamsObject?.tokenID ??
AppState().authenticationTokenID;
if (token != null && token.isNotEmpty) {
base["TokenID"] = token;
}
@ -181,7 +224,13 @@ class OffersAndDiscountsApiClient {
map.removeWhere((key, value) => value == null);
}
Future<List<WalaPlusCategoryModel>> getWalaPlusCategories({BuildContext? context, String? mobileNumber}) async {
// Pass parentId to fetch the subcategories under a given main category (WP_ParentId) -
// omit it (the default) to fetch the top-level main categories, same as before.
Future<List<WalaPlusCategoryModel>> getWalaPlusCategories({
BuildContext? context,
String? mobileNumber,
String? parentId,
}) async {
String url = ApiConsts.walaPlusCategories;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
@ -196,13 +245,20 @@ class OffersAndDiscountsApiClient {
}
postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1;
postParams["CompanyID"] = 1;
if (parentId != null && parentId.trim().isNotEmpty) {
postParams["WP_Parent_Id"] = parentId.trim();
}
_cleanWalaPlusParams(postParams);
return await ApiClient().postJsonForObject(
List<WalaPlusCategoryModel> list = await ApiClient().postJsonForObject(
(response) {
List<WalaPlusCategoryModel> categories = [];
if (response != null) {
dynamic cats = response['WalaPlus_Categories'] ?? response['walaPlus_Categories'] ?? response['WalaPlusCategories'] ?? response['result'];
dynamic cats =
response['WalaPlus_Categories'] ??
response['walaPlus_Categories'] ??
response['WalaPlusCategories'] ??
response['result'];
if (cats is String) {
try {
cats = json.decode(cats);
@ -215,7 +271,9 @@ class OffersAndDiscountsApiClient {
if (v is Map<String, dynamic>) {
categories.add(WalaPlusCategoryModel.fromJson(v));
} else if (v is Map) {
categories.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(v)));
categories.add(
WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(v)),
);
}
});
}
@ -225,6 +283,62 @@ class OffersAndDiscountsApiClient {
url,
postParams,
);
if ((parentId == null || parentId.trim().isEmpty) && list.isNotEmpty) {
cachedCategories = list;
}
return list;
}
// Fetched fresh every time the city picker opens (no caching) - see
// mazaya_city_selector.dart. Only P_USER_NAME/MobileNumber (resolved the same way as every
// other WalaPlus call) and WP_Country vary per call; everything else is the shared default
// WalaPlus params/token from _getBaseWalaPlusParams.
Future<List<WalaPlusCityModel>> getWalaPlusCities({
String? mobileNumber,
String? country,
}) async {
String url = ApiConsts.walaPlusCities;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["P_USER_NAME"] = pUserName;
if (pMobileNumber.isNotEmpty) {
postParams["MobileNumber"] = pMobileNumber;
}
postParams["WP_Country"] = country ?? "";
_cleanWalaPlusParams(postParams);
return await ApiClient().postJsonForObject(
(response) {
List<WalaPlusCityModel> cities = [];
if (response != null) {
dynamic data =
response['WalaPlus_Cities'] ?? response['walaPlus_Cities'];
if (data is String) {
try {
data = json.decode(data);
} catch (e) {
debugPrint("Error decoding WalaPlus_Cities: $e");
}
}
if (data is List) {
for (var v in data) {
if (v is Map<String, dynamic>) {
cities.add(WalaPlusCityModel.fromJson(v));
} else if (v is Map) {
cities.add(
WalaPlusCityModel.fromJson(Map<String, dynamic>.from(v)),
);
}
}
}
}
return cities;
},
url,
postParams,
);
}
// Confirmed with backend team: WP_Offer_Type accepts "online" and "store".
@ -247,19 +361,35 @@ class OffersAndDiscountsApiClient {
Future<WalaPlusRetailersResponse?> getWalaPlusRetailers({
String? categoryId,
// Every existing caller passes categoryId (even "" for "all") and relies on WP_CategoryID
// always being present in the request - default true preserves that for everyone. Pass
// false only when the category filter must be fully omitted (e.g. a cross-category newest
// feed), not just set to empty.
bool includeCategoryId = true,
int page = 1,
int perPage = 10,
String? geoLocation,
String? offerType,
String? sortBy,
int? radius,
int? minDiscount,
// Confirmed with backend team: WP_City_ID=3 or WP_City_ID=0 both mean "all cities" (no
// filter) - any other id filters to that specific city. Default null -> 0, same "always
// send a concrete int" contract as WP_Radius/WP_Min_Discount below.
int? cityId,
BuildContext? context,
String? mobileNumber,
String? query,
String? userReference,
}) async {
String url = ApiConsts.walaPlusRetailers;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
String pDepNationalId = _resolveDepNationalId();
String effectiveCatId = (categoryId == "all" || categoryId == "0" || categoryId == null) ? "" : categoryId;
String effectiveCatId =
(categoryId == "all" || categoryId == "0" || categoryId == null)
? ""
: categoryId;
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["UserName"] = pUserName;
@ -270,7 +400,9 @@ class OffersAndDiscountsApiClient {
}
postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1;
postParams["CompanyID"] = 1;
postParams["WP_CategoryID"] = effectiveCatId;
if (includeCategoryId) {
postParams["WP_CategoryID"] = effectiveCatId;
}
if (offerType != null && offerType.trim().isNotEmpty) {
postParams["WP_Offer_Type"] = offerType.trim();
}
@ -279,6 +411,14 @@ class OffersAndDiscountsApiClient {
if (query != null && query.trim().isNotEmpty) {
postParams["WP_Query"] = query.trim();
}
if (sortBy != null && sortBy.trim().isNotEmpty) {
postParams["WP_Sort_By"] = sortBy.trim();
}
// Backend contract declares these as non-nullable int DataMembers, so "Any"/"All" (null
// here) must be sent as a concrete 0, not omitted.
postParams["WP_Radius"] = radius ?? 0;
postParams["WP_Min_Discount"] = minDiscount ?? 0;
postParams["WP_City_ID"] = cityId ?? 0;
if (geoLocation != null && geoLocation.isNotEmpty) {
postParams["WP_Geo_Location"] = geoLocation;
@ -286,7 +426,8 @@ class OffersAndDiscountsApiClient {
try {
Position? position = await _getCurrentPositionSilently(context);
if (position != null) {
postParams["WP_Geo_Location"] = "${position.latitude},${position.longitude}";
postParams["WP_Geo_Location"] =
"${position.latitude},${position.longitude}";
}
} catch (e) {
debugPrint("getWalaPlusRetailers: unable to get current location: $e");
@ -294,10 +435,22 @@ class OffersAndDiscountsApiClient {
}
_cleanWalaPlusParams(postParams);
// Pass WP_User_Reference: if filled then user ID, else null value
if (userReference != null && userReference.trim().isNotEmpty) {
postParams["WP_User_Reference"] = userReference.trim();
} else {
postParams["WP_User_Reference"] = null;
}
return await ApiClient().postJsonForObject(
(response) {
if (response != null) {
dynamic val = response['WalaPlus_Retailers'] ?? response['walaPlus_Retailers'] ?? response['WalaPlusRetailers'] ?? response['WP_Retailers'] ?? response['result'];
dynamic val =
response['WalaPlus_Retailers'] ??
response['walaPlus_Retailers'] ??
response['WalaPlusRetailers'] ??
response['WP_Retailers'] ??
response['result'];
if (val is String) {
try {
val = json.decode(val);
@ -308,17 +461,26 @@ class OffersAndDiscountsApiClient {
if (val is Map<String, dynamic>) {
return WalaPlusRetailersResponse.fromJson(val);
} else if (val is Map) {
return WalaPlusRetailersResponse.fromJson(Map<String, dynamic>.from(val));
return WalaPlusRetailersResponse.fromJson(
Map<String, dynamic>.from(val),
);
} else if (val is List) {
List<WalaPlusRetailerModel> retailers = [];
for (var v in val) {
if (v is Map<String, dynamic>) {
retailers.add(WalaPlusRetailerModel.fromJson(v));
} else if (v is Map) {
retailers.add(WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(v)));
retailers.add(
WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(v)),
);
}
}
return WalaPlusRetailersResponse(retailers: retailers, page: 1, pagesCount: 1, perPage: retailers.length);
return WalaPlusRetailersResponse(
retailers: retailers,
page: 1,
pagesCount: 1,
perPage: retailers.length,
);
}
}
return null;
@ -328,11 +490,19 @@ class OffersAndDiscountsApiClient {
);
}
Future<WalaPlusRetailerModel?> getWalaPlusRetailerById({required String retailerId, BuildContext? context, String? mobileNumber}) async {
Future<WalaPlusRetailerModel?> getWalaPlusRetailerById({
required String retailerId,
BuildContext? context,
String? mobileNumber,
String? userReference,
}) async {
String url = ApiConsts.walaPlusRetailersByID;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
String pDepNationalId = _resolveDepNationalId();
String pUserReference = _resolveUserReference(
overrideUserReference: userReference,
);
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["WP_RetailerID"] = retailerId;
@ -347,32 +517,54 @@ class OffersAndDiscountsApiClient {
postParams["CompanyID"] = 1;
if (context != null) {
postParams["LanguageID"] = AppState().getLanguageID(context);
try {
Position? position = await _getCurrentPositionSilently(context);
log("context position: $position");
if (position != null) {
postParams["WP_Geo_Location"] = "${position.latitude},${position.longitude}";
postParams["WP_Geo_Location"] =
"${position.latitude},${position.longitude}";
}
} catch (e) {
debugPrint("getWalaPlusRetailerById: unable to get current location: $e");
debugPrint(
"getWalaPlusRetailerById: unable to get current location: $e",
);
}
}
_cleanWalaPlusParams(postParams);
debugPrint("getWalaPlusRetailerById: calling $url with WP_RetailerID: $retailerId");
// Pass WP_User_Reference: if filled then user ID, else null value
if (pUserReference.isNotEmpty) {
postParams["WP_User_Reference"] = pUserReference;
} else {
postParams["WP_User_Reference"] = null;
}
debugPrint(
"getWalaPlusRetailerById: calling $url with params: $postParams",
);
return await ApiClient().postJsonForObject(
(response) {
if (response != null) {
debugPrint("getWalaPlusRetailerById response keys: ${response is Map ? response.keys.toList() : 'non-map'}");
dynamic val = response['WalaPlus_RetailersByID'] ?? response['walaPlus_RetailersByID'] ?? response['WalaPlusRetailersByID'] ?? response['WP_RetailersByID'] ?? response['result'];
debugPrint(
"getWalaPlusRetailerById response keys: ${response is Map ? response.keys.toList() : 'non-map'}",
);
dynamic val =
response['WalaPlus_RetailersByID'] ??
response['walaPlus_RetailersByID'] ??
response['WalaPlusRetailersByID'] ??
response['WP_RetailersByID'] ??
response['result'];
if (val is String) {
try {
val = json.decode(val);
} catch (e) {
debugPrint("Error decoding WalaPlus_RetailersByID JSON string: $e");
debugPrint(
"Error decoding WalaPlus_RetailersByID JSON string: $e",
);
}
}
WalaPlusRetailerModel? model;
@ -381,38 +573,197 @@ class OffersAndDiscountsApiClient {
if (item is Map<String, dynamic>) {
model = WalaPlusRetailerModel.fromJson(item);
} else if (item is Map) {
model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(item));
model = WalaPlusRetailerModel.fromJson(
Map<String, dynamic>.from(item),
);
}
} else if (val is Map) {
Map<String, dynamic> map = (val is Map<String, dynamic>) ? val : Map<String, dynamic>.from(val);
Map<String, dynamic> map =
(val is Map<String, dynamic>)
? val
: Map<String, dynamic>.from(val);
if (map.containsKey('retailer')) {
var byIdResponse = WalaPlusRetailersByIDResponse.fromJson(map);
model = byIdResponse.primaryRetailer ?? WalaPlusRetailerModel.fromJson(map);
model =
byIdResponse.primaryRetailer ??
WalaPlusRetailerModel.fromJson(map);
} else {
model = WalaPlusRetailerModel.fromJson(map);
}
if ((model?.offers == null || model!.offers!.isEmpty) && (map.containsKey('offers') || map.containsKey('Offers'))) {
model?.offers = WalaPlusRetailerModel.parseOffersList(map['offers'] ?? map['Offers']);
if (model?.isFavorite == null) {
if (map.containsKey('isfavorite')) {
model?.isFavorite =
map['isfavorite'] == true ||
map['isfavorite'] == 1 ||
map['isfavorite'] == 'true' ||
map['isfavorite'] == '1';
} else if (map.containsKey('is_favorite')) {
model?.isFavorite =
map['is_favorite'] == true ||
map['is_favorite'] == 1 ||
map['is_favorite'] == 'true' ||
map['is_favorite'] == '1';
}
}
if ((model?.offers == null || model!.offers!.isEmpty) &&
(map.containsKey('offers') || map.containsKey('Offers'))) {
model?.offers = WalaPlusRetailerModel.parseOffersList(
map['offers'] ?? map['Offers'],
);
}
if ((model?.locations == null || model!.locations!.isEmpty) &&
(map.containsKey('locations') || map.containsKey('Locations') || map.containsKey('location') || map.containsKey('Location'))) {
model?.locations = WalaPlusRetailerModel.parseLocationsList(map['locations'] ?? map['Locations'] ?? map['location'] ?? map['Location']);
(map.containsKey('locations') ||
map.containsKey('Locations') ||
map.containsKey('location') ||
map.containsKey('Location'))) {
model?.locations = WalaPlusRetailerModel.parseLocationsList(
map['locations'] ??
map['Locations'] ??
map['location'] ??
map['Location'],
);
}
if ((model?.website == null ||
model!.website!.isEmpty ||
model!.website == "null")) {
String? web =
(map['website'] ??
map['merchant_website'] ??
map['website_url'] ??
map['url'] ??
map['merchant_url'])
?.toString();
if (web != null && web.trim().isNotEmpty && web != "null") {
model?.website = web.trim();
}
}
if ((model?.categories == null || model!.categories!.isEmpty) &&
(map.containsKey('categories') ||
map.containsKey('Categories') ||
map.containsKey('category') ||
map.containsKey('Category'))) {
dynamic catData =
map['categories'] ??
map['Categories'] ??
map['category'] ??
map['Category'];
if (catData is List) {
model?.categories =
catData
.map(
(c) =>
c is Map
? WalaPlusCategoryModel.fromJson(
Map<String, dynamic>.from(c),
)
: WalaPlusCategoryModel(name: c.toString()),
)
.toList();
} else if (catData is Map) {
model?.categories = [
WalaPlusCategoryModel.fromJson(
Map<String, dynamic>.from(catData),
),
];
} else if (catData is String && catData.trim().isNotEmpty) {
model?.categories = [
WalaPlusCategoryModel(name: catData.trim()),
];
}
}
} else if (response is Map<String, dynamic> && response.containsKey('name')) {
} else if (response is Map<String, dynamic> &&
response.containsKey('name')) {
model = WalaPlusRetailerModel.fromJson(response);
}
if (model != null) {
if (response is Map) {
if ((model.offers == null || model.offers!.isEmpty) && (response.containsKey('offers') || response.containsKey('Offers'))) {
model.offers = WalaPlusRetailerModel.parseOffersList(response['offers'] ?? response['Offers']);
if (model.isFavorite == null) {
if (response.containsKey('isfavorite')) {
model.isFavorite =
response['isfavorite'] == true ||
response['isfavorite'] == 1 ||
response['isfavorite'] == 'true' ||
response['isfavorite'] == '1';
} else if (response.containsKey('is_favorite')) {
model.isFavorite =
response['is_favorite'] == true ||
response['is_favorite'] == 1 ||
response['is_favorite'] == 'true' ||
response['is_favorite'] == '1';
}
}
if (model.website == null ||
model.website!.isEmpty ||
model.website == "null") {
String? web =
(response['website'] ??
response['merchant_website'] ??
response['website_url'] ??
response['url'] ??
response['merchant_url'])
?.toString();
if (web != null && web.trim().isNotEmpty && web != "null") {
model.website = web.trim();
}
}
if ((model.offers == null || model.offers!.isEmpty) &&
(response.containsKey('offers') ||
response.containsKey('Offers'))) {
model.offers = WalaPlusRetailerModel.parseOffersList(
response['offers'] ?? response['Offers'],
);
}
if ((model.locations == null || model.locations!.isEmpty) &&
(response.containsKey('locations') || response.containsKey('Locations') || response.containsKey('location') || response.containsKey('Location'))) {
model.locations = WalaPlusRetailerModel.parseLocationsList(response['locations'] ?? response['Locations'] ?? response['location'] ?? response['Location']);
(response.containsKey('locations') ||
response.containsKey('Locations') ||
response.containsKey('location') ||
response.containsKey('Location'))) {
model.locations = WalaPlusRetailerModel.parseLocationsList(
response['locations'] ??
response['Locations'] ??
response['location'] ??
response['Location'],
);
}
if ((model.categories == null || model.categories!.isEmpty) &&
(response.containsKey('categories') ||
response.containsKey('Categories') ||
response.containsKey('category') ||
response.containsKey('Category'))) {
dynamic catData =
response['categories'] ??
response['Categories'] ??
response['category'] ??
response['Category'];
if (catData is List) {
model.categories =
catData
.map(
(c) =>
c is Map
? WalaPlusCategoryModel.fromJson(
Map<String, dynamic>.from(c),
)
: WalaPlusCategoryModel(name: c.toString()),
)
.toList();
} else if (catData is Map) {
model.categories = [
WalaPlusCategoryModel.fromJson(
Map<String, dynamic>.from(catData),
),
];
} else if (catData is String && catData.trim().isNotEmpty) {
model.categories = [
WalaPlusCategoryModel(name: catData.trim()),
];
}
}
}
debugPrint("getWalaPlusRetailerById: parsed retailer '${model.name}', offers: ${model.offers?.length ?? 0}, locations: ${model.locations?.length ?? 0}");
debugPrint(
"getWalaPlusRetailerById: parsed retailer '${model.name}', isFavorite: ${model.isFavorite}, offers: ${model.offers?.length ?? 0}, locations: ${model.locations?.length ?? 0}",
);
return model;
}
}
@ -423,10 +774,29 @@ class OffersAndDiscountsApiClient {
);
}
String resolveUserReference({String? overrideUserReference}) {
return _resolveUserReference(overrideUserReference: overrideUserReference);
}
String _resolveUserReference({String? overrideUserReference}) {
if (overrideUserReference != null && overrideUserReference.trim().isNotEmpty) {
if (overrideUserReference != null &&
overrideUserReference.trim().isNotEmpty) {
return overrideUserReference.trim();
}
// AppState().memberInformationList is never cleared on logout, so after logging out of an
// employee session and logging back in as a dependent (without a full app restart) it can
// still be sitting there from the previous session. Checking the CURRENT session type first
// - rather than just whichever field happens to be non-empty - prevents a dependent session
// from ever resolving to the previous employee's number.
if (ApiConsts.isDependetLogin) {
String? depRef = AppState().getUserName;
if (depRef != null && depRef.trim().isNotEmpty) {
return depRef.trim();
}
return "";
}
String? ref = AppState().memberInformationList?.eMPLOYEENUMBER?.toString();
if (ref != null && ref.trim().isNotEmpty) {
return ref.trim();
@ -438,15 +808,25 @@ class OffersAndDiscountsApiClient {
return "";
}
Future<Map<String, dynamic>?> redeemWalaPlusOffer({required String offerId, String? offerType, String? userReference, BuildContext? context}) async {
Future<Map<String, dynamic>?> redeemWalaPlusOffer({
required String offerId,
String? offerType,
String? userReference,
BuildContext? context,
}) async {
String url = ApiConsts.walaPlusOfferRedeem;
String pUserName = _resolveUserName();
String pUserReference = _resolveUserReference(overrideUserReference: userReference);
String pUserReference = _resolveUserReference(
overrideUserReference: userReference,
);
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["WP_User_Reference"] = pUserReference;
postParams["WP_Offer_Id"] = offerId;
postParams["WP_Offer_Type"] = (offerType != null && offerType.trim().isNotEmpty) ? offerType.trim() : "promo";
postParams["WP_Offer_Type"] =
(offerType != null && offerType.trim().isNotEmpty)
? offerType.trim()
: "promo";
postParams["UserName"] = pUserName;
postParams["P_USER_NAME"] = pUserName;
@ -468,7 +848,11 @@ class OffersAndDiscountsApiClient {
(response) {
if (response != null) {
debugPrint("redeemWalaPlusOffer response: $response");
dynamic val = response['WalaPlus_OfferRedeem'] ?? response['walaPlus_OfferRedeem'] ?? response['WalaPlusOfferRedeem'] ?? response['result'];
dynamic val =
response['WalaPlus_OfferRedeem'] ??
response['walaPlus_OfferRedeem'] ??
response['WalaPlusOfferRedeem'] ??
response['result'];
if (val is String) {
try {
val = json.decode(val);
@ -488,4 +872,138 @@ class OffersAndDiscountsApiClient {
postParams,
);
}
Future<GenericResponseModel?> addWalaPlusFavorite({
Object? retailerId,
List<String>? retailerIds,
BuildContext? context,
String? mobileNumber,
String? userReference,
}) async {
String url = ApiConsts.walaPlusFavoritesAdd;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
String pUserReference = _resolveUserReference(
overrideUserReference: userReference,
);
List<String> ids = [];
if (retailerIds != null && retailerIds.isNotEmpty) {
ids.addAll(retailerIds);
} else if (retailerId != null) {
if (retailerId is List) {
ids.addAll(retailerId.map((e) => e.toString()));
} else {
ids.add(retailerId.toString());
}
}
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["P_USER_NAME"] = pUserName;
postParams["UserName"] = pUserName;
if (pMobileNumber.isNotEmpty) {
postParams["MobileNumber"] = pMobileNumber;
}
if (pUserReference.isNotEmpty) {
postParams["WP_User_Reference"] = pUserReference;
}
postParams["WP_Retailer_Ids"] = ids;
if (context != null) {
postParams["LanguageID"] = AppState().getLanguageID(context);
}
if (ApiConsts.isDependetLogin) {
String pDepNationalId = _resolveDepNationalId();
if (pDepNationalId.isNotEmpty) {
postParams["P_DEP_NATIONAL_ID"] = pDepNationalId;
}
postParams["MemberType"] = 2;
} else {
postParams["MemberType"] = 1;
}
_cleanWalaPlusParams(postParams);
debugPrint("addWalaPlusFavorite calling $url with params: $postParams");
return await ApiClient().postJsonForObject(
(response) {
if (response != null) {
debugPrint("addWalaPlusFavorite response: $response");
return GenericResponseModel.fromJson(response);
}
return null;
},
url,
postParams,
);
}
Future<GenericResponseModel?> removeWalaPlusFavorite({
Object? retailerId,
List<String>? retailerIds,
BuildContext? context,
String? mobileNumber,
String? userReference,
}) async {
String url = ApiConsts.walaPlusFavoritesRemove;
String pUserName = _resolveUserName();
String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber);
String pUserReference = _resolveUserReference(
overrideUserReference: userReference,
);
List<String> ids = [];
if (retailerIds != null && retailerIds.isNotEmpty) {
ids.addAll(retailerIds);
} else if (retailerId != null) {
if (retailerId is List) {
ids.addAll(retailerId.map((e) => e.toString()));
} else {
ids.add(retailerId.toString());
}
}
Map<String, dynamic> postParams = _getBaseWalaPlusParams();
postParams["P_USER_NAME"] = pUserName;
postParams["UserName"] = pUserName;
if (pMobileNumber.isNotEmpty) {
postParams["MobileNumber"] = pMobileNumber;
}
if (pUserReference.isNotEmpty) {
postParams["WP_User_Reference"] = pUserReference;
}
postParams["WP_Retailer_Ids"] = ids;
if (context != null) {
postParams["LanguageID"] = AppState().getLanguageID(context);
}
if (ApiConsts.isDependetLogin) {
String pDepNationalId = _resolveDepNationalId();
if (pDepNationalId.isNotEmpty) {
postParams["P_DEP_NATIONAL_ID"] = pDepNationalId;
}
postParams["MemberType"] = 2;
} else {
postParams["MemberType"] = 1;
}
_cleanWalaPlusParams(postParams);
debugPrint("removeWalaPlusFavorite calling $url with params: $postParams");
return await ApiClient().postJsonForObject(
(response) {
if (response != null) {
debugPrint("removeWalaPlusFavorite response: $response");
return GenericResponseModel.fromJson(response);
}
return null;
},
url,
postParams,
);
}
}

@ -551,6 +551,7 @@ class ProfileApiClient {
if (personId != null) {
postParams["P_PERSON_ID"] = personId;
}
postParams["PatientIdentificationID"] = patientIdentificationId?.replaceAll('-', '').trim();
// Only the Grant call carries these - Revoke's payload stays exactly as it was before
// this feature. P_BIRTH_DATE/P_NHIC_VERIFIED_FLAG are always sent (even as null, if the
// source contact from GET_EMPLOYEE_CONTACTS didn't have a value) once we're granting.
@ -561,7 +562,7 @@ class ProfileApiClient {
if (patientIdentificationId != null && patientIdentificationId.trim().isNotEmpty) {
// National ID comes formatted with dashes (e.g. "2-5616-1545-7") - the backend wants
// the plain digit string (e.g. "2561615457").
postParams["PatientIdentificationID"] = patientIdentificationId.replaceAll('-', '').trim();
}
}
postParams.addAll(AppState().postParamsJson);

@ -7,6 +7,7 @@ import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:mohem_flutter_app/models/member_information_list_model.dart';
import 'package:mohem_flutter_app/models/member_login_list_model.dart';
import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:mohem_flutter_app/models/post_params_model.dart';
import 'package:mohem_flutter_app/models/privilege_list_model.dart';
import 'package:mohem_flutter_app/models/worklist_response_model.dart';
@ -91,7 +92,7 @@ class AppState {
String get getHuaweiPushToken => _huaweiPushToken;
final PostParamsModel _postParamsInitConfig = PostParamsModel(channel: 31, versionID: 10.1, mobileType: Platform.isAndroid ? "android" : "ios");
final PostParamsModel _postParamsInitConfig = PostParamsModel(channel: 31, versionID: 10.2, mobileType: Platform.isAndroid ? "android" : "ios");
PostParamsModel get postParamsInitConfig {
if (_postParamsInitConfig.pLegislationCode == null) {
@ -112,7 +113,7 @@ class AppState {
String? _dependentName;
String? get dependentName => _dependentName ?? _walaplusDepInfoModel?.pDependentName;
String? get dependentName => _dependentName;
set setDependentName(String? name) => _dependentName = name;
@ -134,6 +135,18 @@ class AppState {
set setAuthenticationTokenID(String? token) => _authenticationTokenID = token;
bool? _isEmpLastLoginToDevice;
bool? get isEmpLastLoginToDevice => _isEmpLastLoginToDevice;
set setIsEmpLastLoginToDevice(bool? val) => _isEmpLastLoginToDevice = val;
WalaPlusCityModel? _selectedMazayaCity;
WalaPlusCityModel? get selectedMazayaCity => _selectedMazayaCity;
set setSelectedMazayaCity(WalaPlusCityModel? city) => _selectedMazayaCity = city;
void setPostParamsInitConfig() {
isAuthenticated = false;
isLogged = false;
@ -144,6 +157,8 @@ class AppState {
_dependentLoginType = null;
_dependentDob = null;
_authenticationTokenID = null;
_isEmpLastLoginToDevice = null;
_selectedMazayaCity = null;
_postParamsInitConfig.pLegislationCode = null;
_postParams = _postParamsInitConfig;
}

@ -1,5 +1,5 @@
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
@ -18,20 +18,18 @@ class AppPermissions {
}
static void showErrorLocationDialog(bool isPermissionError, BuildContext context) {
showDialog(
context: context,
builder: (cxt) => ConfirmDialog(
message: "Please provide location permission",
onTap: () {
if (isPermissionError) {
Geolocator.openAppSettings();
} else {
Geolocator.openLocationSettings();
}
Navigator.pop(context);
// createVacationRule(list);
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.pleaseProvideLocationPermission.tr(),
confirmText: LocaleKeys.openSettings.tr(),
showCancelButton: false,
onConfirm: () {
if (isPermissionError) {
Geolocator.openAppSettings();
} else {
Geolocator.openLocationSettings();
}
},
);
}

@ -67,4 +67,5 @@ class MyColors {
static const Color lightGreyIconColor = Color(0xff919191);
static const Color selectedBorderColor = Color(0xff37A4BE);
static const Color mazayaRedColor = Color(0xffED1C2B);
static const Color hintTextColor = Color(0xff9AA4B2);
}

@ -4,21 +4,23 @@ class ApiConsts {
// static String baseUrl = "http://10.201.214.101:2027"; // Local server
// static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server
// static String baseUrl = "http://10.201.204.101:2024";
// static String baseUrl = "https://webservices.hmg.com"; // PreProd
// static String baseUrl = "https://hmgwebservices.com"; // Live server
static String baseUrl = "https://mohemm.hmg.com"; // New Live server
// static String baseUrl = "https://mohemm.hmg.com"; // New Live server
static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server
//
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver
// static String baseUrl = "http://10.201.214.101:2027/";
// static String baseUrl = "http://10.20.200.111:1010/";
// static String baseUrl = "https://mohemm.hmg.com"; // New Live server
static String baseUrl = "https://mohemm.hmg.com"; // New Live server
//static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT
static String eLearningBaseUrl = "https://elearning.hmg.com/moodle/api/"; // Elearning server live
static String enSATLiveURL = "https://ensat.hmg.com/auth/autologin"; // ENSAT server live
static String eLearningBaseUrl =
"https://elearning.hmg.com/moodle/api/"; // Elearning server live
static String enSATLiveURL =
"https://ensat.hmg.com/auth/autologin"; // ENSAT server live
// static String baseUrl = "http://10.20.200.111:1010/";
// static String baseUrl = "https://webservices.hmg.com"; // PreProd
@ -28,7 +30,8 @@ class ApiConsts {
static String baseUrlServices = baseUrl + "/Services/"; // server
// static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server
static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/";
static String authenticationRest = baseUrlServices + "Authentication.svc/REST/";
static String authenticationRest =
baseUrlServices + "Authentication.svc/REST/";
static String erpRest = baseUrlServices + "ERP.svc/REST/";
static String swpRest = baseUrlServices + "SWP.svc/REST/";
static String user = baseUrlServices + "api/User/";
@ -44,9 +47,13 @@ class ApiConsts {
static String walaPlusRetailers = erpRest + "WalaPlus_Retailers";
static String walaPlusRetailersByID = erpRest + "WalaPlus_RetailersByID";
static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem";
static String processContactMazayaAccess = erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS";
static String walaPlusFavoritesAdd = erpRest + "WalaPlus_FavoritesAdd";
static String walaPlusFavoritesRemove = erpRest + "WalaPlus_FavoritesRemove";
static String walaPlusCities = erpRest + "WalaPlus_Cities";
static String processContactMazayaAccess =
erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS";
static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect";
static String ssoAuthRedirection = "$ssoBaseUrl/api/auth/connect";
//Chat
static String chatServerBaseUrl = "https://apiderichat.hmg.com/";
@ -55,16 +62,24 @@ class ApiConsts {
static String chatHubConnectionUrl = chatServerBaseUrl + "ConnectionChatHub";
//Groups
static String getGroupByUserId = chatServerBaseApiUrl + "group/getgroupsbyuserid/";
static String deleteGroup = chatServerBaseApiUrl + "group/updateGroupIsDeleted/";
static String updateGroupAdmin = chatServerBaseApiUrl + "group/updateGroupAdmin/";
static String getGroupChatHistoryAsync = chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/";
static String addGroupsAndUsers = chatServerBaseApiUrl + "group/addgroupandusers/";
static String updateGroupsAndUsers = chatServerBaseApiUrl + "group/updategroupandusers/";
static String getGroupByUserId =
chatServerBaseApiUrl + "group/getgroupsbyuserid/";
static String deleteGroup =
chatServerBaseApiUrl + "group/updateGroupIsDeleted/";
static String updateGroupAdmin =
chatServerBaseApiUrl + "group/updateGroupAdmin/";
static String getGroupChatHistoryAsync =
chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/";
static String addGroupsAndUsers =
chatServerBaseApiUrl + "group/addgroupandusers/";
static String updateGroupsAndUsers =
chatServerBaseApiUrl + "group/updategroupandusers/";
// static String chatSearchMember = chatLoginTokenUrl + "user/";
static String chatRecentUrl = chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem
static String chatSingleUserHistoryUrl = chatServerBaseApiUrl + "UserChatHistory/";
static String chatRecentUrl =
chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem
static String chatSingleUserHistoryUrl =
chatServerBaseApiUrl + "UserChatHistory/";
static String chatMediaImageUploadUrl = chatServerBaseApiUrl + "shared/";
static String chatFavUser = chatServerBaseApiUrl + "FavUser/";
static String chatUserImages = chatServerBaseUrl + "empservice/api/employee/";
@ -74,28 +89,35 @@ class ApiConsts {
static String marathonBaseUrlUAT = "https://marathoon.com/uatservice/api/";
static String marathonBaseUrl = marathonBaseUrlLive;
// static String marathonBaseUrl = marathonBaseUrlUAT;
static String marathonBaseUrlServices = "https://marathoon.com/service/";
static String marathonParticipantLoginUrl = marathonBaseUrl + "auth/participantlogin";
static String marathonParticipantLoginUrl =
marathonBaseUrl + "auth/participantlogin";
static String marathonProjectGetUrl = marathonBaseUrl + "Project/Project_Get";
static String marathonUpcomingUrl = marathonBaseUrl + "marathon/upcoming/";
static String marathonJoinParticipantUrl = marathonBaseUrl + "participant/join";
static String marathonJoinParticipantUrl =
marathonBaseUrl + "participant/join";
static String marathonNextQuestionUrl = marathonBaseUrl + "question/next";
static String marathonSubmitAnswerUrl = marathonBaseUrl + "question/submit";
static String marathonQualifiersUrl = marathonBaseUrl + "winner/getWinner/";
static String marathonSelectedWinner = marathonBaseUrl + "winner/getSelectedWinner/";
static String marathonGetMarathonersCount = marathonBaseUrl + "Participant/GetRemainingParticipants";
static String marathonGetTutorial = marathonBaseUrl + "tutorial/GetTutorialNotification";
static String marathonSelectedWinner =
marathonBaseUrl + "winner/getSelectedWinner/";
static String marathonGetMarathonersCount =
marathonBaseUrl + "Participant/GetRemainingParticipants";
static String marathonGetTutorial =
marathonBaseUrl + "tutorial/GetTutorialNotification";
//DummyCards for the UI
static int tabletMinLength = 500;
// Notification Center GUIDs for OTP
static String notificationCenterAppGUID = "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0";
static String notificationCenterLoginServiceGUID = "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2";
static String notificationCenterForgotPasswordServiceGUID = "5CCCC2C9-EEA0-4207-9BE9-EE1FBBFFE8D0";
static String notificationCenterAppGUID =
"VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0";
static String notificationCenterLoginServiceGUID =
"D8BD5C76-4B50-4122-81D0-C3AA25DB08A2";
static String notificationCenterForgotPasswordServiceGUID =
"15AA0C70-8DA3-430B-8881-EAFBB508F296";
static MemberType selectedMemberType = MemberType.employee;

@ -22,7 +22,7 @@ class DateUtil {
return DateTime.now();
}
static DateTime convertStringToDate(String date) {
static DateTime convertStringToDate(String? date) {
// /Date(1585774800000+0300)/
if (date != null) {
const start = "/Date(";
@ -34,8 +34,9 @@ class DateUtil {
date.substring(startIndex + start.length, endIndex),
),
);
} else
} else {
return DateTime.now();
}
}
static DateTime convertSimpleStringDateToDate(String date, {bool isITG = false}) {

@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:google_api_availability/google_api_availability.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart';
@ -19,8 +18,11 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/provider/chat_provider_model.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/loading_dialog.dart';
import 'package:mohem_flutter_app/widgets/toast/app_toast.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/platform_tags.dart';
import 'package:nfc_manager/nfc_manager_android.dart';
import 'package:nfc_manager/nfc_manager_ios.dart';
// import 'package:nfc_manager/platform_tags.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart';
@ -31,15 +33,25 @@ class Utils {
static bool get isLoading => _isLoadingVisible;
static void showToast(String message, {bool longDuration = true}) {
Fluttertoast.showToast(
msg: message,
toastLength: longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black54,
textColor: Colors.white,
fontSize: 13.0,
// longDuration is accepted but no longer varies the duration - every caller already used the
// default (true), so AppToast.plainSeconds covers both cases with one fixed value.
// isSuccess: true -> soft green/green (same combo as dependent_details_screen.dart's "Active"
// badge); false (default) -> soft red/red (same combo as that screen's "Access Off" badge).
// Callers showing a value straight from an API response should pass isSuccess: response's
// MessageStatus == 1; everywhere else (validation messages, caught errors) just defaults to
// red, and the handful of hardcoded success confirmations pass isSuccess: true explicitly.
// onComplete makes this an action-toast (longer actionSeconds duration + icon shown) instead
// of a plain info toast - this is the single calling point, every caller should go through
// here instead of calling AppToast.show directly, so the color/duration/icon rules in one place.
static void showToast(String message, {bool longDuration = true, bool isSuccess = false, VoidCallback? onComplete}) {
AppToast.show(
message,
seconds: onComplete != null ? AppToast.actionSeconds : AppToast.plainSeconds,
showIcon: onComplete != null,
showCountdownText: true,
bgColor: isSuccess ? AppToast.successBgColor : AppToast.errorBgColor,
textColor: isSuccess ? AppToast.successTextColor : AppToast.errorTextColor,
onComplete: onComplete,
);
}
@ -113,6 +125,8 @@ class Utils {
lower.contains("grant request") ||
lower.contains("grant was found") ||
lower.contains("grant_request") ||
lower.contains("authorized to access") ||
lower.contains("not authorized") ||
lower.contains("e4:") ||
lower.contains("e4 -") ||
lower.contains("e4 :")) {
@ -122,7 +136,9 @@ class Utils {
str.contains("منح نشط") ||
str.contains("لا يوجد طلب منح") ||
str.contains("تم إلغاء وصول مزايا") ||
str.contains("إلغاء مزايا")) {
str.contains("إلغاء مزايا") ||
str.contains("غير مصرح لك بالوصول") ||
str.contains("غير مصرح")) {
return true;
}
return false;
@ -135,15 +151,7 @@ class Utils {
depName = AppState().walaplusDepInfoModel?.pDependentName ?? "";
}
if (cxt != null) {
Navigator.push(
cxt,
MaterialPageRoute(
builder: (context) => MazayaAccessResultScreen(
isGranted: false,
dependentName: depName,
),
),
);
Navigator.push(cxt, MaterialPageRoute(builder: (context) => MazayaAccessResultScreen(isGranted: false, dependentName: depName)));
return;
}
}
@ -165,17 +173,11 @@ class Utils {
onErrorMessage(errorMessage);
} else {
if (!AppState().isAuthenticated) {
showDialog(
barrierDismissible: false,
context: cxt,
builder:
(cxt) => ConfirmDialog(
message: errorMessage,
onTap: () {
Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route<dynamic> route) => false);
},
onCloseTap: () {},
),
showToast(
errorMessage,
onComplete: () {
Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route<dynamic> route) => false);
},
);
} else {
if (cxt != null) {
@ -193,7 +195,15 @@ class Utils {
}
static void confirmDialog(cxt, String message, {VoidCallback? onTap}) {
showDialog(context: cxt, builder: (BuildContext cxt) => ConfirmDialog(message: message, onTap: onTap));
// No onTap means there's nothing to confirm - it's a plain notice, so a self-dismissing
// toast replaces the old "OK just closes" dialog instead of requiring a tap. Every current
// caller of this path passes an error/warning message (catch blocks, location/fake-location
// warnings, session errors) - never success - so red is correct here.
if (onTap == null) {
showToast(message);
} else {
showDialog(context: cxt, builder: (BuildContext cxt) => ConfirmDialog(message: message, onTap: onTap));
}
}
static Widget getNoDataWidget(BuildContext context) {
@ -402,15 +412,34 @@ class Utils {
NfcManager.instance
.startSession(
onDiscovered: (NfcTag tag) async {
MifareUltralight f;
// MifareUltralight f;
// if (Platform.isAndroid) {
// print(tag);
// f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22);
// } else {
// f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22);
// }
Uint8List? id;
if (Platform.isAndroid) {
print(tag);
f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22);
} else {
f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22);
id = NfcTagAndroid.from(tag)?.id;
} else if (Platform.isIOS) {
id = MiFareIos.from(tag)?.identifier;
}
String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join('');
NfcManager.instance.stopSession();
if (id == null) {
await NfcManager.instance.stopSession();
return;
}
String identifier =
id.map((e) => e.toRadixString(16).padLeft(2, '0')).join('');
print(identifier);
await NfcManager.instance.stopSession();
// String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join('');
// NfcManager.instance.stopSession();
onRead(identifier);
},
pollingOptions: {NfcPollingOption.iso14443},
@ -448,9 +477,7 @@ class Utils {
if (chatData != null) {
try {
chatData.disposeData();
} catch (e) {
}
} catch (e) {}
}
// SharedPreferences prefs = await SharedPreferences.getInstance();
// await prefs.clear();

@ -13,9 +13,8 @@ import 'package:mohem_flutter_app/ui/chat/manage_group.dart';
import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_create_request.dart';
import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_ovr_home.dart';
import 'package:mohem_flutter_app/ui/etqan_ovr/etqan_request_detailed.dart';
import 'package:mohem_flutter_app/ui/chatbot/chatbot_screen.dart';
import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart';
import 'package:mohem_flutter_app/ui/landing/dashboard_screen_for_dependet.dart';
import 'package:mohem_flutter_app/ui/landing/wala_plus_retailer_details_screen.dart';
import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart';
import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart';
import 'package:mohem_flutter_app/ui/landing/itg/survey_screen.dart';
@ -33,10 +32,15 @@ import 'package:mohem_flutter_app/ui/marathon/marathon_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_sponsor_video_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_tutorial_viewer_screen.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_waiting_screen.dart';
import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart';
import 'package:mohem_flutter_app/ui/mazaya/mazaya_discover_screen.dart';
import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart';
import 'package:mohem_flutter_app/ui/mazaya/retailer_details_screen.dart';
import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart';
import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart';
// import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart';
import 'package:mohem_flutter_app/ui/my_team/create_request.dart';
import 'package:mohem_flutter_app/ui/my_team/employee_details.dart';
@ -46,6 +50,7 @@ import 'package:mohem_flutter_app/ui/my_team/subordinate_leave.dart';
import 'package:mohem_flutter_app/ui/my_team/team_members.dart';
import 'package:mohem_flutter_app/ui/my_team/view_attendance.dart';
import 'package:mohem_flutter_app/ui/parking_qr/parking_qr.dart';
import 'package:mohem_flutter_app/ui/parking_qr/parking_qr.dart';
import 'package:mohem_flutter_app/ui/payslip/monthly_pay_slip_screen.dart';
import 'package:mohem_flutter_app/ui/performance/performance_appraisal_screen.dart';
import 'package:mohem_flutter_app/ui/profile/add_dependent_verify_screen.dart';
@ -64,6 +69,7 @@ import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details.
import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart';
import 'package:mohem_flutter_app/ui/screens/child_education/child_education_assistance.dart';
import 'package:mohem_flutter_app/ui/screens/courses/courses_screen.dart';
// import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart';
import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart';
import 'package:mohem_flutter_app/ui/screens/event_activity/event_activity.dart';
@ -103,8 +109,10 @@ class AppRoutes {
static const String newPassword = "/newPassword";
static const String loginVerification = "/loginVerification";
static const String dashboard = "/dashboard";
static const String dashboardScreenForDependet = "/dashboardScreenForDependet";
static const String walaPlusRetailerDetails = "/walaPlusRetailerDetails";
static const String retailersListScreen = "/dashboardScreenForDependet";
static const String retailerDetailsScreen = "/walaPlusRetailerDetails";
static const String mazayaDiscover = "/mazayaDiscover";
static const String savedReyailersScreen = "/mazayaSaved";
static const String todayAttendance = "/todayAttendance";
static const String subMenuScreen = "/submenuScreen";
static const String addEitScreen = "/addeitScreen";
@ -210,6 +218,7 @@ class AppRoutes {
static const String chat = "/chat";
static const String chatDetailed = "/chatDetailed";
static const String chatFavoriteUsers = "/chatFavoriteUsers";
static const String chatBotHome = "/chatBotHome";
//Group Chat
static const String manageGroup = "/manageGroup";
@ -243,15 +252,14 @@ class AppRoutes {
verifyLogin: (BuildContext context) => VerifyLoginScreen(),
verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(),
dashboard: (BuildContext context) => DashboardScreen(),
dashboardScreenForDependet: (BuildContext context) => const DashboardScreenForDependet(),
walaPlusRetailerDetails: (BuildContext context) => const WalaPlusRetailerDetailsScreen(),
retailersListScreen: (BuildContext context) => const RetailersListScreen(),
retailerDetailsScreen: (BuildContext context) => const RetailerDetailsScreen(),
mazayaDiscover: (BuildContext context) => const MazayaDiscoverScreen(),
savedReyailersScreen: (BuildContext context) => const SavedRetailersScreen(),
appInAppWebView: (BuildContext context) {
Object? args = ModalRoute.of(context)?.settings.arguments;
if (args is Map<String, dynamic>) {
return AppInAppWebViewScreen(
url: args['url'] ?? "",
title: args['title'],
);
return AppInAppWebViewScreen(url: args['url'] ?? "", title: args['title']);
}
return const Scaffold();
},
@ -262,8 +270,8 @@ class AppRoutes {
newPassword: (BuildContext context) => NewPasswordScreen(),
forgotPassword: (BuildContext context) => ForgotPasswordScreen(),
todayAttendance: (BuildContext context) => TodayAttendanceScreen2(),
//eit
//eit
addEitScreen: (BuildContext context) => AddEITScreen(),
//Work List
@ -274,7 +282,6 @@ class AppRoutes {
worklistSettings: (BuildContext context) => WorklistSettings(),
// Leave Balance
leaveBalance: (BuildContext context) => LeaveBalance(),
addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(),
@ -357,6 +364,7 @@ class AppRoutes {
chat: (BuildContext context) => ChatHome(),
chatDetailed: (BuildContext context) => ChatDetailScreen(),
chatFavoriteUsers: (BuildContext context) => ChatFavoriteUsersScreen(),
chatBotHome: (BuildContext context) => ChatBotScreen(),
//Group Chat
manageGroup: (BuildContext context) => ManageGroupScreen(),
@ -373,7 +381,7 @@ class AppRoutes {
unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(),
appUpdateScreen: (BuildContext context) => const AppUpdateScreen(),
childEducation: (BuildContext context) => ChildEducationAssistance(),
activityScreen:(BuildContext context) => const EventActivityScreen(),
activityScreen: (BuildContext context) => const EventActivityScreen(),
//ETQAN OVR
etqanOvr: (BuildContext context) => EtqanOvrHome(),
@ -383,5 +391,6 @@ class AppRoutes {
//Parking QR
parkingQr: (BuildContext context) => const ParkingQrScreen(),
activityScreen: (BuildContext context) => const EventActivityScreen(),
};
}

@ -82,7 +82,17 @@ extension EmailValidator on String {
),
);
Widget toText12({Color? color, FontWeight? weight, bool isUnderLine = false, Color? underlineColor, double underlineGap = 2, bool isBold = false, bool isCenter = false, int? maxLine, TextOverflow? overflow}) {
Widget toText12({
Color? color,
FontWeight? weight,
bool isUnderLine = false,
Color? underlineColor,
double underlineGap = 2,
bool isBold = false,
bool isCenter = false,
int? maxLine,
TextOverflow? overflow,
}) {
Widget text = Text(
this,
textAlign: isCenter ? TextAlign.center : null,

File diff suppressed because it is too large Load Diff

@ -770,6 +770,7 @@ abstract class LocaleKeys {
static const showAllLocations = 'showAllLocations';
static const searchBranches = 'searchBranches';
static const allCities = 'allCities';
static const allCitiesOption = 'allCitiesOption';
static const noBranchesFound = 'noBranchesFound';
static const browseAllBranches = 'browseAllBranches';
static const branchesCount = 'branchesCount';
@ -787,5 +788,88 @@ abstract class LocaleKeys {
static const nationalIdAndDob = 'nationalIdAndDob';
static const sponsorOtp = 'sponsorOtp';
static const revokingPreventsAccessDesc = 'revokingPreventsAccessDesc';
}
static const locationPermissionRequiredForAttendance = 'locationPermissionRequiredForAttendance';
static const locationServicesRequiredForAttendance = 'locationServicesRequiredForAttendance';
static const pleaseProvideLocationPermission = 'pleaseProvideLocationPermission';
static const microphoneAccessRequired = 'microphoneAccessRequired';
static const shareAccessGuide = 'shareAccessGuide';
static const nationalIdOrIqamaNumber = 'nationalIdOrIqamaNumber';
static const multipleCities = 'multipleCities';
static const filters = 'filters';
static const availability = 'availability';
static const chooseOneOrBoth = 'chooseOneOrBoth';
static const sortBy = 'sortBy';
static const recommended = 'recommended';
static const nearest = 'nearest';
static const highestDiscount = 'highestDiscount';
static const newest = 'newest';
static const distance = 'distance';
static const fromYourCurrentLocation = 'fromYourCurrentLocation';
static const any = 'any';
static const minimumDiscount = 'minimumDiscount';
static const reset = 'reset';
static const applyFilters = 'applyFilters';
static const distanceKm = 'distanceKm';
static const discountPercentPlus = 'discountPercentPlus';
static const visitMerchantWebsite = 'visitMerchantWebsite';
static const favoriteAddedSuccess = 'favoriteAddedSuccess';
static const favoriteRemovedSuccess = 'favoriteRemovedSuccess';
static const favoriteError = 'favoriteError';
static const merchantWebsiteNotAvailable = 'merchantWebsiteNotAvailable';
static const locationServicesRequiredForNearestOffers = 'locationServicesRequiredForNearestOffers';
static const locationPermissionRequiredForNearestOffers = 'locationPermissionRequiredForNearestOffers';
static const mazayaDeals = 'mazayaDeals';
static const helloName = 'helloName';
static const findABrandOrSomething = 'findABrandOrSomething';
static const exploreYourBenefits = 'exploreYourBenefits';
static const theMazayaEdit = 'theMazayaEdit';
static const exclusivelyYours = 'exclusivelyYours';
static const aLittleExtraJustForMazayaMembers = 'aLittleExtraJustForMazayaMembers';
static const seeAll = 'seeAll';
static const latestOnMazaya = 'latestOnMazaya';
static const goodFoodGreatCompanyEvenBetterPerks = 'goodFoodGreatCompanyEvenBetterPerks';
static const travelEssentials = 'travelEssentials';
static const fromEverydayCareToYourNextAdventure = 'fromEverydayCareToYourNextAdventure';
static const homeAndKitchen = 'homeAndKitchen';
static const curatedPicksForYourHomeAndKitchen = 'curatedPicksForYourHomeAndKitchen';
static const discover = 'discover';
static const saved = 'saved';
static const myFamily = 'myFamily';
static const educationAndTraining = 'educationAndTraining';
static const foodsAndDrinks = 'foodsAndDrinks';
static const travelAndEntertainment = 'travelAndEntertainment';
static const beautyAndFitness = 'beautyAndFitness';
static const healthcare = 'healthcare';
static const favoriteMerchant = 'favoriteMerchant';
static const favoriteOffers = 'favoriteOffers';
static const goodToKnow = 'goodToKnow';
static const goodToKnowDesc = 'goodToKnowDesc';
static const termsAndConditions = 'termsAndConditions';
static const haveAQuestionAboutOffer = 'haveAQuestionAboutOffer';
static const getHelp = 'getHelp';
static const redeemOffer = 'redeemOffer';
static const merchantContact = 'merchantContact';
static const website = 'website';
static const phone = 'phone';
static const viewAll = 'viewAll';
static const moreCitiesCount = 'moreCitiesCount';
static const findAnEscapeOrAnExperience = 'findAnEscapeOrAnExperience';
static const allOffers = 'allOffers';
static const sampleAvailability = 'sampleAvailability';
static const offersCount = 'offersCount';
static const mazayaExclusive = 'mazayaExclusive';
static const youAreAllCaughtUp = 'youAreAllCaughtUp';
static const selectCity = 'selectCity';
static const searchCities = 'searchCities';
static const chatBot = 'chatBot';
static const helloAskMe = 'helloAskMe';
static const anything = 'anything';
static const thinking = 'thinking';
static const waitingForResponse = 'waitingForResponse';
static const typeAMessage = 'typeAMessage';
static const initializingChatPleaseWait = 'initializingChatPleaseWait';
static const am = 'am';
static const pm = 'pm';
static const carServices = 'carServices';
}

@ -8,7 +8,6 @@
import 'package:audio_session/audio_session_web.dart';
import 'package:camera_web/camera_web.dart';
import 'package:file_picker/_internal/file_picker_web.dart';
import 'package:firebase_core_web/firebase_core_web.dart';
import 'package:firebase_messaging_web/firebase_messaging_web.dart';
import 'package:fluttertoast/fluttertoast_web.dart';
@ -27,7 +26,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart';
void registerPlugins(Registrar registrar) {
AudioSessionWeb.registerWith(registrar);
CameraPlugin.registerWith(registrar);
FilePickerWeb.registerWith(registrar);
FirebaseCoreWeb.registerWith(registrar);
FirebaseMessagingWeb.registerWith(registrar);
FluttertoastWebPlugin.registerWith(registrar);

@ -19,6 +19,8 @@ import 'package:month_year_picker/month_year_picker.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'package:sizer/sizer.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
// test uat account
// username 199067
@ -77,32 +79,33 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Sizer(
builder: (BuildContext context, Orientation orientation, DeviceType deviceType) {
builder: (BuildContext context, Orientation orientation, ScreenType deviceType){
PostParamsModel? obj = AppState().postParamsObject;
obj?.languageID = EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2;
AppState().setPostParamsModel(obj!);
List<LocalizationsDelegate<dynamic>> delegates = context.localizationDelegates;
// delegates.add(GlobalMaterialLocalizations.delegate);
delegates.add(MonthYearPickerLocalizations.delegate);
return SafeArea(
top: false, // Set to true if you want to avoid the notch area as well
bottom: Platform.isIOS ? false : true,
child: MaterialApp(
navigatorKey: AppRoutes.navigatorKey,
builder: (BuildContext context, Widget? child) {
return MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), child: child!);
},
theme: AppTheme.getTheme(EasyLocalization.of(context)?.locale.languageCode == "ar"),
debugShowCheckedModeBanner: false,
localizationsDelegates: delegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
initialRoute: AppRoutes.initialRoute,
routes: AppRoutes.routes,
),
);
},
obj?.languageID = EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2;
AppState().setPostParamsModel(obj!);
List<LocalizationsDelegate<dynamic>> delegates = context.localizationDelegates;
// delegates.add(GlobalMaterialLocalizations.delegate);
delegates.add(MonthYearPickerLocalizations.delegate);
return SafeArea(
top: false, // Set to true if you want to avoid the notch area as well
bottom: Platform.isIOS ? false : true,
child: MaterialApp(
navigatorKey: AppRoutes.navigatorKey,
builder: (BuildContext context, Widget? child) {
return MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), child: child!);
},
theme: AppTheme.getTheme(EasyLocalization.of(context)?.locale.languageCode == "ar"),
debugShowCheckedModeBanner: false,
localizationsDelegates: delegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
initialRoute: AppRoutes.initialRoute,
routes: AppRoutes.routes,
),
);
}
);
}
}

@ -400,14 +400,18 @@ class GenericResponseModel {
bool? isSMSSent;
PortalDirectionData? portalDirectionData;
String? Mohemm_ENSAT_GetItgToken_Response;
AtlasInitiateChatResponse? atlasInitiateChatResponse;
AtlasContinueChatResponseDetails? atlasContinueChatResponseDetails;
GetWalaplusDepInfoModel? getWalaplusDepInfoModel;
dynamic getWalaplusDepInfoModelInternal;
List<WalaPlusCategoryModel>? walaPlusCategories;
String? wpFavoritesAddRemoveMessage;
String? businessCardQROutside;
EmpBusinessCardInfoModel? empBusinessCardInfoModel;
bool? isEmpLastLoginToDevice;
GenericResponseModel({
this.date,
this.languageID,
@ -683,8 +687,13 @@ class GenericResponseModel {
this.getWalaplusDepInfoModel,
this.getWalaplusDepInfoModelInternal,
this.walaPlusCategories,
this.wpFavoritesAddRemoveMessage,
this.businessCardQROutside,
this.empBusinessCardInfoModel,
this.isEmpLastLoginToDevice,
this.atlasInitiateChatResponse,
this.atlasContinueChatResponseDetails,
});
GenericResponseModel.fromJson(Map<String, dynamic> json) {
@ -1478,6 +1487,10 @@ class GenericResponseModel {
ePharmacyGetItemOnHandList = json['ePharmacy_GetItemOnHandList'];
isActiveCode = json['isActiveCode'];
isSMSSent = json['isSMSSent'];
atlasInitiateChatResponse = json["Atlas_InitiateChat_Response"] == null ? null : AtlasInitiateChatResponse.fromJson(json["Atlas_InitiateChat_Response"]);
atlasContinueChatResponseDetails = json["Atlas_ContinueChat_ResponseDetails"] == null ? null : AtlasContinueChatResponseDetails.fromJson(json["Atlas_ContinueChat_ResponseDetails"]);
Mohemm_ENSAT_GetItgToken_Response = json['Mohemm_ENSAT_GetItgToken_Response'];
getWalaplusDepInfoModel = json['GET_WALAPLUS_DEP_INFO_Model'] != null ? GetWalaplusDepInfoModel.fromJson(json['GET_WALAPLUS_DEP_INFO_Model']) : null;
@ -1492,6 +1505,8 @@ class GenericResponseModel {
walaPlusCategories!.add(WalaPlusCategoryModel.fromJson(v));
});
}
wpFavoritesAddRemoveMessage = json['WP_Favorites_AddRemove_Message'] ?? json['wp_Favorites_AddRemove_Message'];
isEmpLastLoginToDevice = json["IsEmpLastLoginToDevice"];
}
Map<String, dynamic> toJson() {
@ -1977,6 +1992,9 @@ class GenericResponseModel {
data['ePharmacy_GetItemOnHandList'] = this.ePharmacyGetItemOnHandList;
data['isActiveCode'] = this.isActiveCode;
data['isSMSSent'] = this.isSMSSent;
data["Atlas_InitiateChat_Response"] = atlasInitiateChatResponse?.toJson();
data["Atlas_ContinueChat_ResponseDetails"] = atlasContinueChatResponseDetails?.toJson();
data['Mohemm_ENSAT_GetItgToken_Response'] = this.Mohemm_ENSAT_GetItgToken_Response;
data['GET_WALAPLUS_DEP_INFO_Model_Internal'] = this.getWalaplusDepInfoModelInternal;
@ -1992,7 +2010,10 @@ class GenericResponseModel {
if (walaPlusCategories != null) {
data['WalaPlus_Categories'] = walaPlusCategories!.map((WalaPlusCategoryModel v) => v.toJson()).toList();
}
if (this.wpFavoritesAddRemoveMessage != null) {
data['WP_Favorites_AddRemove_Message'] = this.wpFavoritesAddRemoveMessage;
}
data["IsEmpLastLoginToDevice"] = this.isEmpLastLoginToDevice;
return data;
}
}
@ -2062,3 +2083,138 @@ class EmpBusinessCardInfoModel {
"P_WORK_EXTENSION_NUMBER": pWorkExtensionNumber,
};
}
class AtlasInitiateChatResponse {
String? atlasAccessToken;
String? atlasChatSessionId;
String? atlasTokenType;
AtlasInitiateChatResponse({this.atlasAccessToken, this.atlasChatSessionId, this.atlasTokenType});
factory AtlasInitiateChatResponse.fromRawJson(String str) => AtlasInitiateChatResponse.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory AtlasInitiateChatResponse.fromJson(Map<String, dynamic> json) =>
AtlasInitiateChatResponse(atlasAccessToken: json["Atlas_AccessToken"], atlasChatSessionId: json["Atlas_ChatSessionID"], atlasTokenType: json["Atlas_TokenType"]);
Map<String, dynamic> toJson() => {"Atlas_AccessToken": atlasAccessToken, "Atlas_ChatSessionID": atlasChatSessionId, "Atlas_TokenType": atlasTokenType};
}
class AtlasContinueChatResponseDetails {
AiResponse? aiResponse;
QueryClassification? queryClassification;
String? sessionId;
AiResponse? userMessage;
AtlasContinueChatResponseDetails({
this.aiResponse,
this.queryClassification,
this.sessionId,
this.userMessage,
});
factory AtlasContinueChatResponseDetails.fromRawJson(String str) => AtlasContinueChatResponseDetails.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory AtlasContinueChatResponseDetails.fromJson(Map<String, dynamic> json) => AtlasContinueChatResponseDetails(
aiResponse: json["ai_response"] == null ? null : AiResponse.fromJson(json["ai_response"]),
queryClassification: json["query_classification"] == null ? null : QueryClassification.fromJson(json["query_classification"]),
sessionId: json["session_id"],
userMessage: json["user_message"] == null ? null : AiResponse.fromJson(json["user_message"]),
);
Map<String, dynamic> toJson() => {
"ai_response": aiResponse?.toJson(),
"query_classification": queryClassification?.toJson(),
"session_id": sessionId,
"user_message": userMessage?.toJson(),
};
}
class AiResponse {
DateTime? createdAt;
String? id;
StructuredData? structuredData;
String? text;
AiResponse({
this.createdAt,
this.id,
this.structuredData,
this.text,
});
factory AiResponse.fromRawJson(String str) => AiResponse.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory AiResponse.fromJson(Map<String, dynamic> json) => AiResponse(
createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]),
id: json["id"],
structuredData: json["structured_data"] == null ? null : StructuredData.fromJson(json["structured_data"]),
text: json["text"],
);
Map<String, dynamic> toJson() => {
"created_at": createdAt?.toIso8601String(),
"id": id,
"structured_data": structuredData?.toJson(),
"text": text,
};
}
class StructuredData {
String? answer;
List<dynamic>? sources;
String? type;
StructuredData({
this.answer,
this.sources,
this.type,
});
factory StructuredData.fromRawJson(String str) => StructuredData.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory StructuredData.fromJson(Map<String, dynamic> json) => StructuredData(
answer: json["answer"],
sources: json["sources"] == null ? [] : List<dynamic>.from(json["sources"]!.map((x) => x)),
type: json["type"],
);
Map<String, dynamic> toJson() => {
"answer": answer,
"sources": sources == null ? [] : List<dynamic>.from(sources!.map((x) => x)),
"type": type,
};
}
class QueryClassification {
int? confidence;
String? type;
QueryClassification({
this.confidence,
this.type,
});
factory QueryClassification.fromRawJson(String str) => QueryClassification.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory QueryClassification.fromJson(Map<String, dynamic> json) => QueryClassification(
confidence: json["confidence"],
type: json["type"],
);
Map<String, dynamic> toJson() => {
"confidence": confidence,
"type": type,
};
}

@ -5,12 +5,17 @@ class WalaPlusCategoryModel {
String? name;
String? parentId;
String? countryNameAr;
String? countryNameEn;
WalaPlusCategoryModel({
this.id,
this.image,
this.imageThumb,
this.name,
this.parentId,
this.countryNameAr,
this.countryNameEn,
}) {
if ((image == null || image!.isEmpty) && id != null && id!.trim().isNotEmpty && id != "all" && id != "0") {
String cleanId = id!.trim();
@ -23,9 +28,11 @@ class WalaPlusCategoryModel {
WalaPlusCategoryModel.fromJson(Map<String, dynamic> json) {
id = (json['id'] ?? json['Id'] ?? json['ID'])?.toString();
name = (json['name'] ?? json['Name'])?.toString();
name = (json['name'] ?? json['Name'] ?? json['category_name'] ?? json['categoryName'] ?? json['cat_name'] ?? json['CategoryName'])?.toString();
parentId = (json['parent_id'] ?? json['Parent_Id'] ?? json['parentId'])?.toString();
imageThumb = (json['image_thumb'] ?? json['Image_Thumb'] ?? json['imageThumb'])?.toString();
countryNameAr = (json['country_name_ar'] ?? json['countryNameAr'] ?? json['category_name_ar'] ?? json['categoryNameAr'] ?? json['cat_name_ar'] ?? json['name_ar'])?.toString();
countryNameEn = (json['country_name_en'] ?? json['countryNameEn'] ?? json['category_name_en'] ?? json['categoryNameEn'] ?? json['cat_name_en'] ?? json['name_en'])?.toString();
// Use category image URL from HMG middleware using ID from WalaPlus_Categories API:
// https://epharmacy.hmg.com/middleware/images/category/{id}.png
@ -47,6 +54,8 @@ class WalaPlusCategoryModel {
data['image_thumb'] = imageThumb;
data['name'] = name;
data['parent_id'] = parentId;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
return data;
}

@ -0,0 +1,51 @@
class WalaPlusCityModel {
int? cityId;
String? cityNameAr;
String? cityNameEn;
String? countryNameAr;
String? countryNameEn;
WalaPlusCityModel({
this.cityId,
this.cityNameAr,
this.cityNameEn,
this.countryNameAr,
this.countryNameEn,
});
factory WalaPlusCityModel.fromJson(Map<String, dynamic> json) {
return WalaPlusCityModel(
cityId:
json['city_id'] is int
? json['city_id']
: int.tryParse(json['city_id']?.toString() ?? ''),
cityNameAr: json['city_name_ar']?.toString(),
cityNameEn: json['city_name_en']?.toString(),
countryNameAr: json['country_name_ar']?.toString(),
countryNameEn: json['country_name_en']?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'city_id': cityId,
'city_name_ar': cityNameAr,
'city_name_en': cityNameEn,
'country_name_ar': countryNameAr,
'country_name_en': countryNameEn,
};
}
String getDisplayName(bool isAr) {
String? primary = isAr ? cityNameAr : cityNameEn;
if (primary != null && primary.trim().isNotEmpty) return primary.trim();
String? fallback = isAr ? cityNameEn : cityNameAr;
return fallback?.trim() ?? '';
}
bool get isAllCities {
String en = (cityNameEn ?? '').trim().toLowerCase();
String ar = (cityNameAr ?? '').trim();
return en == 'all cities' || ar == 'جميع المدن';
}
}

@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
class WalaPlusRetailersResponse {
@ -9,8 +9,10 @@ class WalaPlusRetailersResponse {
int? pagesCount;
int? perPage;
List<WalaPlusRetailerModel>? retailers;
String? countryNameAr;
String? countryNameEn;
WalaPlusRetailersResponse({this.page, this.pagesCount, this.perPage, this.retailers});
WalaPlusRetailersResponse({this.page, this.pagesCount, this.perPage, this.retailers, this.countryNameAr, this.countryNameEn});
factory WalaPlusRetailersResponse.fromJson(Map<String, dynamic> json) {
List<WalaPlusRetailerModel> retailersList = [];
@ -35,6 +37,8 @@ class WalaPlusRetailersResponse {
pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? json['totalPages']?.toString() ?? '1'),
perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? json['WP_Per_page']?.toString() ?? '10'),
retailers: retailersList,
countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar'] ?? json['countryNameAr'] ?? json['country_ar']),
countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en'] ?? json['countryNameEn'] ?? json['country_en']),
);
}
@ -43,6 +47,8 @@ class WalaPlusRetailersResponse {
data['page'] = page;
data['pages_count'] = pagesCount;
data['per_page'] = perPage;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
if (retailers != null) {
data['retailers'] = retailers!.map((v) => v.toJson()).toList();
}
@ -63,6 +69,23 @@ class WalaPlusRetailersByIDResponse {
factory WalaPlusRetailersByIDResponse.fromJson(Map<String, dynamic> json) {
List<WalaPlusRetailerLocation> locList = WalaPlusRetailerModel.parseLocationsList(json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']);
List<WalaPlusOfferModel> offList = WalaPlusRetailerModel.parseOffersList(json['offers'] ?? json['Offers']);
List<WalaPlusCategoryModel> catList = [];
dynamic catData = json['categories'] ?? json['Categories'] ?? json['category'] ?? json['Category'];
if (catData is List) {
for (var c in catData) {
if (c is Map<String, dynamic>) {
catList.add(WalaPlusCategoryModel.fromJson(c));
} else if (c is Map) {
catList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(c)));
} else if (c is String && c.trim().isNotEmpty) {
catList.add(WalaPlusCategoryModel(name: c.trim()));
}
}
} else if (catData is Map) {
catList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(catData)));
} else if (catData is String && catData.trim().isNotEmpty) {
catList.add(WalaPlusCategoryModel(name: catData.trim()));
}
List<WalaPlusRetailerModel> retList = [];
dynamic retData = json['retailer'] ?? json['Retailer'];
@ -72,11 +95,27 @@ class WalaPlusRetailersByIDResponse {
var model = WalaPlusRetailerModel.fromJson(r);
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
} else if (r is Map) {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(r));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
}
}
@ -84,6 +123,14 @@ class WalaPlusRetailersByIDResponse {
var model = WalaPlusRetailerModel.fromJson(Map<String, dynamic>.from(retData));
if (model.locations == null || model.locations!.isEmpty) model.locations = locList;
if (model.offers == null || model.offers!.isEmpty) model.offers = offList;
if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList;
if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) {
model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString();
}
if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) {
var favVal = json['isfavorite'] ?? json['is_favorite'];
model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1';
}
retList.add(model);
}
@ -148,6 +195,8 @@ class WalaPlusRetailerModel {
List<WalaPlusOfferModel>? offers;
List<WalaPlusRetailerLocation>? locations;
List<WalaPlusCategoryModel>? categories;
String? categoryId;
String? categoryName;
String? validity;
String? validTo;
String? validUntil;
@ -156,6 +205,14 @@ class WalaPlusRetailerModel {
String? duration;
String? email;
String? phone;
String? countryNameAr;
String? countryNameEn;
bool? isFavorite;
// Drives the "MAZAYA EXCLUSIVE" badge on the retailer card (Figma node 927:496). NOT parsed
// from the WalaPlus_Retailers/WalaPlus_RetailersByID API - this is a different, separate
// "Exclusive" API/section, so callers set this manually on the model after fetching from there.
bool? isExclusive;
WalaPlusRetailerModel({
this.id,
@ -169,6 +226,8 @@ class WalaPlusRetailerModel {
this.logo,
this.logoThumb,
this.website,
this.isFavorite,
this.isExclusive,
this.type,
this.usage,
this.weight,
@ -183,6 +242,8 @@ class WalaPlusRetailerModel {
this.offers,
this.locations,
this.categories,
this.categoryId,
this.categoryName,
this.validity,
this.validTo,
this.validUntil,
@ -191,6 +252,8 @@ class WalaPlusRetailerModel {
this.duration,
this.email,
this.phone,
this.countryNameAr,
this.countryNameEn,
});
static List<WalaPlusOfferModel> parseOffersList(data) {
@ -265,10 +328,82 @@ class WalaPlusRetailerModel {
List<WalaPlusRetailerLocation> locationsList = parseLocationsList(locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']);
List<WalaPlusCategoryModel> categoriesList = [];
if (targetJson['categories'] != null && targetJson['categories'] is List) {
targetJson['categories'].forEach((v) {
categoriesList.add(WalaPlusCategoryModel.fromJson(v));
});
dynamic rawCats = targetJson['categories'] ?? targetJson['Categories'] ?? targetJson['category'] ?? targetJson['Category'];
if (rawCats != null && rawCats is List) {
for (var v in rawCats) {
if (v is Map<String, dynamic>) {
categoriesList.add(WalaPlusCategoryModel.fromJson(v));
} else if (v is Map) {
categoriesList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(v)));
} else if (v is String && v.trim().isNotEmpty) {
categoriesList.add(WalaPlusCategoryModel(name: v.trim()));
}
}
} else if (rawCats is Map) {
categoriesList.add(WalaPlusCategoryModel.fromJson(Map<String, dynamic>.from(rawCats)));
} else if (rawCats is String && rawCats.toString().trim().isNotEmpty && rawCats.toString().trim() != "null") {
categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim()));
}
String? singleCatName =
(targetJson['category_name'] ??
targetJson['categoryName'] ??
targetJson['cat_name'] ??
targetJson['catName'] ??
targetJson['CategoryName'] ??
targetJson['Category_Name'] ??
targetJson['category_title'] ??
targetJson['categoryTitle'])
?.toString()
.trim();
String? singleCatAr =
(targetJson['category_name_ar'] ??
targetJson['categoryNameAr'] ??
targetJson['cat_name_ar'] ??
targetJson['catNameAr'] ??
targetJson['category_ar'] ??
targetJson['country_name_ar'] ??
targetJson['countryNameAr'])
?.toString()
.trim();
String? singleCatEn =
(targetJson['category_name_en'] ??
targetJson['categoryNameEn'] ??
targetJson['cat_name_en'] ??
targetJson['catNameEn'] ??
targetJson['category_en'] ??
targetJson['country_name_en'] ??
targetJson['countryNameEn'])
?.toString()
.trim();
String? foundCategoryId =
(targetJson['category_id'] ?? targetJson['categoryId'] ?? targetJson['cat_id'] ?? targetJson['catId'] ?? targetJson['WP_CategoryID'] ?? targetJson['WP_CategoryId'])?.toString().trim();
String? rawType = targetJson['type']?.toString().trim();
String? lowerType = rawType?.toLowerCase();
bool isTypeStoreKeyword =
lowerType == null ||
lowerType.isEmpty ||
lowerType == 'online' ||
lowerType == 'instore' ||
lowerType == 'in_store' ||
lowerType == 'in store' ||
lowerType == 'store' ||
lowerType == 'online,instore' ||
lowerType == 'instore,online';
if ((singleCatName == null || singleCatName.isEmpty) && !isTypeStoreKeyword && rawType != null && rawType.isNotEmpty) {
singleCatName = rawType;
}
if (categoriesList.isEmpty) {
if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") ||
(singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") ||
(singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) {
categoriesList.add(
WalaPlusCategoryModel(id: foundCategoryId, name: singleCatName ?? singleCatEn ?? singleCatAr, countryNameAr: singleCatAr ?? singleCatName, countryNameEn: singleCatEn ?? singleCatName),
);
}
}
WalaPlusRetailerContact? contactObj;
@ -304,6 +439,51 @@ class WalaPlusRetailerModel {
}
}
String? foundWebsite =
targetJson['website']?.toString() ??
targetJson['merchant_website']?.toString() ??
targetJson['merchantWebsite']?.toString() ??
targetJson['website_url']?.toString() ??
targetJson['websiteUrl']?.toString() ??
targetJson['web_site']?.toString() ??
targetJson['url']?.toString() ??
targetJson['merchant_url']?.toString() ??
targetJson['merchantUrl']?.toString() ??
targetJson['merchantLink']?.toString() ??
targetJson['merchant_link']?.toString() ??
targetJson['link']?.toString() ??
contactObj?.website;
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
if (targetJson['merchant'] is Map) {
var m = targetJson['merchant'];
foundWebsite = (m['website'] ?? m['merchant_website'] ?? m['url'] ?? m['link'])?.toString();
}
}
if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") {
for (var entry in targetJson.entries) {
String k = entry.key.toLowerCase();
if ((k.contains('website') || k.contains('web_site') || k == 'url' || k.contains('merchant_url')) && !k.contains('image') && !k.contains('logo')) {
if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") {
foundWebsite = entry.value.toString().trim();
break;
}
}
}
}
bool? parsedIsFavorite;
if (targetJson.containsKey('isfavorite')) {
parsedIsFavorite = targetJson['isfavorite'] == true || targetJson['isfavorite'] == 1 || targetJson['isfavorite'] == 'true' || targetJson['isfavorite'] == '1';
} else if (targetJson.containsKey('is_favorite')) {
parsedIsFavorite = targetJson['is_favorite'] == true || targetJson['is_favorite'] == 1 || targetJson['is_favorite'] == 'true' || targetJson['is_favorite'] == '1';
} else if (targetJson.containsKey('isFavorite')) {
parsedIsFavorite = targetJson['isFavorite'] == true || targetJson['isFavorite'] == 1 || targetJson['isFavorite'] == 'true' || targetJson['isFavorite'] == '1';
} else if (targetJson.containsKey('favorite')) {
parsedIsFavorite = targetJson['favorite'] == true || targetJson['favorite'] == 1 || targetJson['favorite'] == 'true' || targetJson['favorite'] == '1';
}
return WalaPlusRetailerModel(
id:
targetJson['id']?.toString() ??
@ -320,7 +500,8 @@ class WalaPlusRetailerModel {
imageThumb: targetJson['image_thumb']?.toString(),
logo: targetJson['logo']?.toString(),
logoThumb: targetJson['logo_thumb']?.toString(),
website: targetJson['website']?.toString(),
website: foundWebsite,
isFavorite: parsedIsFavorite,
type: targetJson['type']?.toString(),
usage: targetJson['usage'] is String ? decodeUnicodeString(targetJson['usage']) : targetJson['usage'],
weight: targetJson['weight'],
@ -335,6 +516,8 @@ class WalaPlusRetailerModel {
offers: offersList,
locations: locationsList,
categories: categoriesList,
categoryId: foundCategoryId,
categoryName: singleCatName ?? singleCatEn ?? singleCatAr,
validity: foundValidity,
validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(),
validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(),
@ -343,9 +526,59 @@ class WalaPlusRetailerModel {
duration: targetJson['duration']?.toString(),
email: targetJson['email']?.toString() ?? targetJson['contact_email']?.toString() ?? contactObj?.primaryEmail,
phone: targetJson['phone']?.toString() ?? targetJson['contact_phone']?.toString() ?? targetJson['mobile']?.toString() ?? contactObj?.primaryPhone,
countryNameAr: decodeUnicodeString(targetJson['country_name_ar'] ?? targetJson['countryNameAr'] ?? targetJson['country_ar']),
countryNameEn: decodeUnicodeString(targetJson['country_name_en'] ?? targetJson['countryNameEn'] ?? targetJson['country_en']),
);
}
// Adapter for the Mazaya "Exclusive Offers" section, which reuses the legacy Offers &
// Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) rather than a
// WalaPlus endpoint. Maps whatever overlaps onto this model so the existing grid/card/detail
// screen can render it unchanged - fields with no equivalent on OffersListModel (favorite,
// redeem, locations, contact, ...) are simply left null, and isExclusive hides the actions
// that depend on them (see WalaPlusRetailerCard, RetailerDetailsScreen).
factory WalaPlusRetailerModel.fromExclusiveOffer(OffersListModel offer, {bool isAr = false}) {
String? name = decodeUnicodeString(isAr ? (offer.titleAr ?? offer.titleEn) : (offer.titleEn ?? offer.titleAr));
String? description = decodeUnicodeString(isAr ? (offer.descriptionAr ?? offer.descriptionEn) : (offer.descriptionEn ?? offer.descriptionAr));
String? categoryName = decodeUnicodeString(isAr ? (offer.categoryNameAr ?? offer.categoryNameEn) : (offer.categoryNameEn ?? offer.categoryNameAr));
String? discountText = _formatExclusiveDiscountText(decodeUnicodeString(offer.discountDescription) ?? decodeUnicodeString(offer.discount));
return WalaPlusRetailerModel(
id: "exclusive-${offer.offersDiscountId ?? ''}",
name: name,
// WalaPlusRetailerCard reads offerTitle as the promo subtitle shown under the name (e.g.
// "20% off") - the discount text is the closest equivalent on a legacy offer.
offerTitle: discountText,
description: description,
info: description,
image: offer.bannerImage,
logo: offer.logo,
website: offer.websiteUrl,
endDate: offer.endDate,
validTo: offer.endDate,
categoryName: categoryName,
categories: categoryName != null ? <WalaPlusCategoryModel>[WalaPlusCategoryModel(name: categoryName)] : null,
isExclusive: true,
);
}
// The legacy Offers & Discounts API sometimes returns a bare number ("10", "20") for its
// discount fields rather than a formatted "10% off" string. WalaPlusRetailerCard's badge
// logic (getDiscountBadge) only recognizes a "<digits>%" pattern, so a bare number would
// otherwise render verbatim instead of as a discount. Already-formatted text (containing "%",
// "off", or "خصم") is left untouched.
static String? _formatExclusiveDiscountText(String? raw) {
if (raw == null) return null;
String trimmed = raw.trim();
if (trimmed.isEmpty) return null;
if (trimmed.contains('%') || trimmed.toLowerCase().contains('off') || trimmed.contains('خصم')) {
return trimmed;
}
if (RegExp(r'^\d+(\.\d+)?$').hasMatch(trimmed)) {
return LocaleKeys.offDiscount.tr(args: ['$trimmed%']);
}
return trimmed;
}
static String? decodeUnicodeString(Object? val) {
if (val == null) return null;
String s = val.toString().trim();
@ -424,6 +657,11 @@ class WalaPlusRetailerModel {
data['duration'] = duration;
data['email'] = email;
data['phone'] = phone;
data['country_name_ar'] = countryNameAr;
data['country_name_en'] = countryNameEn;
data['is_favorite'] = isFavorite;
data['isfavorite'] = isFavorite;
data['is_exclusive'] = isExclusive;
if (offers != null) {
data['offers'] = offers!.map((v) => v.toJson()).toList();
}
@ -433,9 +671,50 @@ class WalaPlusRetailerModel {
if (categories != null) {
data['categories'] = categories!.map((v) => v.toJson()).toList();
}
data['category_id'] = categoryId;
data['category_name'] = categoryName;
return data;
}
String? getDisplayCategory({bool isAr = false, List<WalaPlusCategoryModel>? fallbackCategories}) {
if (categories != null && categories!.isNotEmpty) {
for (var cat in categories!) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
String? resolved =
(primary != null && primary.trim().isNotEmpty && primary != "null") ? primary.trim() : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
}
}
if (categoryName != null && categoryName!.trim().isNotEmpty && categoryName != "null") {
return categoryName!.trim();
}
if (fallbackCategories != null && categoryId != null && categoryId!.trim().isNotEmpty) {
for (var cat in fallbackCategories) {
if (cat.id?.toString() == categoryId?.toString()) {
String? primary = isAr ? cat.countryNameAr : cat.countryNameEn;
String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr);
String? resolved =
(primary != null && primary.trim().isNotEmpty && primary != "null")
? primary.trim()
: (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null);
if (resolved != null && resolved.isNotEmpty) {
return resolved;
}
}
}
}
if (type != null && type!.trim().isNotEmpty) {
String lt = type!.trim().toLowerCase();
if (lt != 'online' && lt != 'instore' && lt != 'store' && lt != 'in_store') {
return type!.trim();
}
}
return null;
}
String? get displayImage {
if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim();
if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim();
@ -449,12 +728,12 @@ class WalaPlusRetailerModel {
}
String? get resolvedWebsite {
if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") {
return contact!.website!.trim();
}
if (website != null && website!.trim().isNotEmpty && website != "null") {
return website!.trim();
}
if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") {
return contact!.website!.trim();
}
return null;
}
@ -471,32 +750,39 @@ class WalaPlusRetailerModel {
return false;
}
String getDisplayCity({bool isAr = false}) {
if (locations != null && locations!.isNotEmpty) {
// 1. First pass: look for specific city name
for (var loc in locations!) {
String? cName = isAr ? loc.cityNameAr : loc.cityNameEn;
if (cName != null && cName.trim().isNotEmpty && cName != "null") {
return cName.trim();
}
}
// 2. Second pass: fallback to city if not generic country
for (var loc in locations!) {
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
String c = loc.city!.trim();
if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') {
return c;
}
}
}
// 3. Third pass: any non-empty city
for (var loc in locations!) {
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
return loc.city!.trim();
}
// Resolves a single location's city using the same 3-pass priority this used to apply
// globally: specific city name first, then a non-generic-country city, then any city at all.
String? _resolveLocationCity(WalaPlusRetailerLocation loc, bool isAr) {
String? cName = isAr ? loc.cityNameAr : loc.cityNameEn;
if (cName != null && cName.trim().isNotEmpty && cName != "null") {
return cName.trim();
}
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
String c = loc.city!.trim();
if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') {
return c;
}
}
return '';
if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") {
return loc.city!.trim();
}
return null;
}
// Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of
// just picking the first one found, so the badge doesn't silently hide the other branches.
String getDisplayCity({bool isAr = false}) {
if (locations == null || locations!.isEmpty) return '';
Set<String> distinctCities = <String>{};
for (var loc in locations!) {
String? resolved = _resolveLocationCity(loc, isAr);
if (resolved != null) distinctCities.add(resolved);
}
if (distinctCities.isEmpty) return '';
if (distinctCities.length > 1) return LocaleKeys.multipleCities.tr();
return distinctCities.first;
}
String get displayCity => getDisplayCity(isAr: false);
@ -582,37 +868,63 @@ class WalaPlusRetailerModel {
return null;
}
bool get isOnlineStore => (type?.trim().toLowerCase() == 'online');
bool get isInstore =>
(type?.trim().toLowerCase() == 'instore' ||
type?.trim().toLowerCase() == 'in_store' ||
type?.trim().toLowerCase() == 'in store' ||
type?.trim().toLowerCase() == 'store' ||
(type == null && locations != null && locations!.isNotEmpty));
String? get normalizedType {
// type can be a comma-separated combo (e.g. "online,instore") when a retailer offers both -
// this splits and normalizes every part instead of only ever matching a single exact value,
// so a combined retailer is correctly recognized as both rather than neither/just one.
List<String> get normalizedTypes {
Set<String> result = <String>{};
if (type != null && type!.trim().isNotEmpty) {
String t = type!.trim().toLowerCase();
if (t == 'online') return 'online';
if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') return 'instore';
return t;
for (String part in type!.split(',')) {
String t = part.trim().toLowerCase();
if (t.isEmpty) continue;
if (t == 'online') {
result.add('online');
} else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') {
result.add('instore');
} else {
// Non-online types (e.g. category names like "Physiotherapy") represent
// in-store merchants/services that require physical presence.
result.add('instore');
}
}
}
if (locations != null && locations!.isNotEmpty) {
return 'instore';
result.add('instore');
}
return null;
if (result.isEmpty && hasPhysicalLocation) {
result.add('instore');
}
return result.toList();
}
String? getDisplayType() {
String? norm = normalizedType;
bool get isOnlineStore => normalizedTypes.contains('online');
bool get isInstore => normalizedTypes.contains('instore');
// Kept for any existing single-value callers - returns the first normalized type found.
String? get normalizedType {
List<String> types = normalizedTypes;
return types.isEmpty ? null : types.first;
}
String _displayTypeLabel(String norm) {
if (norm == 'online') return LocaleKeys.online.tr();
if (norm == 'instore') return LocaleKeys.inStore.tr();
if (type != null && type!.trim().isNotEmpty) {
return type!.trim();
}
return LocaleKeys.inStore.tr();
}
String? getDisplayType() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return _displayTypeLabel(types.first);
return null;
}
// One label per distinct type (e.g. ["Online", "In Store"]) - used to render a separate chip
// for each, instead of collapsing a dual-type retailer down to a single badge.
List<String> getDisplayTypes() {
List<String> types = normalizedTypes;
if (types.isNotEmpty) return types.map(_displayTypeLabel).toList();
return <String>[];
}
}
class WalaPlusRetailerContact {

@ -7,20 +7,23 @@ class PrivilegeListModel {
int? iD;
String? serviceName;
bool? previlege;
bool? previlegeHMG;
PrivilegeListModel({this.iD, this.serviceName, this.previlege});
PrivilegeListModel({this.iD, this.serviceName, this.previlege, this.previlegeHMG});
PrivilegeListModel.fromJson(Map<String, dynamic> json) {
iD = json['ID'];
serviceName = json['ServiceName'];
previlege = json['Previlege'];
previlege = json['Previlege'] ?? json['previlege'] ?? json['Privilege'] ?? json['privilege'];
previlegeHMG = json['Previlege_HMG'] ?? json['PrevilegeHMG'] ?? json['previlege_HMG'] ?? json['previlegeHMG'] ?? json['Privilege_HMG'] ?? json['PrivilegeHMG'];
}
Map<String, dynamic> toJson() {
Map<String, dynamic> data = new Map<String, dynamic>();
data['ID'] = this.iD;
data['ServiceName'] = this.serviceName;
data['Previlege'] = this.previlege;
Map<String, dynamic> data = <String, dynamic>{};
data['ID'] = iD;
data['ServiceName'] = serviceName;
data['Previlege'] = previlege;
data['Previlege_HMG'] = previlegeHMG;
return data;
}

@ -16,6 +16,8 @@ import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/classes/encryption.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/main.dart';
import 'package:mohem_flutter_app/models/chat/chat_user_image_model.dart';
@ -1616,13 +1618,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin {
await File('$dirPath/.nomedia').create();
}
path = "$dirPath/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac";
recorderController = RecorderController()
..androidEncoder = AndroidEncoder.aac
..androidOutputFormat = AndroidOutputFormat.mpeg4
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
..sampleRate = 6000
..updateFrequency = const Duration(milliseconds: 100)
..bitRate = 18000;
recorderController = RecorderController();
// ..androidEncoder = AndroidEncoder.aac
// ..androidOutputFormat = AndroidOutputFormat.mpeg4
// ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
// ..sampleRate = 6000
// ..updateFrequency = const Duration(milliseconds: 100)
// ..bitRate = 18000;
playerController = PlayerController();
}
@ -1638,20 +1640,22 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin {
void startRecoding(BuildContext context) async {
await Permission.microphone.request().then((PermissionStatus status) {
if (status.isPermanentlyDenied) {
Utils.confirmDialog(
ConfirmBottomSheet.show(
context,
"The app needs microphone access to be able to record audio.",
onTap: () {
Navigator.of(context).pop();
message: LocaleKeys.microphoneAccessRequired.tr(),
confirmText: LocaleKeys.openSettings.tr(),
showCancelButton: false,
onConfirm: () {
openAppSettings();
},
);
} else if (status.isDenied) {
Utils.confirmDialog(
ConfirmBottomSheet.show(
context,
"The app needs microphone access to be able to record audio.",
onTap: () {
Navigator.of(context).pop();
message: LocaleKeys.microphoneAccessRequired.tr(),
confirmText: LocaleKeys.openSettings.tr(),
showCancelButton: false,
onConfirm: () {
openAppSettings();
},
);

@ -27,7 +27,6 @@ import 'package:mohem_flutter_app/models/greetings/greeting_card_model.dart';
import 'package:mohem_flutter_app/models/itg/itg_response_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart';
import 'package:mohem_flutter_app/models/sso_auth_model.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin
@ -190,15 +189,11 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners();
Utils.handleException(ex, context, (err) {
Utils.hideLoading(context);
showDialog(
context: context,
builder:
(cxt) => ConfirmDialog(
message: err,
onTap: () {
Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route<dynamic> route) => false);
},
),
Utils.showToast(
err,
onComplete: () {
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route<dynamic> route) => false);
},
);
});
}
@ -230,9 +225,9 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners();
} catch (ex) {
isLeaveTicketBalanceLoading = false;
logger.wtf(ex);
notifyListeners();
Utils.handleException(ex, context, null);
// logger.wtf(ex);
// notifyListeners();
// Utils.handleException(ex, context, null);
}
}

@ -24,7 +24,7 @@ import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/search_employee_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/dynamic_forms/dynamic_textfield_widget.dart';
import 'package:mohem_flutter_app/widgets/item_detail_view_widget.dart';
@ -216,7 +216,7 @@ class _AddVacationRuleScreenState extends State<AddVacationRuleScreen> {
CreateVacationRuleList? createVacationRuleList = await VacationRuleApiClient().createVacationRule(DateUtil.convertDateToStringLocation(startTime), DateUtil.convertDateToStringLocation(endTime!),
selectedItemType!.iTEMTYPE!, selectedItemTypeNotification!.nOTIFICATIONNAME!, message, getPAction(), selectedReplacementEmployee!.userName!, respondAttributeList);
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.vacationRuleAdded.tr());
Utils.showToast(LocaleKeys.vacationRuleAdded.tr(), isSuccess: true);
Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard));
} catch (ex) {
Utils.hideLoading(context);
@ -490,15 +490,14 @@ class _AddVacationRuleScreenState extends State<AddVacationRuleScreen> {
}
}
showDialog(
context: context,
builder: (cxt) => ConfirmDialog(
message: LocaleKeys.areYouSureYouWantToSubmit.tr(),
onTap: () {
Navigator.pop(context);
createVacationRule(list);
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.areYouSureYouWantToSubmit.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
createVacationRule(list);
},
);
},
).insideContainer,

@ -27,14 +27,14 @@ class ChatHome extends StatefulWidget {
class _ChatHomeState extends State<ChatHome> {
int tabIndex = 0;
PageController controller = PageController();
late ChatProviderModel data;
late ChatProviderModel data;
@override
void initState() {
super.initState();
data = Provider.of<ChatProviderModel>(context, listen: false);
if (chatHubConnection?.state == HubConnectionState.Connected) {
data = Provider.of<ChatProviderModel>(context, listen: false);
data.registerEvents();
}
}

@ -19,6 +19,7 @@ import 'package:mohem_flutter_app/ui/chat/create_group.dart';
import 'package:mohem_flutter_app/ui/chat/group_chat_detaied_screen.dart';
import 'package:mohem_flutter_app/ui/chat/manage_group.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/search_employee_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart';
import 'package:provider/provider.dart';
@ -269,11 +270,13 @@ void goToSelected(GroupResponse? groupDetails, ChatProviderModel m, String value
void deleteGroup(
GroupResponse? groupDetails, ChatProviderModel m, BuildContext context) {
groupDetails!.groupUserList;
Utils.confirmDialog(
ConfirmBottomSheet.show(
context,
LocaleKeys.areYouSureWantTodelete.tr(),
onTap: () {
Navigator.pop(context);
message: LocaleKeys.areYouSureWantTodelete.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
isDestructive: true,
onConfirm: () {
m.deleteGroup(groupDetails);
},
);

@ -0,0 +1,499 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mohem_flutter_app/api/dashboard_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/extensions/widget_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
class MessagesHolder{
static List<ChatMessage> messages = [];
}
enum ChatMode { home, chatActive }
class ChatMessage {
final String text;
final bool isUser;
final DateTime timestamp;
ChatMessage({required this.text, required this.isUser, required this.timestamp});
}
class ChatBotScreen extends StatefulWidget {
const ChatBotScreen({Key? key}) : super(key: key);
@override
State<ChatBotScreen> createState() => _ChatBotScreenState();
}
class _ChatBotScreenState extends State<ChatBotScreen> {
ChatMode _currentMode = ChatMode.home;
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
AtlasInitiateChatResponse? atlasChatTokenResponse;
bool _isThinking = false;
// Speech to text
final SpeechToText _speechToText = SpeechToText();
bool _speechEnabled = false;
@override
void initState() {
// TODO: implement initState
super.initState();
getChatSession();
_initSpeech();
}
/// Initialize speech to text
void _initSpeech() async {
_speechEnabled = await _speechToText.initialize();
if(MessagesHolder.messages.isNotEmpty){
setState(() {
_messages.addAll(MessagesHolder.messages);
_currentMode = ChatMode.chatActive;
});
}
setState(() {});
}
/// Start listening for speech
void _startListening() async {
await _speechToText.listen(
onResult: _onSpeechResult,
localeId: AppState().isArabic(context) ? 'ar_SA' : 'en_US',
);
setState(() {});
}
/// Stop listening for speech
void _stopListening() async {
await _speechToText.stop();
setState(() {});
}
/// Callback when speech is recognized
void _onSpeechResult(SpeechRecognitionResult result) {
setState(() {
_messageController.text = result.recognizedWords;
// Move cursor to end of text
_messageController.selection = TextSelection.fromPosition(TextPosition(offset: _messageController.text.length));
});
}
void getChatSession() async {
try {
GenericResponseModel? res = await DashboardApiClient().getChatBotSession();
if (res != null) {
setState(() {
atlasChatTokenResponse = res.atlasInitiateChatResponse;
});
}
} catch (ex) {
Utils.handleException(ex, context, null);
}
}
@override
void dispose() {
atlasChatTokenResponse = null;
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
void _sendMessage({String? suggestionText}) async {
String text = suggestionText ?? _messageController.text.trim();
if (text.isEmpty || atlasChatTokenResponse == null) return;
try {
setState(() {
_messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now()));
_currentMode = ChatMode.chatActive;
_messageController.clear();
_isThinking = true;
});
_scrollToBottom();
GenericResponseModel? res = await DashboardApiClient().sendChatBotMessage(atlasChatTokenResponse: atlasChatTokenResponse!, atlasChatText: text);
if (res != null) {
res.atlasContinueChatResponseDetails;
if (mounted) {
setState(() {
_isThinking = false;
_messages.add(
ChatMessage(
text: res.atlasContinueChatResponseDetails!.aiResponse!.structuredData!.answer ?? "",
isUser: false,
timestamp: res.atlasContinueChatResponseDetails!.aiResponse!.createdAt ?? DateTime.now(),
),
);
});
}
_scrollToBottom();
}
} catch (ex) {
setState(() {
_isThinking = false;
});
Utils.handleException(ex, context, null);
}
_scrollToBottom();
}
void _scrollToBottom() {
Future<void>.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
});
}
void _sendSuggestion(String text) {
if (atlasChatTokenResponse == null) {
Utils.showToast(LocaleKeys.initializingChatPleaseWait.tr());
return;
}
_sendMessage(suggestionText: text);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyColors.backgroundColor,
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(LocaleKeys.chatBot.tr(), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))),
backgroundColor: MyColors.backgroundColor,
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.minimize, color: Color(0xFF2B353E)),
onPressed: () {
MessagesHolder.messages = _messages;
Navigator.of(context).pop();
},
),
IconButton(
icon: const Icon(Icons.close, color: Color(0xFF2B353E)),
onPressed: () {
MessagesHolder.messages.clear();
Navigator.of(context).pop();
},
),
],
),
body: Column(children: <Widget>[Expanded(child: _currentMode == ChatMode.home ? _buildHomeMode() : _buildChatMode()), _buildBottomInputBar()]),
);
}
Widget _buildHomeMode() {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(LocaleKeys.helloAskMe.tr(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))),
Text(LocaleKeys.anything.tr(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))),
const SizedBox(height: 50),
const Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 12,
children: <Widget>[
// _buildSuggestionChip("Nostalgia Perfume", () => _sendSuggestion("Nostalgia Perfume")),
// _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")),
// _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")),
// _buildSuggestionChip("Al Jadi", () => _sendSuggestion("Al Jadi")),
],
),
],
),
),
);
}
Widget _buildChatMode() {
return ListView.separated(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
itemCount: _messages.length + (_isThinking ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
if (index == _messages.length && _isThinking) {
return _buildThinkingIndicator();
}
ChatMessage message = _messages[index];
return _buildMessageBubble(message);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(height: 15);
},
);
}
Widget _buildThinkingIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: const LinearGradient(colors: <Color>[Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight),
borderRadius: BorderRadius.circular(15),
boxShadow: <BoxShadow>[BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)],
),
child: const ThinkingDotsAnimation(),
),
const Spacer(flex: 2),
],
);
}
Widget _buildMessageBubble(ChatMessage message) {
bool isUser = message.isUser;
Widget bubbleContent = Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (isUser) const Spacer(flex: 2),
Flexible(
flex: 7,
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isUser ? Colors.white : null,
gradient: isUser ? null : const LinearGradient(colors: <Color>[Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight),
borderRadius: BorderRadius.circular(15),
boxShadow: isUser ? null : <BoxShadow>[BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(message.text, softWrap: true, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: isUser ? const Color(0xFF2B353E) : MyColors.white)),
Align(
alignment: Alignment.centerRight,
child: Text(
_formatTimestamp(message.timestamp),
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: -0.4, color: isUser ? const Color(0xFF2B353E) : MyColors.whiteColor),
),
),
],
),
),
],
),
),
if (!isUser) const Spacer(flex: 2),
],
);
if (!isUser) {
return AnimatedMessageBubble(child: bubbleContent);
}
return bubbleContent;
}
String _formatTimestamp(DateTime timestamp) {
int hourInt = timestamp.hour;
int minute = timestamp.minute;
String period = hourInt >= 12 ? LocaleKeys.pm.tr() : LocaleKeys.am.tr();
int formattedHour = hourInt % 12 == 0 ? 12 : hourInt % 12;
String hour = formattedHour.toString().padLeft(2, '0');
String min = minute.toString().padLeft(2, '0');
String month = timestamp.month.toString().padLeft(2, '0');
String day = timestamp.day.toString().padLeft(2, '0');
int year = timestamp.year;
return "$month/$day/$year $hour:$min $period";
}
Widget _buildSuggestionChip(String label, VoidCallback onTap) {
return Material(
color: Colors.white,
elevation: 3,
shadowColor: const Color(0xFF000015).withValues(alpha: 0.0784),
borderRadius: BorderRadius.circular(10),
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: 0.2)),
),
),
);
}
Widget _buildBottomInputBar() {
return Container(
decoration: const BoxDecoration(color: MyColors.backgroundColor),
child: SafeArea(
top: false,
child: Row(
children: <Widget>[
// Plus button
// IconButton(padding: EdgeInsets.zero, icon: const Icon(Icons.add, color: MyColors.darkTextColor, size: 34), onPressed: () {}),
// const SizedBox(width: 12),
// Text field
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
height: 48,
decoration: BoxDecoration(color: Colors.transparent, borderRadius: BorderRadius.circular(10), border: BoxBorder.all(width: 1, color: const Color(0xFFE5E5E5))),
child: Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _messageController,
onSubmitted: _isThinking ? null : (_) => _sendMessage(),
enabled: !_isThinking,
maxLines: 1,
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
hintText: _isThinking ? LocaleKeys.waitingForResponse.tr() : LocaleKeys.typeAMessage.tr(),
hintStyle: const TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
GestureDetector(
onTap: _isThinking ? null : (_speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null),
child: SvgPicture.asset(
"assets/icons/microphone.svg",
colorFilter: ColorFilter.mode(_isThinking ? MyColors.hintTextColor : (_speechToText.isListening ? Colors.red : MyColors.darkTextColor), BlendMode.srcIn),
),
),
],
),
),
),
const SizedBox(width: 6),
// Send button
GestureDetector(
onTap: _isThinking ? null : _sendMessage,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(color: _isThinking ? MyColors.hintTextColor.withValues(alpha: 0.3) : MyColors.white, shape: BoxShape.circle),
child: Center(
child: SvgPicture.asset(
"assets/icons/send.svg",
matchTextDirection: true,
colorFilter: _isThinking ? const ColorFilter.mode(MyColors.hintTextColor, BlendMode.srcIn) : null,
),
),
),
),
],
),
),
).paddingOnly(left: 16, right: 16, bottom: 16);
}
}
class ThinkingDotsAnimation extends StatefulWidget {
const ThinkingDotsAnimation({Key? key}) : super(key: key);
@override
State<ThinkingDotsAnimation> createState() => _ThinkingDotsAnimationState();
}
class _ThinkingDotsAnimationState extends State<ThinkingDotsAnimation> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(LocaleKeys.thinking.tr(), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)),
const SizedBox(width: 2),
AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
int dotCount = (_controller.value * 4).floor() % 4;
String dots = '.' * dotCount;
return SizedBox(width: 18, child: Text(dots, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)));
},
),
],
);
}
}
class AnimatedMessageBubble extends StatefulWidget {
final Widget child;
const AnimatedMessageBubble({Key? key, required this.child}) : super(key: key);
@override
State<AnimatedMessageBubble> createState() => _AnimatedMessageBubbleState();
}
class _AnimatedMessageBubbleState extends State<AnimatedMessageBubble> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400));
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_slideAnimation = Tween<Offset>(begin: const Offset(-0.3, 0.0), end: Offset.zero).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(opacity: _fadeAnimation, child: ScaleTransition(scale: _scaleAnimation, alignment: Alignment.centerLeft, child: widget.child)),
);
}
}

@ -87,23 +87,11 @@ class _EtqanOvrCreateRequestState extends State<EtqanOvrCreateRequest> {
DefaultButton(LocaleKeys.submit.tr(), () async {
provider.setDescription(_descriptionController.text);
if (provider.selectedProject == null) {
Utils.showErrorDialog(
context: context,
message: LocaleKeys.pleaseSelectProject.tr(),
onOkTapped: () {
Navigator.of(context).pop();
},
);
Utils.showToast(LocaleKeys.pleaseSelectProject.tr());
return;
}
if (!provider.isDescriptionValid()) {
Utils.showErrorDialog(
context: context,
message: LocaleKeys.pleaseEnterDescription.tr(),
onOkTapped: () {
Navigator.of(context).pop();
},
);
Utils.showToast(LocaleKeys.pleaseEnterDescription.tr());
return;
}
Map<String, dynamic>? ticketInfo = await provider.submitRequest(context);
@ -112,15 +100,10 @@ class _EtqanOvrCreateRequestState extends State<EtqanOvrCreateRequest> {
String ticketId = ticketInfo['id'] ?? '';
// String successMessage = '${LocaleKeys.requestCreatedSuccessfully.tr()}\n\n${LocaleKeys.ticketNumber.tr()}: $ticketNumber\n${LocaleKeys.ticketId.tr()}: $ticketId';
String successMessage = '${LocaleKeys.requestCreatedSuccessfully.tr()}\n\n${LocaleKeys.ticketNumber.tr()}: $ticketNumber';
Utils.showErrorDialog(
context: context,
message: successMessage,
onOkTapped: () {
Navigator.pop(context);
Navigator.pop(context);
// Navigator.popAndPushNamed(context, AppRoutes.etqanOvr);
},
onCloseTap: () {
Utils.showToast(
successMessage,
isSuccess: true,
onComplete: () {
Navigator.pop(context);
Navigator.pop(context);
// Navigator.popAndPushNamed(context, AppRoutes.etqanOvr);

@ -29,6 +29,7 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/marathon_banner.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/mark_attendance_widget.dart';
import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart';
import 'package:mohem_flutter_app/widgets/shimmer/offers_shimmer_widget.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
@ -62,6 +63,7 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
isDisplayMazaya = checkIfPrivilegedForMazaya();
cProvider = Provider.of<ChatProviderModel>(context, listen: false);
scheduleMicrotask(() {
data = Provider.of<DashboardProviderModel>(context, listen: false);
@ -111,7 +113,7 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
} else {
cProvider.buildHubConnection();
Future.delayed(const Duration(seconds: 2), () {
cProvider.invokeChatCounter(userId: AppState().chatDetails!.response!.id!);
if (AppState().chatDetails != null) cProvider.invokeChatCounter(userId: AppState().chatDetails!.response!.id!);
});
}
}
@ -254,6 +256,7 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
@override
Widget build(BuildContext context) {
isDisplayMazaya = checkIfPrivilegedForMazaya();
return SafeArea(
bottom: Platform.isAndroid ? true : false,
top: false,
@ -457,7 +460,7 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.pushNamed(context, AppRoutes.dashboardScreenForDependet);
Navigator.pushNamed(context, AppRoutes.mazayaDiscover);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
@ -471,101 +474,100 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
),
),
).paddingOnly(left: 21, right: 21, top: 0, bottom: 21),
],
// Offers & Discounts commented out as requested
/*
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
LocaleKeys.offers.tr().toText12(),
Row(
children: [
LocaleKeys.discounts.tr().toText24(isBold: true),
6.width,
Container(
padding: const EdgeInsets.only(left: 8, right: 8),
decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)),
child: LocaleKeys.newString.tr().toText10(isBold: true),
),
],
),
],
] else ...<Widget>[
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
LocaleKeys.offers.tr().toText12(),
Row(
children: [
LocaleKeys.discounts.tr().toText24(isBold: true),
6.width,
Container(
padding: const EdgeInsets.only(left: 8, right: 8),
decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)),
child: LocaleKeys.newString.tr().toText10(isBold: true),
),
],
),
],
),
),
),
LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() {
Navigator.pushNamed(context, AppRoutes.offersAndDiscounts);
}),
],
).paddingOnly(left: 21, right: 21),
Consumer<DashboardProviderModel>(
builder: (BuildContext context, DashboardProviderModel model, Widget? child) {
return SizedBox(
height: 103 + 33,
child: ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(left: 21, right: 21, top: 13),
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext cxt, int index) {
return model.isOffersLoading
? const OffersShimmerWidget()
: InkWell(
onTap: () {
navigateToDetails(data.getOffersList[index]);
},
child: SizedBox(
width: 73,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() {
Navigator.pushNamed(context, AppRoutes.offersAndDiscounts);
}),
],
).paddingOnly(left: 21, right: 21),
Consumer<DashboardProviderModel>(
builder: (BuildContext context, DashboardProviderModel model, Widget? child) {
return SizedBox(
height: 103 + 33,
child: ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(left: 21, right: 21, top: 13),
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext cxt, int index) {
return model.isOffersLoading
? const OffersShimmerWidget()
: data.getOffersList.isNotEmpty
? InkWell(
onTap: () {
navigateToDetails(data.getOffersList[index]);
},
child: SizedBox(
width: 73,
height: 73,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(100)),
border: Border.all(color: MyColors.lightGreyE3Color, width: 1),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(50)),
child: Hero(
tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!,
transitionOnUserGestures: true,
child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 73,
height: 73,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(100)),
border: Border.all(color: MyColors.lightGreyE3Color, width: 1),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(50)),
child: Hero(
tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString(),
transitionOnUserGestures: true,
child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain),
),
),
),
4.height,
Expanded(
child:
AppState().isArabic(context)
? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1)
: data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1),
),
],
),
),
4.height,
Expanded(
child:
AppState().isArabic(context)
? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1)
: data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1),
),
],
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => 8.width,
itemCount: 9,
),
);
},
),
],
),
*/
)
: Container();
},
separatorBuilder: (BuildContext cxt, int index) => 8.width,
itemCount: model.isOffersLoading ? 9 : (data.getOffersList.length > 9 ? 9 : data.getOffersList.length),
),
);
},
),
],
),
],
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 31),
@ -734,6 +736,19 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
},
),
),
// floatingActionButton: FloatingActionButton.large(
// onPressed: () {
// Navigator.pushNamed(context, AppRoutes.chatBotHome);
// },
// backgroundColor: Colors.transparent,
// elevation: 0,
// focusElevation: 0,
// hoverElevation: 0,
// highlightElevation: 0,
//
// shape: const CircleBorder(),
// child: SvgPicture.asset("assets/icons/chatbot.svg", width: 100, height: 100),
// ),
),
);
}
@ -774,4 +789,14 @@ class _DashboardScreenState extends State<DashboardScreen> with WidgetsBindingOb
}
return false;
}
bool checkIfPrivilegedForMazaya() {
if (AppState().privilegeListModel == null) return false;
for (PrivilegeListModel element in AppState().privilegeListModel!) {
if (element.serviceName?.toLowerCase() == "mazayaeligibility") {
return (element.previlege ?? false) || (element.previlegeHMG ?? false);
}
}
return false;
}
}

@ -1,970 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mohem_flutter_app/api/login_api_client.dart';
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/main.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/widgets/info_chip_widget.dart';
import 'package:shimmer/shimmer.dart';
class DashboardScreenForDependet extends StatefulWidget {
const DashboardScreenForDependet({Key? key}) : super(key: key);
@override
State<DashboardScreenForDependet> createState() => _DashboardScreenForDependetState();
}
class _DashboardScreenForDependetState extends State<DashboardScreenForDependet> {
List<WalaPlusRetailerModel> retailersList = [];
List<WalaPlusRetailerModel> _filteredRetailersList = [];
List<WalaPlusCategoryModel> categoriesList = [];
String currentCategoryId = "";
String selectedTypeFilter = "all";
bool isNearestOffers = true;
bool isLoading = true;
bool isLoadingMore = false;
bool isCategoriesLoading = true;
bool hasMore = true;
bool _isFetchingPage = false;
int currentPage = 1;
int totalPages = 1;
final int pageSize = 10;
String searchQuery = "";
Timer? _debounceTimer;
final TextEditingController _searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _hasRegisteredMobileLoginInfo = false;
List<String> get _availableTypeFilters {
Set<String> types = <String>{};
for (WalaPlusRetailerModel r in retailersList) {
String? t = r.normalizedType;
if (t != null && t.isNotEmpty) {
types.add(t);
}
}
List<String> list = <String>['all', 'instore', 'online'];
for (String t in types) {
if (!list.contains(t)) {
list.add(t);
}
}
return list;
}
String _getTypeFilterTitle(String type) {
if (type == 'all') {
return LocaleKeys.allCategories.tr();
} else if (type == 'instore') {
return LocaleKeys.inStore.tr();
} else if (type == 'online') {
return LocaleKeys.online.tr();
}
return type.isNotEmpty ? (type[0].toUpperCase() + type.substring(1)) : type;
}
IconData _getTypeFilterIcon(String type) {
if (type == 'instore') {
return Icons.storefront_rounded;
} else if (type == 'online') {
return Icons.language_rounded;
}
return Icons.tune_rounded;
}
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
_loadDependentName();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
await _registerMobileLoginInfo();
await fetchCategories();
await fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
}
});
}
Future<void> _registerMobileLoginInfo() async {
if (_hasRegisteredMobileLoginInfo) return;
if (!ApiConsts.isDependetLogin) return;
_hasRegisteredMobileLoginInfo = true;
try {
int loginType = AppState().dependentLoginType ?? 1;
String nationalId = AppState().getUserName ?? "";
String dob = AppState().dependentDob ?? "";
String name = AppState().dependentName ?? "";
String mobile = AppState().postParamsObject?.mobileNumber ?? "";
String tokenID =
(AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) ? AppState().postParamsObject!.tokenID! : (AppState().authenticationTokenID ?? "");
if (tokenID.isEmpty) {
tokenID = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID");
}
int sessionId = AppState().postParamsObject?.pSessionId ?? 0;
String email = AppState().postParamsObject?.pEmailAddress ?? "";
String deviceToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? "");
await LoginApiClient().insertMobileLoginInfoDependent(
loginType: loginType,
nationalId: nationalId,
dob: dob,
dependentName: name,
mobileNumber: mobile,
tokenID: tokenID,
sessionId: sessionId,
email: email,
deviceToken: deviceToken,
context: context,
);
} catch (e) {
if (kDebugMode) {
print("Error calling insertMobileLoginInfoDependent: $e");
}
}
}
void _loadDependentName() {
if (AppState().dependentName != null && AppState().dependentName!.isNotEmpty && mounted) {
setState(() {});
}
}
@override
void dispose() {
_debounceTimer?.cancel();
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _scrollListener() {
if (!_scrollController.hasClients) return;
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
// Trigger only when user scrolls to within 35px of the very bottom
if (currentScroll >= maxScroll - 35) {
if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) {
fetchNextPage();
}
}
}
Future<void> fetchCategories() async {
setState(() {
isCategoriesLoading = true;
});
try {
List<WalaPlusCategoryModel> list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
if (list.isEmpty) {
await Future.delayed(const Duration(milliseconds: 600));
list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
}
if (mounted) {
List<WalaPlusCategoryModel> fullList = [];
bool hasAll = list.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل"));
if (!hasAll) {
fullList.add(WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr()));
}
fullList.addAll(list);
setState(() {
categoriesList = fullList;
if (currentCategoryId.isEmpty) {
currentCategoryId = "all";
}
});
}
} catch (e) {
debugPrint("Error fetching WalaPlus categories (attempt 1): $e");
try {
await Future.delayed(const Duration(milliseconds: 600));
List<WalaPlusCategoryModel> retryList = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context);
if (mounted && retryList.isNotEmpty) {
List<WalaPlusCategoryModel> fullList = [];
bool hasAll = retryList.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل"));
if (!hasAll) {
fullList.add(WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr()));
}
fullList.addAll(retryList);
setState(() {
categoriesList = fullList;
if (currentCategoryId.isEmpty) {
currentCategoryId = "all";
}
});
return;
}
} catch (retryEx) {
debugPrint("Error fetching WalaPlus categories (attempt 2): $retryEx");
}
if (mounted && categoriesList.isEmpty) {
setState(() {
categoriesList = [WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr())];
if (currentCategoryId.isEmpty) {
currentCategoryId = "all";
}
});
}
} finally {
if (mounted) {
setState(() {
isCategoriesLoading = false;
});
}
}
}
Future<void> fetchOffers({String? categoryId, String? query}) async {
String targetCategoryId = categoryId ?? (currentCategoryId.isEmpty ? "all" : currentCategoryId);
if (query != null) {
searchQuery = query;
}
setState(() {
isLoading = true;
currentPage = 1;
hasMore = true;
});
try {
String catParam = (targetCategoryId == "all" || targetCategoryId == "0") ? "" : targetCategoryId;
String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
print("Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize");
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
categoryId: catParam,
page: 1,
perPage: pageSize,
offerType: offerTypeParam,
query: queryParam,
context: isNearestOffers ? context : null,
);
retailersList = response?.retailers ?? <WalaPlusRetailerModel>[];
_filteredRetailersList = retailersList;
currentPage = 1;
totalPages = response?.pagesCount ?? 1;
hasMore = currentPage < totalPages && (response?.retailers?.isNotEmpty ?? false);
} catch (e) {
print("Error fetching WalaPlus retailers: $e");
retailersList = <WalaPlusRetailerModel>[];
_filteredRetailersList = <WalaPlusRetailerModel>[];
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
Future<void> fetchNextPage() async {
if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return;
_isFetchingPage = true;
setState(() {
isLoadingMore = true;
});
try {
int nextPage = currentPage + 1;
String catParam = (currentCategoryId == "all" || currentCategoryId == "0") ? "" : currentCategoryId;
String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'");
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
categoryId: catParam,
page: nextPage,
perPage: pageSize,
offerType: offerTypeParam,
query: queryParam,
context: isNearestOffers ? context : null,
);
List<WalaPlusRetailerModel> newRetailers = response?.retailers ?? [];
if (newRetailers.isNotEmpty) {
currentPage = nextPage;
totalPages = response?.pagesCount ?? totalPages;
hasMore = currentPage < totalPages;
retailersList.addAll(newRetailers);
_filteredRetailersList = retailersList;
} else {
hasMore = false;
}
} catch (e) {
print("Error fetching next page of WalaPlus retailers: $e");
} finally {
_isFetchingPage = false;
if (mounted) {
setState(() {
isLoadingMore = false;
});
}
}
}
Future<void> _onRefresh() async {
await Future.wait<dynamic>(<Future<void>>[fetchCategories(), fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId, query: _searchController.text)]);
}
void _onSearchChanged(String query) {
_debounceTimer?.cancel();
searchQuery = query;
if (query.trim().isEmpty) {
fetchOffers(query: "");
} else {
_debounceTimer = Timer(const Duration(milliseconds: 1000), () {
fetchOffers(query: query.trim());
});
}
}
void _openRetailer(WalaPlusRetailerModel item) {
Navigator.pushNamed(context, AppRoutes.walaPlusRetailerDetails, arguments: item);
}
void _performLogout() {
Utils.confirmDialog(
context,
LocaleKeys.areYouSureWantToLogOut.tr(),
onTap: () {
ApiConsts.selectedMemberType = MemberType.employee;
ApiConsts.ISDependetLogin = false;
AppState().setPostParamsInitConfig();
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route<dynamic> route) => false);
},
);
}
@override
Widget build(BuildContext context) {
bool isAr = AppState().isArabic(context);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
} else {
_performLogout();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Icon(isAr ? Icons.arrow_forward_ios_rounded : Icons.arrow_back_ios_new_rounded, size: 20, color: const Color(0xFF1E293B)),
),
),
const SizedBox(width: 8),
SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 48, fit: BoxFit.contain),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!ApiConsts.isDependetLogin) ...[
IconButton(
icon: Image.asset(
'assets/icons/ic_add_dependents.png',
width: 24,
height: 24,
color: const Color(0xFF1E293B),
errorBuilder:
(context, error, stackTrace) =>
SvgPicture.asset('assets/icons/ic_dependents_top.svg', width: 24, height: 24, colorFilter: const ColorFilter.mode(Color(0xFF1E293B), BlendMode.srcIn)),
),
tooltip: LocaleKeys.addDependents.tr(),
onPressed: () {
Navigator.pushNamed(context, AppRoutes.familyMembers, arguments: {'fromMazaya': true});
},
),
],
IconButton(
icon: const Icon(Icons.home_rounded, size: 26, color: Color(0xFF1E293B)),
onPressed: () {
if (ApiConsts.isDependetLogin) {
_onRefresh();
} else {
Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard));
}
},
),
],
),
],
),
),
),
body: SafeArea(
child: RefreshIndicator(
onRefresh: _onRefresh,
color: MyColors.textMixColor,
child: SingleChildScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search Input Card
_buildSearchBar(isAr),
// "Browse Categories" Header
_buildBrowseCategoriesHeader(isAr),
// Categories Section
if (categoriesList.isNotEmpty) _buildCategoriesList(isAr) else _buildCategoriesShimmer(),
// Type Filter Chips (All, In Store, Online)
_buildTypeFilterChips(isAr),
const SizedBox(height: 16),
// Offers Grid
if (isLoading)
_buildRetailersShimmer()
else if (_filteredRetailersList.isNotEmpty) ...[
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, childAspectRatio: 0.7, crossAxisSpacing: 12, mainAxisSpacing: 16),
itemCount: _filteredRetailersList.length,
itemBuilder: (context, index) {
return _buildOfferCard(_filteredRetailersList[index], isAr);
},
),
if (isLoadingMore) ...[
16.height,
Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))),
],
] else
Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))),
],
),
),
),
),
);
}
Widget _buildSearchBar(bool isAr) {
return Container(
width: double.infinity,
height: 70,
padding: const EdgeInsets.only(top: 14, right: 16, bottom: 14, left: 16),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0),
boxShadow: [BoxShadow(color: const Color(0xFF000000).withOpacity(0.0314), offset: const Offset(0, 2), blurRadius: 5, spreadRadius: 0)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
LocaleKeys.search.tr().toText12(isBold: true, color: const Color(0xFF2A333C)),
const SizedBox(height: 2),
TextField(
controller: _searchController,
onChanged: _onSearchChanged,
onSubmitted: (String val) {
_debounceTimer?.cancel();
fetchOffers(query: val.trim());
},
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
hintText: LocaleKeys.searchItems.tr(),
hintStyle: const TextStyle(color: Color(0xFFACAFB1), fontSize: 14, fontWeight: FontWeight.w400, height: 1.2),
border: InputBorder.none,
),
style: const TextStyle(color: Color(0xFF2A333C), fontSize: 14, fontWeight: FontWeight.w400),
),
],
),
);
}
Widget _buildBrowseCategoriesHeader(bool isAr) {
return Padding(padding: const EdgeInsets.only(top: 22, bottom: 14), child: LocaleKeys.browseCategories.tr().toText18(isBold: true, color: const Color(0xFF1E293B)));
}
Widget _buildCategoriesList(bool isAr) {
return SizedBox(
height: 136.0,
width: double.infinity,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: categoriesList.length,
separatorBuilder: (_, __) => const SizedBox(width: 14),
itemBuilder: (BuildContext context, int index) {
WalaPlusCategoryModel item = categoriesList[index];
bool isSelected = (item.id == "all" && (currentCategoryId == "all" || currentCategoryId.isEmpty)) || (currentCategoryId.isNotEmpty && currentCategoryId == item.id);
String title = (item.id == "all") ? LocaleKeys.allCategories.tr() : (item.name ?? "");
return InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () {
if (currentCategoryId != item.id) {
setState(() {
currentCategoryId = item.id ?? "";
});
fetchOffers(categoryId: currentCategoryId);
}
},
child: SizedBox(
width: 84,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_buildCategoryCard(item, isSelected),
const SizedBox(height: 8),
title.toText13(
// isBold: true,
color: const Color(0xFF1E293B),
maxlines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
),
);
}
Widget _buildTypeFilterChips(bool isAr) {
List<String> types = _availableTypeFilters;
return Padding(
padding: const EdgeInsets.only(top: 14, bottom: 2),
child: SizedBox(
height: 38,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
itemCount: types.length,
separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8),
itemBuilder: (BuildContext context, int index) {
String typeKey = types[index];
bool isSelected = selectedTypeFilter == typeKey;
String title = _getTypeFilterTitle(typeKey);
IconData icon = _getTypeFilterIcon(typeKey);
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () {
if (selectedTypeFilter != typeKey) {
setState(() {
selectedTypeFilter = typeKey;
});
fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId);
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
alignment: Alignment.center,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(icon, size: 14, color: isSelected ? MyColors.white : MyColors.black),
const SizedBox(width: 5),
title.toText12(color: isSelected ? MyColors.white : MyColors.black),
],
),
),
);
},
),
),
);
}
Widget _buildCategoryCard(WalaPlusCategoryModel item, bool isSelected) {
const double cardSize = 84.0;
const double borderRadiusVal = 18.0;
const double borderWidthVal = 2.8;
const Color activeBorderColor = Color(0xFF00A86B);
bool isAll = (item.id == "all" || item.id == "0" || (item.name ?? "").toLowerCase() == "all" || item.name == "الجميع" || item.name == "الكل");
Widget imageContent;
if (isAll) {
imageContent = Image.asset('assets/images/cat_all.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item));
} else {
String categoryImageUrl = item.categoryImageUrl.isNotEmpty ? item.categoryImageUrl : (item.image?.trim() ?? "");
if (categoryImageUrl.isNotEmpty) {
if (categoryImageUrl.toLowerCase().endsWith('.svg') || item.isSvg) {
imageContent = SvgPicture.network(
categoryImageUrl,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
placeholderBuilder: (_) => const Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00A86B)))),
);
} else {
imageContent = Image.network(categoryImageUrl, width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item));
}
} else {
imageContent = _getFallbackCategoryAsset(item);
}
}
return Container(
width: cardSize,
height: cardSize,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(borderRadiusVal),
border: isSelected ? Border.all(color: activeBorderColor, width: borderWidthVal) : null,
),
child: ClipRRect(borderRadius: BorderRadius.circular(isSelected ? (borderRadiusVal - borderWidthVal) : borderRadiusVal), child: imageContent),
);
}
Widget _getFallbackCategoryAsset(WalaPlusCategoryModel item) {
String name = (item.name ?? "").toLowerCase();
if (item.id == "all" || item.id == "0" || name == "all" || name == "الكل" || name == "الجميع") {
return Image.asset('assets/images/cat_all.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item));
} else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) {
return Image.asset('assets/images/cat_retail.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item));
} else if (name.contains("food") || name.contains("restaurant") || name.contains("مطاعم") || name.contains("أغذية") || name.contains("cafe") || name.contains("مقهى")) {
return Image.asset('assets/images/cat_food.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item));
} else if (name.contains("fitness") || name.contains("wellness") || name.contains("sport") || name.contains("gym") || name.contains("رياضة") || name.contains("لياقة") || name.contains("صحة")) {
return Image.asset('assets/images/cat_fitness.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item));
}
return _buildFallbackCategoryIconWidget(item);
}
Widget _buildFallbackCategoryIconWidget(WalaPlusCategoryModel item) {
return Container(color: Colors.transparent, alignment: Alignment.center, child: _getFallbackCategoryIcon(item, const Color(0xFF168A89)));
}
Widget _getFallbackCategoryIcon(WalaPlusCategoryModel item, Color iconColor) {
String name = (item.name ?? "").toLowerCase();
IconData iconData = Icons.grid_view_rounded;
Color color = iconColor;
if (item.id == "all" || name == "all" || name == "الكل" || name == "الجميع") {
iconData = Icons.confirmation_number_outlined;
color = const Color(0xFFE52332);
} else if (name.contains("finance") || name.contains("بنوك") || name.contains("مالي")) {
iconData = Icons.account_balance_rounded;
} else if (name.contains("entertain") || name.contains("activities") || name.contains("أنشطة") || name.contains("ترفيه")) {
iconData = Icons.theater_comedy_rounded;
} else if (name.contains("educat") || name.contains("تعليم")) {
iconData = Icons.menu_book_rounded;
} else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) {
iconData = Icons.shopping_bag_outlined;
} else if (name.contains("health") || name.contains("medical") || name.contains("صحة") || name.contains("طبي")) {
iconData = Icons.health_and_safety_outlined;
} else if (name.contains("beauty") || name.contains("جمال") || name.contains("عناية")) {
iconData = Icons.face_retouching_natural_rounded;
} else if (name.contains("fitness") || name.contains("sport") || name.contains("gym") || name.contains("رياضة") || name.contains("لياقة")) {
iconData = Icons.fitness_center_rounded;
} else if (name.contains("hotel") || name.contains("travel") || name.contains("فندق") || name.contains("فنادق") || name.contains("سفر")) {
iconData = Icons.hotel_rounded;
} else if (name.contains("service") || name.contains("خدمات")) {
iconData = Icons.build_circle_outlined;
} else if (name.contains("food") || name.contains("restaurant") || name.contains("مطاعم") || name.contains("أغذية")) {
iconData = Icons.restaurant_rounded;
} else if (name.contains("home") || name.contains("منزل")) {
iconData = Icons.home_rounded;
}
return Icon(iconData, color: color, size: 26);
}
String _getDiscountBadge(WalaPlusRetailerModel item) {
if (item.offerTitle != null) {
RegExp percentRegex = RegExp(r'(\d+%)');
Match? match = percentRegex.firstMatch(item.offerTitle!);
if (match != null) {
return match.group(1)!;
}
}
if (item.name != null) {
RegExp percentRegex = RegExp(r'(\d+%)');
Match? match = percentRegex.firstMatch(item.name!);
if (match != null) {
return match.group(1)!;
}
}
return "";
}
Widget _buildMerchantMIcon() {
return Container(color: const Color(0xFFED1C24), alignment: Alignment.center, child: "M".toText10(isBold: true, color: Colors.white));
}
Widget _buildOfferCard(WalaPlusRetailerModel item, bool isAr) {
String? bannerUrl = (item.imageThumb != null && item.imageThumb!.isNotEmpty) ? item.imageThumb : item.image;
String? logoUrl = (item.logoThumb != null && item.logoThumb!.isNotEmpty) ? item.logoThumb : item.logo;
String discountText = _getDiscountBadge(item);
String offerSubtitle = "";
if (discountText.isNotEmpty) {
if (item.offerTitle != null && item.offerTitle!.toLowerCase().contains("off")) {
offerSubtitle = item.offerTitle!;
} else {
offerSubtitle = LocaleKeys.offDiscount.tr(args: [discountText]);
}
} else if (item.offerTitle != null && item.offerTitle!.trim().isNotEmpty) {
offerSubtitle = item.offerTitle!.trim();
}
String? typeText = item.getDisplayType();
bool isOnline = item.isOnlineStore;
String validityText = item.getDisplayValidity();
String usageText = "";
if (item.displayUsage.isNotEmpty) {
usageText = item.displayUsage.replaceAll('usage:', LocaleKeys.usageLabel.tr()).replaceAll('Usage :', LocaleKeys.usageLabel.tr());
}
String cityText = item.getDisplayCity(isAr: isAr);
if (cityText.trim().isEmpty) {
cityText = item.displayCity;
}
return InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _openRetailer(item),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF0D1720).withOpacity(0.0392), offset: const Offset(0, 5), blurRadius: 18, spreadRadius: 0)],
),
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Inset image with rounded corners
ClipRRect(
borderRadius: BorderRadius.circular(14),
child: SizedBox(
height: 120,
width: double.infinity,
child: Stack(
fit: StackFit.expand,
children: [
// Banner Image
(bannerUrl != null && bannerUrl.isNotEmpty)
? Image.network(
bannerUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)),
)
: Container(color: const Color(0xFFF1F5F9), child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32)),
// Gradient overlay at bottom of image
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 50,
child: Container(
decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(0.75), Colors.transparent])),
),
),
// Discount badge at top right (flush with edge, pill on start side)
if (discountText.isNotEmpty)
PositionedDirectional(
top: 20,
end: 0,
child: Container(
padding: const EdgeInsetsDirectional.only(start: 14, end: 10, top: 4, bottom: 4),
decoration: const BoxDecoration(color: Color(0xFFED1C24), borderRadius: BorderRadiusDirectional.only(topStart: Radius.circular(20), bottomStart: Radius.circular(20))),
child: discountText.toText14(isBold: true, color: Colors.white),
),
),
// Brand logo & name at bottom left
PositionedDirectional(
bottom: 8,
start: 8,
end: 8,
child: Row(
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4)),
clipBehavior: Clip.antiAlias,
child: (logoUrl != null && logoUrl.isNotEmpty) ? Image.network(logoUrl, fit: BoxFit.contain, errorBuilder: (_, __, ___) => _buildMerchantMIcon()) : _buildMerchantMIcon(),
),
const SizedBox(width: 5),
Expanded(child: (item.name ?? "").toText11(isBold: true, maxLine: 1, color: Colors.white)),
],
),
),
],
),
),
),
const SizedBox(height: 8),
// Retailer Name / Bundle Title
(item.name ?? "").toText14(isBold: true, maxlines: 1, overflow: TextOverflow.ellipsis, color: const Color(0xFF1E293B)),
if (offerSubtitle.isNotEmpty) ...[const SizedBox(height: 2), offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B))],
// Badges Row: Type, Validity, Location & Usage
if ((typeText != null && typeText.trim().isNotEmpty) || validityText.trim().isNotEmpty || cityText.trim().isNotEmpty || usageText.trim().isNotEmpty) ...[
const SizedBox(height: 6),
Wrap(
spacing: 5,
runSpacing: 4,
children: <Widget>[
// Type Chip (Online or In Store)
if (typeText != null && typeText.trim().isNotEmpty)
InfoChipWidget(
text: typeText,
icon: isOnline ? Icons.language_rounded : Icons.storefront_rounded,
backgroundColor: isOnline ? const Color(0xFFEFF6FF) : const Color(0xFFF5F3FF),
textColor: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED),
),
// Validity badge
if (validityText.trim().isNotEmpty) InfoChipWidget(text: validityText, backgroundColor: const Color(0xFFE6F7F0), textColor: const Color(0xFF00A651)),
// Location Badge
if (cityText.trim().isNotEmpty)
InfoChipWidget(
text: cityText,
svgIcon: "assets/icons/location.svg",
isBold: false,
backgroundColor: const Color(0xFFF3F4F6),
textColor: const Color(0xFF4B5563),
iconColor: const Color(0xFF64748B),
iconTextGap: 2.5,
),
// Usage badge
if (usageText.trim().isNotEmpty) InfoChipWidget(text: usageText, backgroundColor: const Color(0xFFFEF3C7), textColor: const Color(0xFFB45309)),
],
),
],
],
),
),
);
}
Widget _buildRetailersShimmer() {
return Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, childAspectRatio: 0.7, crossAxisSpacing: 12, mainAxisSpacing: 16),
itemCount: 4,
itemBuilder: (_, __) {
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: 105, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))),
const SizedBox(height: 8),
Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 4),
Container(height: 12, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 6),
Wrap(
spacing: 5,
runSpacing: 4,
children: <Widget>[
Container(height: 16, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
Container(height: 16, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
Container(height: 16, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
],
),
],
),
);
},
),
);
}
Widget _buildCategoriesShimmer() {
return SizedBox(
height: 136.0,
child: Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: ListView.separated(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: 5,
separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 14),
itemBuilder: (BuildContext _, int index) {
return SizedBox(
width: 84,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(width: 84, height: 84, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(18))),
const SizedBox(height: 8),
Container(width: 65, height: 11, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 4),
Container(width: 42, height: 11, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
],
),
);
},
),
),
);
}
}

@ -41,7 +41,7 @@ class _ChangeItgAdPasswordScreenState extends State<ChangeItgAdPasswordScreen> {
GenericResponseModel response = await LoginApiClient().changePasswordFromActiveDirectorySession(password.text, AppState().memberInformationList!.eMPLOYEEEMAILADDRESS!);
Utils.hideLoading(context);
if ((response.messageStatus ?? 0) == 1) {
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr());
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true);
Navigator.pop(context);
}
} catch (ex) {

@ -222,7 +222,7 @@ class _SurveyScreenState extends State<SurveyScreen> {
Utils.hideLoading(context);
if (res!.mohemmItgResponseItem!.statusCode == 200) {
Utils.showToast("Survey has been submitted successfully");
Utils.showToast("Survey has been submitted successfully", isSuccess: true);
Navigator.pop(context);
} else {
Utils.showToast(res.mohemmItgResponseItem!.message.toString());

File diff suppressed because it is too large Load Diff

@ -111,7 +111,7 @@ class ServicesWidget extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true)),
Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true, maxLine: 3)),
RotatedBox(quarterTurns: AppState().isArabic(context) ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)),
],
),

@ -39,7 +39,7 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
try {
var genericResponseModel = await LoginApiClient().changePasswordFromActiveSession(oldPassword.text, password.text, confirmPassword.text);
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr());
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true);
Navigator.pop(context);
} catch (ex) {
Utils.hideLoading(context);

@ -50,29 +50,27 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
context,
1,
int.tryParse(_basicMemberInformation?.pMOBILENUMBER ?? ""),
(value,TextEditingController _pinPutController) async {
(value, TextEditingController _pinPutController) async {
Utils.showLoading(context);
try{
GenericResponseModel? genericResponseModel = await LoginApiClient().checkPublicActivationCode(value, employeeId.text);
if (genericResponseModel?.errorMessage != null) {
Utils.showToast(genericResponseModel?.errorMessage ?? "");
return;
}
Utils.hideLoading(context);
await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: employeeId.text);
Navigator.pop(context);
Navigator.pop(context);
}catch(ex){
print(ex);
_pinPutController.clear();
otpFieldClear.value = "";
Utils.hideLoading(context);
Utils.handleException(ex, context, null);
}
},
() => {
Navigator.pop(context),
try {
GenericResponseModel? genericResponseModel = await LoginApiClient().checkPublicActivationCode(value, employeeId.text);
if (genericResponseModel?.errorMessage != null) {
Utils.showToast(genericResponseModel?.errorMessage ?? "");
return;
}
Utils.hideLoading(context);
await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: employeeId.text);
Navigator.pop(context);
Navigator.pop(context);
} catch (ex) {
print(ex);
_pinPutController.clear();
otpFieldClear.value = "";
Utils.hideLoading(context);
Utils.handleException(ex, context, null);
}
},
() => {Navigator.pop(context)},
onResendCode: () {
performForgotPassword();
},
@ -87,13 +85,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor),
onPressed: () => Navigator.pop(context),
),
),
appBar: AppBar(backgroundColor: Colors.transparent, leading: IconButton(icon: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), onPressed: () => Navigator.pop(context))),
body: Column(
children: [
//const SizedBox(height: 23),
@ -122,19 +114,19 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
),
],
),
)
),
],
),
),
),
DefaultButton(
LocaleKeys.changePassword.tr(),
employeeId.text.isEmpty
? null
: () async {
performForgotPassword();
})
.insideContainer
LocaleKeys.changePassword.tr(),
employeeId.text.isEmpty
? null
: () async {
performForgotPassword();
},
).insideContainer,
],
),
);

@ -8,6 +8,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
// import 'package:huawei_hmsavailability/huawei_hmsavailability.dart';
import 'package:mohem_flutter_app/api/login_api_client.dart';
@ -18,6 +19,7 @@ import 'package:mohem_flutter_app/classes/notifications.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/dialogs/otp_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/extensions/widget_extensions.dart';
@ -215,16 +217,56 @@ class _LoginScreenState extends State<LoginScreen> {
}
String devType = Platform.isAndroid ? "android" : "ios";
if (ApiConsts.selectedMemberType.isDependent) {
dependentLoginInfo = await LoginApiClient().getMobileLoginInfoNEWDependent(devToken, devType, memberType: MemberType.dependent.value);
Utils.hideLoading(context);
if (dependentLoginInfo != null) {
_handleDependentLoginInfo(dependentLoginInfo!);
// if (ApiConsts.selectedMemberType.isDependent) {
// dependentLoginInfo = await LoginApiClient().getMobileLoginInfoNEWDependent(devToken, devType, memberType: MemberType.dependent.value);
// Utils.hideLoading(context);
// if (dependentLoginInfo != null) {
// _handleDependentLoginInfo(dependentLoginInfo!);
// }
// return;
// }
loginInfo = await LoginApiClient().getMobileLoginInfoNEW(devToken, devType, memberType: ApiConsts.selectedMemberType.value);
bool isEmpLast = AppState().isEmpLastLoginToDevice ?? true;
if (!isEmpLast) {
// Dependent login flow
ApiConsts.selectedMemberType = MemberType.dependent;
ApiConsts.ISDependetLogin = true;
if (mounted) {
setState(() {
ApiConsts.selectedMemberType = MemberType.dependent;
ApiConsts.ISDependetLogin = true;
});
}
bool autoLoggedIn = await _tryDependentAutoLogin();
if (autoLoggedIn) {
return;
} else {
dependentLoginInfo = await LoginApiClient().getMobileLoginInfoNEWDependent(devToken, devType, memberType: MemberType.dependent.value);
Utils.hideLoading(context);
if (dependentLoginInfo != null) {
_handleDependentLoginInfo(dependentLoginInfo!);
}
if (mounted) {
setState(() {});
}
return;
}
return;
}
loginInfo = await LoginApiClient().getMobileLoginInfoNEW(devToken, devType, memberType: ApiConsts.selectedMemberType.value);
// Employee login flow
ApiConsts.selectedMemberType = MemberType.employee;
ApiConsts.ISDependetLogin = false;
if (mounted) {
setState(() {
ApiConsts.selectedMemberType = MemberType.employee;
ApiConsts.ISDependetLogin = false;
});
}
if (loginInfo == null) {
await checkPrefs();
_autoLogin = false;
@ -261,8 +303,9 @@ class _LoginScreenState extends State<LoginScreen> {
}
if (info.dependentName != null && info.dependentName!.isNotEmpty) {
AppState().setDependentName = info.dependentName;
Utils.saveStringFromPrefs("P_DEPENDENT_NAME", info.dependentName!);
String cleanedName = info.dependentName!.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
AppState().setDependentName = cleanedName;
Utils.saveStringFromPrefs("P_DEPENDENT_NAME", cleanedName);
}
}
@ -361,10 +404,26 @@ class _LoginScreenState extends State<LoginScreen> {
AppState().setMohemmWifiPassword = mohemmWifiPassword;
}
Utils.hideLoading(context);
if (_autoLogin) {
if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") {
if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") {
firebaseToken = AppState().getDeviceToken;
} else {
String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
if (saved.isNotEmpty && saved != "null") {
firebaseToken = saved;
}
}
}
String effectiveTokenArg = (firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null")
? firebaseToken!
: (AppState().getDeviceToken ?? "");
if (_autoLogin && loginInfo != null) {
if (loginInfo!.deviceToken == null || loginInfo!.deviceToken!.isEmpty || loginInfo!.deviceToken == "null") {
loginInfo!.deviceToken = effectiveTokenArg;
}
Navigator.pushReplacementNamed(context, AppRoutes.verifyLastLogin, arguments: loginInfo);
} else {
Navigator.pushNamed(context, AppRoutes.verifyLogin, arguments: "$firebaseToken");
Navigator.pushNamed(context, AppRoutes.verifyLogin, arguments: effectiveTokenArg);
}
Utils.saveStringFromPrefs(SharedPrefsConsts.password, password.text);
} catch (ex) {
@ -372,19 +431,21 @@ class _LoginScreenState extends State<LoginScreen> {
print(ex.toString());
Utils.hideLoading(context);
Utils.handleException(ex, context, (msg) {
Utils.confirmDialog(
context,
msg,
onTap: () async {
if (msg.toLowerCase().contains("password has expired")) {
Navigator.pop(context);
// await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: username.text);
if (msg.toLowerCase().contains("password has expired")) {
// Sends an OTP/activation code via performForgotPassword() - requires an explicit
// tap, not an auto-firing toast, since it's a real side effect, not just navigation.
ConfirmBottomSheet.show(
context,
message: msg,
confirmText: LocaleKeys.forgotPassword.tr(),
showCancelButton: false,
onConfirm: () {
performForgotPassword();
} else {
Navigator.pop(context);
}
},
);
},
);
} else {
Utils.showToast(msg);
}
});
}
}
@ -436,12 +497,15 @@ class _LoginScreenState extends State<LoginScreen> {
void _switchToMode(MemberType type) async {
if (ApiConsts.selectedMemberType != type) {
AppState().setIsEmpLastLoginToDevice = (type == MemberType.employee);
setState(() {
ApiConsts.selectedMemberType = type;
ApiConsts.ISDependetLogin = (type == MemberType.dependent);
});
if (type == MemberType.dependent) {
await checkDependentPrefs();
} else {
await checkPrefs();
}
if (firebaseToken == null || firebaseToken!.isEmpty) {
String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
@ -449,11 +513,10 @@ class _LoginScreenState extends State<LoginScreen> {
firebaseToken = saved;
}
}
checkLoginInfo();
}
}
Widget _buildCtaCard({required BuildContext context, required IconData icon, required String title, required String subtitle, required VoidCallback onTap}) {
Widget _buildCtaCard({bool isSVG = false, required String iconPath, required BuildContext context, required String title, required String subtitle, required VoidCallback onTap}) {
bool isRtl = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context);
return InkWell(
@ -464,14 +527,14 @@ class _LoginScreenState extends State<LoginScreen> {
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: const Color(0xFFEAF8F4), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFB8E8DB), width: 1.0)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)),
alignment: Alignment.center,
child: Icon(icon, color: const Color(0xFF168A89), size: 22),
child: isSVG ? SvgPicture.asset(iconPath, height: 22, width: 22).paddingOnly(top: 2, left: 3, right: 3) : Image.asset(iconPath, height: 22, width: 22).paddingOnly(top: 2),
),
const SizedBox(width: 12),
Expanded(
@ -495,7 +558,8 @@ class _LoginScreenState extends State<LoginScreen> {
Widget _buildDependentCtaCard(BuildContext context) {
return _buildCtaCard(
context: context,
icon: Icons.person_outline_rounded,
isSVG: false,
iconPath: 'assets/images/mazaya.png',
title: LocaleKeys.joinMazaya.tr(),
subtitle: LocaleKeys.joinMazayaSubtitle.tr(),
onTap: () => _switchToMode(MemberType.dependent),
@ -505,7 +569,7 @@ class _LoginScreenState extends State<LoginScreen> {
Widget _buildEmployeeCtaCard(BuildContext context) {
return _buildCtaCard(
context: context,
icon: Icons.badge_outlined,
iconPath: 'assets/images/mohem.png',
title: LocaleKeys.continueAsEmployee.tr(),
subtitle: LocaleKeys.continueAsEmployeeSubtitle.tr(),
onTap: () => _switchToMode(MemberType.employee),
@ -544,13 +608,30 @@ class _LoginScreenState extends State<LoginScreen> {
return "";
}
void _navigateToMazayaAccessRevoked([String? depName]) async {
void _navigateToMazayaAccessRevoked([String? depName, String? message]) async {
await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN");
await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID");
await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE");
await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID");
await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID");
await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID");
await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER");
await Utils.removeStringFromPrefs("P_DEPENDENT_NAME");
await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE");
ApiConsts.selectedMemberType = MemberType.employee;
ApiConsts.ISDependetLogin = false;
AppState().setDependentName = null;
AppState().setDependentDob = null;
AppState().setDependentLoginType = null;
AppState().setWalaplusDepInfoModel = null;
AppState().setPostParamsInitConfig();
String name = depName ?? AppState().dependentName ?? "";
if (name.isEmpty) {
name = await Utils.getStringFromPrefs("P_DEPENDENT_NAME");
}
if (!mounted) return;
Navigator.push(context, MaterialPageRoute(builder: (_) => MazayaAccessResultScreen(isGranted: false, dependentName: name)));
Navigator.push(context, MaterialPageRoute(builder: (_) => MazayaAccessResultScreen(isGranted: false, dependentName: name, message: message)));
}
void performDependentLogin() async {
@ -601,8 +682,9 @@ class _LoginScreenState extends State<LoginScreen> {
AppState().setWalaplusDepInfoModel = depLoginResponse!.getWalaplusDepInfoModel;
String? depName = depLoginResponse.getWalaplusDepInfoModel?.pDependentName;
if (depName != null && depName.trim().isNotEmpty) {
AppState().setDependentName = depName.trim();
Utils.saveStringFromPrefs("P_DEPENDENT_NAME", depName.trim());
String cleanedName = depName.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
AppState().setDependentName = cleanedName;
Utils.saveStringFromPrefs("P_DEPENDENT_NAME", cleanedName);
}
}
@ -705,7 +787,7 @@ class _LoginScreenState extends State<LoginScreen> {
try {
await LoginApiClient().mohemmSendOTPDependent(nationalId: nationalId.text.trim(), mobileNumber: mobileNumber, logInTokenID: logInTokenID, context: context);
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.verificationCodeResent.tr());
Utils.showToast(LocaleKeys.verificationCodeResent.tr(), isSuccess: true);
_showDependentOtpDialog(logInTokenID, mobileNumber);
} catch (ex) {
Utils.hideLoading(context);
@ -720,6 +802,136 @@ class _LoginScreenState extends State<LoginScreen> {
).displayDialog(context);
}
bool _isAutoLoggingInDependent = false;
Future<bool> _tryDependentAutoLogin() async {
if (_isAutoLoggingInDependent) return true;
_isAutoLoggingInDependent = true;
try {
String isDepLoggedIn = await Utils.getStringFromPrefs("P_DEP_IS_LOGGED_IN");
if (isDepLoggedIn != "true") {
return false;
}
String savedNid = await Utils.getStringFromPrefs("P_DEP_NATIONAL_ID");
if (savedNid.isEmpty) {
return false;
}
String savedDob = await Utils.getStringFromPrefs("P_DEP_BIRTH_DATE");
String savedToken = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID");
String savedLoginToken = await Utils.getStringFromPrefs("P_DEP_LOGIN_TOKEN_ID");
String savedAuthToken = await Utils.getStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID");
String savedMobile = await Utils.getStringFromPrefs("P_DEP_MOBILE_NUMBER");
String savedName = await Utils.getStringFromPrefs("P_DEPENDENT_NAME");
String savedLoginType = await Utils.getStringFromPrefs("P_DEP_LOGIN_TYPE");
int loginType = int.tryParse(savedLoginType) ?? 1;
String effectiveToken = savedAuthToken.isNotEmpty ? savedAuthToken : (savedToken.isNotEmpty ? savedToken : savedLoginToken);
if (nationalId.text.isEmpty) {
nationalId.text = savedNid;
}
if (savedDob.isNotEmpty && dobController.text.isEmpty) {
DateTime? parsed = _parseBirthDate(savedDob);
if (parsed != null) {
selectedDob = parsed;
dobController.text = DateFormat('dd-MM-yyyy').format(parsed);
} else {
dobController.text = savedDob;
}
}
if (AppState().postParamsObject == null) {
AppState().setPostParamsInitConfig();
}
ApiConsts.selectedMemberType = MemberType.dependent;
ApiConsts.ISDependetLogin = true;
AppState().postParamsObject?.setTokenID = effectiveToken;
AppState().setAuthenticationTokenID = effectiveToken;
AppState().postParamsObject?.setLogInTokenID = savedLoginToken.isNotEmpty ? savedLoginToken : effectiveToken;
AppState().postParamsObject?.setUserName = savedNid;
AppState().postParamsObject?.setPUserName = savedNid;
if (savedMobile.isNotEmpty) {
AppState().postParamsObject?.setMobileNumer = savedMobile;
}
AppState().postParamsObject?.channel = 31;
AppState().postParamsObject?.versionID = 20;
AppState().setUserName = savedNid;
AppState().isAuthenticated = true;
AppState().setLogged = true;
if (savedName.isNotEmpty) {
AppState().setDependentName = savedName;
}
if (savedDob.isNotEmpty) {
AppState().setDependentDob = savedDob;
}
AppState().setDependentLoginType = loginType;
if (savedDob.isNotEmpty) {
String devToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (firebaseToken ?? "");
if (devToken.isEmpty && !AppState().getIsHuawei) {
if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty) {
devToken = AppState().getDeviceToken!;
} else {
devToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
}
}
String devType = Platform.isAndroid ? "android" : "ios";
try {
GenericResponseModel? depLoginResponse = await LoginApiClient().memberLoginDependent(nationalId: savedNid, birthDate: savedDob, deviceToken: devToken, deviceType: devType, context: context);
if (depLoginResponse?.errorMessage != null && depLoginResponse!.errorMessage!.isNotEmpty) {
if (Utils.isMazayaAccessRevoked(depLoginResponse.errorMessage)) {
if (mounted) {
Utils.hideLoading(context);
_navigateToMazayaAccessRevoked(savedName);
}
return true;
}
}
if (depLoginResponse != null) {
String? authToken = (depLoginResponse.authenticationTokenID != null && depLoginResponse.authenticationTokenID!.trim().isNotEmpty) ? depLoginResponse.authenticationTokenID!.trim() : null;
String? logInTokenID = depLoginResponse.logInTokenID;
String? mobileNumber = depLoginResponse.getWalaplusDepInfoModel?.pEmpMobileNumber ?? savedMobile;
if (authToken != null && authToken.isNotEmpty) {
if (mounted) {
Utils.hideLoading(context);
_completeDependentLogin(depLoginResponse, logInTokenID ?? authToken, mobileNumber, loginType: loginType, navigateToDashboard: true);
}
return true;
}
}
} catch (ex) {
if (Utils.isMazayaAccessRevoked(ex)) {
if (mounted) {
Utils.hideLoading(context);
_navigateToMazayaAccessRevoked(savedName);
}
return true;
}
if (kDebugMode) print("Error in memberLoginDependent auto-login: $ex");
}
}
if (effectiveToken.isNotEmpty && mounted) {
Utils.hideLoading(context);
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route<dynamic> route) => false);
return true;
}
} catch (e) {
if (kDebugMode) print("Error in _tryDependentAutoLogin: $e");
} finally {
_isAutoLoggingInDependent = false;
}
return false;
}
void _completeDependentLogin(GenericResponseModel? checkOtpResponse, String logInTokenID, String mobileNumber, {int loginType = 1, bool navigateToDashboard = true}) {
String effectiveToken =
(checkOtpResponse?.authenticationTokenID != null && checkOtpResponse!.authenticationTokenID!.trim().isNotEmpty)
@ -728,11 +940,14 @@ class _LoginScreenState extends State<LoginScreen> {
? checkOtpResponse.tokenID!
: ((checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) ? checkOtpResponse.logInTokenID! : logInTokenID));
String effectiveNationalId = nationalId.text.trim().isNotEmpty ? nationalId.text.trim() : (AppState().getUserName ?? "");
String effectiveDob = getFormattedBirthDate().isNotEmpty ? getFormattedBirthDate() : (AppState().dependentDob ?? "");
AppState().postParamsObject?.setTokenID = effectiveToken;
AppState().setAuthenticationTokenID = effectiveToken;
AppState().postParamsObject?.setLogInTokenID = (checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) ? checkOtpResponse.logInTokenID! : logInTokenID;
AppState().postParamsObject?.setUserName = nationalId.text.trim();
AppState().postParamsObject?.setPUserName = nationalId.text.trim();
AppState().postParamsObject?.setUserName = effectiveNationalId;
AppState().postParamsObject?.setPUserName = effectiveNationalId;
String resolvedMobile = mobileNumber.isNotEmpty ? mobileNumber : (checkOtpResponse?.getWalaplusDepInfoModel?.pEmpMobileNumber ?? "");
if (resolvedMobile.isNotEmpty) {
@ -740,9 +955,7 @@ class _LoginScreenState extends State<LoginScreen> {
Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", resolvedMobile);
}
AppState().postParamsObject?.channel = 31;
AppState().postParamsObject?.versionID = 20;
AppState().setUserName = nationalId.text.trim();
AppState().setUserName = effectiveNationalId;
AppState().isAuthenticated = true;
AppState().setLogged = true;
@ -756,20 +969,52 @@ class _LoginScreenState extends State<LoginScreen> {
}
AppState().setDependentLoginType = loginType;
AppState().setDependentDob = getFormattedBirthDate();
AppState().setDependentDob = effectiveDob;
Utils.saveStringFromPrefs("P_DEP_NATIONAL_ID", nationalId.text.trim());
Utils.saveStringFromPrefs("P_DEP_BIRTH_DATE", getFormattedBirthDate());
Utils.saveStringFromPrefs("P_DEP_IS_LOGGED_IN", "true");
Utils.saveStringFromPrefs("P_DEP_NATIONAL_ID", effectiveNationalId);
Utils.saveStringFromPrefs("P_DEP_BIRTH_DATE", effectiveDob);
Utils.saveStringFromPrefs("P_DEP_LOGIN_TOKEN_ID", AppState().postParamsObject?.logInTokenID ?? logInTokenID);
Utils.saveStringFromPrefs("P_DEP_TOKEN_ID", effectiveToken);
if (checkOtpResponse?.authenticationTokenID != null && checkOtpResponse!.authenticationTokenID!.trim().isNotEmpty) {
Utils.saveStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID", checkOtpResponse.authenticationTokenID!.trim());
}
Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken ?? "");
String tokenToSave = AppState().getIsHuawei
? AppState().getHuaweiPushToken
: ((firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null")
? firebaseToken!
: (AppState().getDeviceToken ?? ""));
if (tokenToSave.isNotEmpty && tokenToSave != "null") {
Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tokenToSave);
}
Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString());
if (navigateToDashboard && ApiConsts.ISDependetLogin) {
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (route) => false);
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route<dynamic> route) => false);
}
}
void _ensureDeviceTokenLoaded() async {
if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") {
if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") {
firebaseToken = AppState().getDeviceToken;
} else {
String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN");
if (saved.isNotEmpty && saved != "null") {
firebaseToken = saved;
AppState().setDeviceToken = saved;
} else if (!AppState().getIsHuawei) {
try {
await Firebase.initializeApp();
String? tok = await FirebaseMessaging.instance.getToken();
if (tok != null && tok.isNotEmpty) {
firebaseToken = tok;
AppState().setDeviceToken = tok;
Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok);
}
} catch (_) {}
}
}
}
}
@ -788,7 +1033,11 @@ class _LoginScreenState extends State<LoginScreen> {
// 13777
// Ab12345cd
}
if (isAppOpenBySystem!) checkFirebaseToken();
if (isAppOpenBySystem!) {
checkFirebaseToken();
} else {
_ensureDeviceTokenLoaded();
}
}
// username.text = "15444";
@ -809,10 +1058,12 @@ class _LoginScreenState extends State<LoginScreen> {
children: [
LocaleKeys.english.tr().toText14(color: AppState().isArabic(context) ? null : MyColors.textMixColor).onPress(() {
context.setLocale(const Locale("en", "US"));
_ensureDeviceTokenLoaded();
}),
Container(width: 1, color: MyColors.darkWhiteColor, height: 16, margin: const EdgeInsets.only(left: 10, right: 10)),
LocaleKeys.arabic.tr().toText14(color: !AppState().isArabic(context) ? null : MyColors.textMixColor).onPress(() {
context.setLocale(const Locale("ar", "SA"));
_ensureDeviceTokenLoaded();
}),
],
),
@ -847,7 +1098,7 @@ class _LoginScreenState extends State<LoginScreen> {
20.height,
_buildDependentCtaCard(context),
] else ...[
InputWidget(LocaleKeys.nationalId.tr(), "1234567890", nationalId, isInputTypeNum: true),
InputWidget(LocaleKeys.nationalIdOrIqamaNumber.tr(), "1234567890", nationalId, isInputTypeNum: true),
12.height,
GestureDetector(
behavior: HitTestBehavior.opaque,

@ -41,7 +41,7 @@ class _NewPasswordScreenState extends State<NewPasswordScreen> {
try {
var genericResponseModel = await LoginApiClient().changePasswordForget(AppState().getForgetPasswordTokenID ?? "", password.text, confirmPassword.text, userName);
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr());
Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true);
Navigator.pop(context);
} catch (ex) {
print(ex);

@ -60,10 +60,9 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
@override
Widget build(BuildContext context) {
if (ModalRoute.of(context)!.settings.arguments != null) {
mobileLoginInfoListModel ??= ModalRoute.of(context)!.settings.arguments as GetMobileLoginInfoListModel;
// String empName = AppState().isArabic(context) ? AppState().memberInformationList!.eMPLOYEEDISPLAYNAMEAr! : AppState().memberInformationList!.eMPLOYEEDISPLAYNAMEEn!;
String empName = mobileLoginInfoListModel!.employeeName!;
Object? args = ModalRoute.of(context)?.settings.arguments;
if (args is GetMobileLoginInfoListModel) {
mobileLoginInfoListModel ??= args;
}
return Scaffold(
@ -124,7 +123,7 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LocaleKeys.lastLoginDetails.tr().toText16(),
DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel!.editedOn ?? DateTime.now().toString()), false).toText12(),
mobileLoginInfoListModel != null ? DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? null), false).toText12() : const SizedBox(),
],
),
Row(
@ -132,9 +131,9 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LocaleKeys.verificationType.tr().toText10(color: MyColors.grey57Color),
getVerificationType(mobileLoginInfoListModel!.loginType!).toText12(),
getVerificationType(mobileLoginInfoListModel?.loginType).toText12(),
const Expanded(child: SizedBox()),
DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel!.editedOn!)).toText12(),
DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? DateTime.now().toString())).toText12(),
],
),
],
@ -206,7 +205,7 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
if (mounted) setState(() {});
}
String getVerificationType(int type) {
String getVerificationType(int? type) {
if (type == 1) {
return LocaleKeys.sms.tr();
} else if (type == 2) {
@ -222,9 +221,9 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
Future<bool> loginWithFaceIDAndBiometrics() async {
IOSAuthMessages iosStrings = const IOSAuthMessages(
cancelButton: 'cancel',
goToSettingsButton: 'settings',
goToSettingsDescription: 'Please set up your Touch ID.',
lockOut: 'Please reenable your Touch ID',
// goToSettingsButton: 'settings',
// goToSettingsDescription: 'Please set up your Touch ID.',
// lockOut: 'Please reenable your Touch ID',
);
// IOSAuthMessages iosStrings = const IOSAuthMessages(
@ -238,7 +237,9 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
try {
authenticated = await auth.authenticate(
localizedReason: 'Scan your fingerprint to authenticate',
options: const AuthenticationOptions(useErrorDialogs: true, stickyAuth: true, biometricOnly: true),
// options: const AuthenticationOptions(useErrorDialogs: true, stickyAuth: true, biometricOnly: true),
persistAcrossBackgrounding: false, // Replaces stickyAuth
biometricOnly: true,
authMessages: [iosStrings, const AndroidAuthMessages()],
);
} on PlatformException catch (e) {
@ -278,7 +279,7 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
if (!authenticateWithFaceAndTouchID) {
return;
} else {
if (mobileLoginInfoListModel!.loginType == 3 || mobileLoginInfoListModel!.loginType == 4) {
if (mobileLoginInfoListModel?.loginType == 3 || mobileLoginInfoListModel?.loginType == 4) {
// bool authenticateWithFaceAndTouchID = await loginWithFaceIDAndBiometrics();
// if (!authenticateWithFaceAndTouchID) {
// return;
@ -388,7 +389,12 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
Future<void> performDirectApiCall(String _title, String _icon, int _flag, String value, TextEditingController? _pinPutController, {bool isDirectLogin = false}) async {
try {
GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName);
GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName,_flag.toString());
String devToken = AppState().getIsHuawei
? AppState().getHuaweiPushToken
: ((mobileLoginInfoListModel?.deviceToken != null && mobileLoginInfoListModel!.deviceToken!.isNotEmpty && mobileLoginInfoListModel!.deviceToken != "null")
? mobileLoginInfoListModel!.deviceToken!
: (AppState().getDeviceToken ?? ""));
GenericResponseModel? genericResponseModel1 = await LoginApiClient().insertMobileLoginInfoNEW(
AppState().memberLoginList?.pEMAILADDRESS ?? "",
genericResponseModel?.pSESSIONID ?? 0,
@ -396,7 +402,7 @@ class _VerifyLastLoginScreenState extends State<VerifyLastLoginScreen> {
_flag,
AppState().memberLoginList?.pMOBILENUMBER ?? "",
AppState().getUserName!,
AppState().getIsHuawei ? AppState().getHuaweiPushToken : mobileLoginInfoListModel!.deviceToken!,
devToken,
Platform.isAndroid ? "android" : "ios",
memberType: MemberType.employee.value,
);

@ -55,7 +55,18 @@ class _VerifyLoginScreenState extends State<VerifyLoginScreen> {
@override
Widget build(BuildContext context) {
firebaseToken ??= ModalRoute.of(context)!.settings.arguments as String;
Object? rawArg = ModalRoute.of(context)?.settings.arguments;
if (rawArg != null) {
String argStr = rawArg.toString().trim();
if (argStr.isNotEmpty && argStr != "null") {
firebaseToken ??= argStr;
}
}
if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") {
if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") {
firebaseToken = AppState().getDeviceToken;
}
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
@ -515,11 +526,11 @@ class _VerifyLoginScreenState extends State<VerifyLoginScreen> {
Future<bool> loginWithFaceIDAndBiometrics() async {
IOSAuthMessages iosStrings =
const IOSAuthMessages(cancelButton: 'cancel', goToSettingsButton: 'settings', goToSettingsDescription: 'Please set up your Touch ID.', lockOut: 'Please reenable your Touch ID');
const IOSAuthMessages(cancelButton: 'cancel',);
bool authenticated = false;
try {
authenticated = await auth.authenticate(localizedReason: 'Scan your fingerprint to authenticate',
options: const AuthenticationOptions(useErrorDialogs: true, stickyAuth: true, biometricOnly: true,), authMessages: [
authMessages: [
iosStrings,
const AndroidAuthMessages(),
],);
@ -641,7 +652,12 @@ class _VerifyLoginScreenState extends State<VerifyLoginScreen> {
(value, TextEditingController _pinPutController) async {
Utils.showLoading(context);
try {
GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName);
GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName, _flag.toString());
String devToken = AppState().getIsHuawei
? AppState().getHuaweiPushToken
: ((firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null")
? firebaseToken!
: (AppState().getDeviceToken ?? ""));
GenericResponseModel? genericResponseModel1 = await LoginApiClient().insertMobileLoginInfoNEW(
AppState().memberLoginList?.pEMAILADDRESS ?? "",
genericResponseModel?.pSESSIONID ?? 0,
@ -649,7 +665,7 @@ class _VerifyLoginScreenState extends State<VerifyLoginScreen> {
_flag,
AppState().memberLoginList?.pMOBILENUMBER ?? "",
AppState().getUserName!,
AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken!,
devToken,
Platform.isAndroid ? "android" : "ios",
memberType: MemberType.employee.value);
if (genericResponseModel?.errorMessage != null) {

@ -23,7 +23,7 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart';
import 'package:mohem_flutter_app/ui/marathon/widgets/question_card_builder.dart';
import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:provider/provider.dart';
class MarathonScreen extends StatelessWidget {
@ -299,16 +299,16 @@ class MarathonScreen extends StatelessWidget {
provider.getMarathonDetailsFromApi();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
} else {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
provider.getMarathonDetailsFromApi();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
provider.getMarathonDetailsFromApi();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
}
},
@ -318,15 +318,15 @@ class MarathonScreen extends StatelessWidget {
provider.getMarathonDetailsFromApi();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
} else {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
}
},
@ -390,15 +390,15 @@ class MarathonScreen extends StatelessWidget {
),
),
onWillPop: () {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
return Future<bool>.value(false);
},

@ -10,7 +10,7 @@ import 'package:mohem_flutter_app/extensions/widget_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/ui/marathon/marathon_provider.dart';
import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:provider/provider.dart';
class MarathonWaitingScreen extends StatelessWidget {
@ -21,15 +21,15 @@ class MarathonWaitingScreen extends StatelessWidget {
MarathonProvider provider = context.watch<MarathonProvider>();
return WillPopScope(
onWillPop: () {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
return Future<bool>.value(false);
},
@ -38,27 +38,27 @@ class MarathonWaitingScreen extends StatelessWidget {
context,
title: LocaleKeys.brainMarathon.tr(),
onHomeTapped: () {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
},
onBackTapped: () {
showDialog(
context: context,
builder: (BuildContext context) => ConfirmDialog(
message: LocaleKeys.youWantToLeaveMarathon.tr(),
onTap: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.youWantToLeaveMarathon.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
provider.resetValues();
Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard));
},
);
},
),

@ -30,18 +30,21 @@ class QuestionCard extends StatelessWidget {
width: double.infinity,
child: AppinioSwiper(
duration: const Duration(milliseconds: 400),
padding: EdgeInsets.zero,
swipeOptions: const SwipeOptions.all(),
isDisabled: true,
controller: provider.swiperController,
unswipe: (int index, AppinioSwiperDirection direction) {},
onSwipe: (int index, AppinioSwiperDirection direction) {},
cards: provider.cardContentList,
cardCount: provider.cardContentList.length,
cardBuilder: (BuildContext context, int index) {
return provider.cardContentList[index];
},
),
),
);
}
}
class CardContent extends StatelessWidget {
const CardContent({Key? key}) : super(key: key);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,390 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/generic_response_model.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart';
import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart';
import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
// Mazaya Saved tab - full-screen version of what was FavoriteMerchantsBottomSheet (a modal
// bottom sheet). Same fetch/search/pagination/toggle-favorite logic, now its own pushed route
// with the shared MazayaBottomNav instead of a dismissible sheet - matches Discover's "own
// route, fetches fresh on mount" pattern.
class SavedRetailersScreen extends StatefulWidget {
const SavedRetailersScreen({Key? key}) : super(key: key);
@override
State<SavedRetailersScreen> createState() => _SavedRetailersScreenState();
}
class _SavedRetailersScreenState extends State<SavedRetailersScreen> {
List<WalaPlusRetailerModel> favoriteRetailers = <WalaPlusRetailerModel>[];
bool isLoading = true;
bool isLoadingMore = false;
bool hasMore = true;
bool _isFetchingPage = false;
int currentPage = 1;
int totalPages = 1;
final int pageSize = 10;
String searchQuery = "";
Timer? _debounceTimer;
final TextEditingController _searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) fetchFavorites();
});
}
@override
void dispose() {
_debounceTimer?.cancel();
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _scrollListener() {
if (!_scrollController.hasClients) return;
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
if (currentScroll >= maxScroll - 35) {
if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) {
fetchNextPage();
}
}
}
String? _getUserReferenceForFavorites() {
String ref = OffersAndDiscountsApiClient().resolveUserReference();
if (ref.trim().isNotEmpty) return ref.trim();
String? userName = AppState().getUserName;
if (userName != null && userName.trim().isNotEmpty) return userName.trim();
return null;
}
Future<void> fetchFavorites({String? query}) async {
if (query != null) searchQuery = query;
setState(() {
isLoading = true;
currentPage = 1;
hasMore = true;
});
try {
String? queryParam =
searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
String? userRef = _getUserReferenceForFavorites();
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
.getWalaPlusRetailers(
categoryId: "",
page: 1,
perPage: pageSize,
offerType: null,
query: queryParam,
sortBy: null,
radius: null,
minDiscount: null,
context: null,
userReference: userRef,
);
favoriteRetailers = response?.retailers ?? <WalaPlusRetailerModel>[];
for (WalaPlusRetailerModel r in favoriteRetailers) {
r.isFavorite = true;
}
currentPage = 1;
totalPages = response?.pagesCount ?? 1;
hasMore =
currentPage < totalPages &&
(response?.retailers?.isNotEmpty ?? false);
} catch (e) {
debugPrint("MazayaSavedScreen: error fetching favorites: $e");
favoriteRetailers = <WalaPlusRetailerModel>[];
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Future<void> fetchNextPage() async {
if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return;
_isFetchingPage = true;
setState(() => isLoadingMore = true);
try {
int nextPage = currentPage + 1;
String? queryParam =
searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
String? userRef = _getUserReferenceForFavorites();
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient()
.getWalaPlusRetailers(
categoryId: "",
page: nextPage,
perPage: pageSize,
offerType: null,
query: queryParam,
sortBy: null,
radius: null,
minDiscount: null,
context: null,
userReference: userRef,
);
List<WalaPlusRetailerModel> newRetailers =
response?.retailers ?? <WalaPlusRetailerModel>[];
if (newRetailers.isNotEmpty) {
for (WalaPlusRetailerModel r in newRetailers) {
r.isFavorite = true;
}
currentPage = nextPage;
totalPages = response?.pagesCount ?? totalPages;
hasMore = currentPage < totalPages;
favoriteRetailers.addAll(newRetailers);
} else {
hasMore = false;
}
} catch (e) {
debugPrint("MazayaSavedScreen: error fetching next page: $e");
} finally {
_isFetchingPage = false;
if (mounted) setState(() => isLoadingMore = false);
}
}
void _onSearchChanged(String query) {
_debounceTimer?.cancel();
searchQuery = query;
if (query.trim().isEmpty) {
fetchFavorites(query: "");
} else {
_debounceTimer = Timer(const Duration(milliseconds: 600), () {
fetchFavorites(query: query.trim());
});
}
}
Future<void> _toggleFavorite(WalaPlusRetailerModel item) async {
String retailerId = item.id ?? "";
if (retailerId.isEmpty) return;
bool currentFav = item.isFavorite ?? true;
bool targetFav = !currentFav;
setState(() {
item.isFavorite = targetFav;
if (!targetFav) {
favoriteRetailers.removeWhere(
(WalaPlusRetailerModel r) => r.id == item.id,
);
}
});
Utils.showLoading(context);
try {
GenericResponseModel? response;
String? userRef = _getUserReferenceForFavorites();
if (targetFav) {
response = await OffersAndDiscountsApiClient().addWalaPlusFavorite(
retailerId: retailerId,
context: context,
userReference: userRef,
);
} else {
response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite(
retailerId: retailerId,
context: context,
userReference: userRef,
);
}
Utils.hideLoading(context);
if (response != null && response.errorMessage == null) {
bool isAr = AppState().isArabic(context);
String msg =
isAr
? (targetFav
? LocaleKeys.favoriteAddedSuccess.tr()
: LocaleKeys.favoriteRemovedSuccess.tr())
: (response.wpFavoritesAddRemoveMessage ??
(targetFav
? LocaleKeys.favoriteAddedSuccess.tr()
: LocaleKeys.favoriteRemovedSuccess.tr()));
Utils.showToast(msg, isSuccess: true);
} else {
Utils.showToast(
response?.errorEndUserMessage ??
response?.errorMessage ??
LocaleKeys.favoriteError.tr(),
);
await fetchFavorites(query: _searchController.text);
}
} catch (e) {
Utils.hideLoading(context);
debugPrint("MazayaSavedScreen: error toggling favorite: $e");
Utils.showToast(LocaleKeys.favoriteError.tr());
await fetchFavorites(query: _searchController.text);
}
}
void _openRetailer(WalaPlusRetailerModel item) async {
await Navigator.pushNamed(
context,
AppRoutes.retailerDetailsScreen,
arguments: item,
);
if (mounted) {
await fetchFavorites(query: _searchController.text);
}
}
@override
Widget build(BuildContext context) {
bool isAr = AppState().isArabic(context);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBarWidget(
context,
title: LocaleKeys.favoriteOffers.tr(),
showHomeButton: false,
// Reached from the Mazaya bottom nav - same as Discover, no back button for a
// dependent-login session since this is one of their root tabs, not a pushed screen.
showBackButton: false,
),
bottomNavigationBar: const MazayaBottomNav(currentTab: MazayaTab.saved),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container(
width: double.infinity,
height: 70,
padding: const EdgeInsets.only(
top: 14,
right: 16,
bottom: 14,
left: 16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0),
boxShadow: <BoxShadow>[
BoxShadow(
color: const Color(0xFF000000).withOpacity(0.0314),
offset: const Offset(0, 2),
blurRadius: 5,
spreadRadius: 0,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LocaleKeys.search.tr().toText12(
isBold: true,
color: const Color(0xFF2A333C),
),
const SizedBox(height: 2),
TextField(
controller: _searchController,
onChanged: _onSearchChanged,
onSubmitted: (String val) {
_debounceTimer?.cancel();
fetchFavorites(query: val.trim());
},
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
hintText: LocaleKeys.searchItems.tr(),
hintStyle: const TextStyle(
color: Color(0xFFACAFB1),
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.2,
),
border: InputBorder.none,
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: _searchController,
builder: (
BuildContext context,
TextEditingValue value,
Widget? child,
) {
if (value.text.isEmpty)
return const SizedBox.shrink();
return InkWell(
child: const Icon(
Icons.clear,
size: 18,
color: Color(0xFF848486),
),
onTap: () {
_searchController.clear();
_onSearchChanged("");
},
);
},
),
suffixIconConstraints: const BoxConstraints(
maxWidth: 24,
maxHeight: 28,
),
),
style: const TextStyle(
color: Color(0xFF2A333C),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => fetchFavorites(query: _searchController.text),
color: MyColors.textMixColor,
child: SingleChildScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Column(
children: <Widget>[
WalaPlusRetailerGrid(
isLoading: isLoading,
retailers: favoriteRetailers,
isAr: isAr,
isLoadingMore: isLoadingMore,
onTapRetailer: _openRetailer,
onFavoriteTap: _toggleFavorite,
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
);
}
}

@ -0,0 +1,130 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/ui/mazaya/mazaya_discover_screen.dart';
import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart';
import 'package:mohem_flutter_app/ui/profile/family_members.dart';
enum MazayaTab { discover, saved, myFamily }
// Shared bottom nav for the Mazaya module (Discover / Saved / My Family). Each tab is its own
// screen, swapped in with zero-duration transition (see _onTabTapped) so switching tabs feels
// instant rather than like a new screen opening - not an IndexedStack shell, since each tab still
// needs to reload its own data fresh on every switch, not preserve state across tabs.
class MazayaBottomNav extends StatelessWidget {
final MazayaTab currentTab;
const MazayaBottomNav({Key? key, required this.currentTab}) : super(key: key);
static const Color _activeColor = Color(0xFF147D6B);
static const Color _inactiveColor = Color(0xFF78817F);
void _onTabTapped(BuildContext context, MazayaTab tab) {
if (tab == currentTab) return;
Widget targetScreen;
RouteSettings? settings;
switch (tab) {
case MazayaTab.discover:
targetScreen = const MazayaDiscoverScreen();
break;
case MazayaTab.saved:
targetScreen = const SavedRetailersScreen();
break;
case MazayaTab.myFamily:
targetScreen = const FamilyMembers();
settings = const RouteSettings(
arguments: <String, dynamic>{'fromMazaya': true},
);
break;
}
Navigator.pushReplacement(
context,
PageRouteBuilder<void>(
settings: settings,
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) => targetScreen,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
}
Widget _buildItem(
BuildContext context, {
required MazayaTab tab,
required String svgAsset,
required String label,
}) {
bool isActive = tab == currentTab;
return Expanded(
child: InkWell(
onTap: () => _onTabTapped(context, tab),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SvgPicture.asset(
svgAsset,
width: 22,
height: 22,
colorFilter: ColorFilter.mode(
isActive ? _activeColor : _inactiveColor,
BlendMode.srcIn,
),
),
const SizedBox(height: 5),
label.toText11(
weight: isActive ? FontWeight.w600 : FontWeight.w400,
color: isActive ? _activeColor : _inactiveColor,
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Color(0xFFE8EAE4))),
),
padding: const EdgeInsets.only(top: 13, bottom: 9),
child: Row(
children: <Widget>[
_buildItem(
context,
tab: MazayaTab.discover,
svgAsset: "assets/icons/mazaya_compass.svg",
label: LocaleKeys.discover.tr(),
),
_buildItem(
context,
tab: MazayaTab.saved,
svgAsset: "assets/icons/mazaya_heart_outline.svg",
label: LocaleKeys.saved.tr(),
),
// My Family lets the account holder manage their dependents' Mazaya access - not
// applicable (and not shown) when a dependent is the one logged in.
if (!ApiConsts.isDependetLogin)
_buildItem(
context,
tab: MazayaTab.myFamily,
svgAsset: "assets/icons/mazaya_user_round.svg",
label: LocaleKeys.myFamily.tr(),
),
],
),
),
);
}
}

@ -0,0 +1,196 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart';
import 'package:shimmer/shimmer.dart';
// Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay
// visually/behaviorally identical - change the sheet design here only. Fetched fresh from
// getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan.
void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged<WalaPlusCityModel> onCitySelected}) {
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (BuildContext sheetContext) {
return _MazayaCitySelectionSheet(selectedCityId: selectedCityId, onCitySelected: onCitySelected);
},
);
}
class _MazayaCitySelectionSheet extends StatefulWidget {
final int? selectedCityId;
final ValueChanged<WalaPlusCityModel> onCitySelected;
const _MazayaCitySelectionSheet({required this.selectedCityId, required this.onCitySelected});
@override
State<_MazayaCitySelectionSheet> createState() => _MazayaCitySelectionSheetState();
}
class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> {
bool _isLoading = true;
List<WalaPlusCityModel> _cities = <WalaPlusCityModel>[];
List<WalaPlusCityModel> _filteredCities = <WalaPlusCityModel>[];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_fetchCities();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _fetchCities() async {
try {
List<WalaPlusCityModel> cities = await OffersAndDiscountsApiClient().getWalaPlusCities();
// "All Cities" always first - it's the default selection when nothing else was picked yet.
cities.sort((a, b) {
if (a.isAllCities == b.isAllCities) return 0;
return a.isAllCities ? -1 : 1;
});
if (mounted) {
setState(() {
_cities = cities;
_filteredCities = cities;
_isLoading = false;
});
}
} catch (e) {
debugPrint("MazayaCitySelector: error fetching cities: $e");
if (mounted) setState(() => _isLoading = false);
}
}
// Local/client-side only - the already-fetched list is small enough that there's no need to
// re-hit getWalaPlusCities per keystroke.
void _onSearchChanged(String query) {
String q = query.trim().toLowerCase();
setState(() {
_filteredCities =
q.isEmpty
? _cities
: _cities.where((WalaPlusCityModel c) {
return (c.cityNameEn ?? '').toLowerCase().contains(q) || (c.cityNameAr ?? '').toLowerCase().contains(q);
}).toList();
});
}
@override
Widget build(BuildContext context) {
bool isAr = AppState().isArabic(context);
// Fixed height regardless of result count - a search that narrows the list down to one or
// two cities (or the full unfiltered list) should never make the sheet itself grow/shrink,
// only the list inside it scrolls.
return Container(
height: MediaQuery.of(context).size.height * 0.75,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
child: SafeArea(
top: false,
child: Column(
children: [
Container(width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2))),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)),
InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(16),
child: const Padding(padding: EdgeInsets.all(4), child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B))),
),
],
),
const SizedBox(height: 12),
_buildSearchField(),
const SizedBox(height: 12),
Expanded(child: _isLoading ? _buildShimmer() : _buildList(isAr)),
],
),
),
);
}
// Same design as MazayaDiscoverScreen's own search field - filters the already-fetched city
// list locally as the user types (see _onSearchChanged).
Widget _buildSearchField() {
return Container(
height: 47,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFE8EAE4)), borderRadius: BorderRadius.circular(12)),
child: Row(
children: <Widget>[
SvgPicture.asset("assets/icons/mazaya_search.svg", width: 18, height: 18, colorFilter: const ColorFilter.mode(Color(0xFF78817F), BlendMode.srcIn)),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: _searchController,
onChanged: _onSearchChanged,
decoration: InputDecoration(border: InputBorder.none, isDense: true, hintText: LocaleKeys.searchCities.tr(), hintStyle: const TextStyle(color: Color(0xFF78817F), fontSize: 12)),
style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)),
),
),
],
),
);
}
// Matches ListTile's own default one-line height (56) and contentPadding (16 horizontal) so
// there's no layout jump between the shimmer and the real list underneath it.
static const double _rowHeight = 56;
Widget _buildShimmer() {
return Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder:
(BuildContext _, int __) => Container(
height: _rowHeight,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: AlignmentDirectional.centerStart,
child: Container(height: 28, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8))),
),
),
);
}
Widget _buildList(bool isAr) {
if (_filteredCities.isEmpty) {
return Center(child: LocaleKeys.noResultsFound.tr().toText13(color: const Color(0xFF7A8180)));
}
// ListTile paints its background/ink splashes on the nearest Material ancestor - without
// this, the opaque DecoratedBox the whole sheet sits in (see build()) hides them.
return Material(
type: MaterialType.transparency,
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _filteredCities.length,
itemBuilder: (BuildContext _, int index) {
WalaPlusCityModel city = _filteredCities[index];
bool isSelected = widget.selectedCityId != null ? city.cityId == widget.selectedCityId : city.isAllCities;
return ListTile(
title: city.getDisplayName(isAr).toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)),
trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null,
onTap: () {
widget.onCitySelected(city);
Navigator.pop(context);
},
);
},
),
);
}
}

@ -0,0 +1,124 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mohem_flutter_app/extensions/int_extensions.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/main.dart';
import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart';
import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart';
import 'package:shimmer/shimmer.dart';
class WalaPlusRetailerGrid extends StatelessWidget {
final bool isLoading;
final List<WalaPlusRetailerModel> retailers;
final bool isAr;
final void Function(WalaPlusRetailerModel item) onTapRetailer;
final void Function(WalaPlusRetailerModel item) onFavoriteTap;
final bool isLoadingMore;
final int shimmerItemCount;
final double? mainAxisExtent;
const WalaPlusRetailerGrid({
Key? key,
required this.isLoading,
required this.retailers,
required this.isAr,
required this.onTapRetailer,
required this.onFavoriteTap,
this.isLoadingMore = false,
this.shimmerItemCount = 4,
this.mainAxisExtent,
}) : super(key: key);
bool get _hasAnyChips {
for (WalaPlusRetailerModel r in retailers) {
if (r.normalizedTypes.isNotEmpty ||
r.displayCity.trim().isNotEmpty ||
r.displayUsage.trim().isNotEmpty) {
return true;
}
}
return false;
}
double get _effectiveMainAxisExtent {
if (mainAxisExtent != null) return mainAxisExtent!;
if (retailers.isNotEmpty && !_hasAnyChips) return 190;
return 235;
}
SliverGridDelegateWithFixedCrossAxisCount get _gridDelegate =>
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isTablet ? 4 : 2,
mainAxisExtent: _effectiveMainAxisExtent,
crossAxisSpacing: 12,
mainAxisSpacing: 16,
);
@override
Widget build(BuildContext context) {
if (isLoading) {
return Shimmer.fromColors(
baseColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFFF8FAFC),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: _gridDelegate,
itemCount: shimmerItemCount,
itemBuilder: (BuildContext _, int __) => _buildShimmerCard(),
),
);
}
if (retailers.isEmpty) {
return Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B))));
}
return Column(
children: <Widget>[
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: _gridDelegate,
itemCount: retailers.length,
itemBuilder: (BuildContext context, int index) {
return WalaPlusRetailerCard(item: retailers[index], isAr: isAr, onTap: () => onTapRetailer(retailers[index]), onFavoriteTap: () => onFavoriteTap(retailers[index]));
},
),
if (isLoadingMore) ...<Widget>[
16.height,
Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))),
],
],
);
}
Widget _buildShimmerCard() {
bool showChips = _effectiveMainAxisExtent > 200;
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(height: 110, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))),
const SizedBox(height: 8),
Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 6),
Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
if (showChips) ...<Widget>[
const SizedBox(height: 6),
Row(
children: <Widget>[
Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
const SizedBox(width: 5),
Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
],
),
],
],
),
);
}
}

@ -21,7 +21,7 @@ import 'package:mohem_flutter_app/widgets/app_bar_widget.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/button/simple_button.dart';
import 'package:mohem_flutter_app/widgets/circular_avatar.dart';
import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/image_picker.dart';
import 'package:mohem_flutter_app/widgets/input_widget.dart';
@ -145,7 +145,7 @@ class _RequestSubmitScreenState extends State<RequestSubmitScreen> {
);
} else {}
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.yourRequestHasBeenSubmittedForApprovals.tr(), longDuration: true);
Utils.showToast(LocaleKeys.yourRequestHasBeenSubmittedForApprovals.tr(), longDuration: true, isSuccess: true);
AppState().cancelRequestTrancsection = false;
if (params?.popNavigateToSpecificRoute == null) {
@ -216,15 +216,14 @@ class _RequestSubmitScreenState extends State<RequestSubmitScreen> {
return;
}
showDialog(
context: context,
builder: (cxt) => ConfirmDialog(
message: LocaleKeys.areYouSureYouWantToSubmit.tr(),
onTap: () {
Navigator.pop(context);
submitRequest();
},
),
ConfirmBottomSheet.show(
context,
message: LocaleKeys.areYouSureYouWantToSubmit.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
submitRequest();
},
);
},
).insideContainer,

@ -18,6 +18,7 @@ import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.
import 'package:mohem_flutter_app/models/profile_menu.model.dart';
import 'package:mohem_flutter_app/models/worklist/get_favorite_replacements_model.dart';
import 'package:mohem_flutter_app/models/worklist/replacement_list_model.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/circular_avatar.dart';
import 'package:url_launcher/url_launcher.dart';
@ -217,19 +218,19 @@ class _EmployeeDetailsState extends State<EmployeeDetails> {
}
void showFavoriteAlertDialog(BuildContext context) {
Widget cancelButton = TextButton(
child: Text(
LocaleKeys.cancel.tr(),
),
onPressed: () {
Navigator.pop(context);
},
);
Widget continueButton = TextButton(
child: Text(
LocaleKeys.ok.tr(),
),
onPressed: () {
ConfirmBottomSheet.show(
context,
message: LocaleKeys.addFavoriteList.tr(namedArgs: <String, String>{'name': '${getEmployeeSubordinates!.eMPLOYEENAME}'}),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
avatar: getEmployeeSubordinates!.eMPLOYEEIMAGE == null
? SvgPicture.asset("assets/images/user.svg", height: 68, width: 68)
: CircleAvatar(
radius: 68,
backgroundImage: MemoryImage(Utils.dataFromBase64String(getEmployeeSubordinates!.eMPLOYEEIMAGE!)),
backgroundColor: Colors.black,
),
onConfirm: () {
fetchChangeFav(
email: getEmployeeSubordinates?.eMPLOYEEEMAILADDRESS ?? "",
employeName: getEmployeeSubordinates!.eMPLOYEENAME ?? "",
@ -238,54 +239,6 @@ class _EmployeeDetailsState extends State<EmployeeDetails> {
isFav: true,
);
setState(() {});
Navigator.pop(context);
},
);
AlertDialog alert = AlertDialog(
title: Text(
LocaleKeys.confirm.tr(),
),
content: Container(
height: 150,
child: Column(
children: [
Text(LocaleKeys.addFavoriteList).tr(namedArgs: {'name': '${getEmployeeSubordinates!.eMPLOYEENAME}'}),
getEmployeeSubordinates!.eMPLOYEEIMAGE == null
? SvgPicture.asset(
"assets/images/user.svg",
height: 68,
width: 68,
)
: CircleAvatar(
radius: 68,
backgroundImage: MemoryImage(Utils.dataFromBase64String(getEmployeeSubordinates!.eMPLOYEEIMAGE!)),
backgroundColor: Colors.black,
),
// Image.memory(
// Utils.getPostBytes(
// getEmployeeSubordinates!.eMPLOYEEIMAGE ?? "",
// ),
// errorBuilder: (BuildContext context, error, stackTrace) {
// return SvgPicture.asset(
// "assets/images/user.svg", height: 50, width: 50,
// );
// },
// width: 50,
// height: 50,
// fit: BoxFit.cover,
// ).circle(50).paddingOnly(top: 21),
],
),
),
actions: [
cancelButton,
continueButton,
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

@ -97,8 +97,8 @@ class _SubordinateLeaveState extends State<SubordinateLeave> {
separatorBuilder: (BuildContext cxt, int index) => 12.height,
itemCount: getSubordinatesLeavesTotalList.length,
itemBuilder: (BuildContext context, int index) {
var diffDays = DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND!)
.difference(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART!))
var diffDays = DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND)
.difference(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART))
.inDays + 1;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -109,8 +109,8 @@ class _SubordinateLeaveState extends State<SubordinateLeave> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
getSubordinatesLeavesTotalList[index].eMPLOYEENAME!.toText16(),
ItemDetailView(LocaleKeys.from.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART!), "MMM dd yyyy")),
ItemDetailView(LocaleKeys.to.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND!), "MMM dd yyyy")),
ItemDetailView(LocaleKeys.from.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART), "MMM dd yyyy")),
ItemDetailView(LocaleKeys.to.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND), "MMM dd yyyy")),
ItemDetailView(LocaleKeys.numberDays.tr(), diffDays.toString()),
],
).expanded

@ -866,21 +866,21 @@ class _AddDependentVerifyScreenState extends State<AddDependentVerifyScreen> {
}
}
// 14. Ticket / Education / Medical Insurance Eligibility
else if (prompt.contains('ticket') || colName.contains('TICKET') ||
prompt.contains('education') || colName.contains('EDU') ||
prompt.contains('insurance') || colName.contains('MED')) {
if (eServices != null && eServices.isNotEmpty) {
for (var opt in eServices) {
String valCol = (opt.vALUECOLUMNNAME ?? '').toLowerCase();
String idCol = (opt.iDCOLUMNNAME ?? '').toUpperCase();
if (idCol == 'Y' || valCol.contains('eligible') || valCol.contains('مؤهل') || valCol.contains('نعم') || valCol.contains('yes')) {
details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME;
details.vARCHAR2VALUE = opt.iDCOLUMNNAME;
break;
}
}
}
}
// else if (prompt.contains('ticket') || colName.contains('TICKET') ||
// prompt.contains('education') || colName.contains('EDU') ||
// prompt.contains('insurance') || colName.contains('MED')) {
// if (eServices != null && eServices.isNotEmpty) {
// for (var opt in eServices) {
// String valCol = (opt.vALUECOLUMNNAME ?? '').toLowerCase();
// String idCol = (opt.iDCOLUMNNAME ?? '').toUpperCase();
// if (idCol == '' || valCol.contains('eligible') || valCol.contains('مؤهل') || valCol.contains('نعم') || valCol.contains('yes')) {
// details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME;
// details.vARCHAR2VALUE = opt.iDCOLUMNNAME;
// break;
// }
// }
//}
// }
// 15. Relationship Start Date
else if (prompt.contains('start') || colName.contains('DATE_START') || colName.contains('START_DATE')) {
String startVal = ymdDob.isNotEmpty ? ymdDob : DateFormat('yyyy-MM-dd').format(DateTime.now());
@ -1827,7 +1827,7 @@ class _AddDependentVerifyScreenState extends State<AddDependentVerifyScreen> {
Utils.hideLoading(context);
Navigator.pop(context);
} else {
Utils.showToast(LocaleKeys.dependentAddedSuccessfully.tr());
Utils.showToast(LocaleKeys.dependentAddedSuccessfully.tr(), isSuccess: true);
Navigator.pop(context);
}
} catch (ex) {
@ -1858,7 +1858,7 @@ class _AddDependentVerifyScreenState extends State<AddDependentVerifyScreen> {
onHomeTapped: () {
Navigator.pushNamedAndRemoveUntil(
context,
ApiConsts.ISDependetLogin ? AppRoutes.dashboardScreenForDependet : AppRoutes.dashboard,
ApiConsts.ISDependetLogin ? AppRoutes.mazayaDiscover : AppRoutes.dashboard,
(Route<dynamic> route) => false,
);
},

@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:mohem_flutter_app/api/profile_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/classes/consts.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
@ -52,13 +53,22 @@ class _ShieldIconPainter extends CustomPainter {
checkPath.lineTo(w * 0.66, h * 0.39);
canvas.drawPath(checkPath, paint);
} else {
canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint);
canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint);
canvas.drawLine(
Offset(w * 0.38, h * 0.38),
Offset(w * 0.62, h * 0.62),
paint,
);
canvas.drawLine(
Offset(w * 0.62, h * 0.38),
Offset(w * 0.38, h * 0.62),
paint,
);
}
}
@override
bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted;
bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) =>
color != oldDelegate.color || isGranted != oldDelegate.isGranted;
}
class DependentDetailsScreen extends StatefulWidget {
@ -71,7 +81,8 @@ class DependentDetailsScreen extends StatefulWidget {
// equivalent local state directly instead of needing this.
final bool? recentAction;
const DependentDetailsScreen({Key? key, this.contact, this.recentAction}) : super(key: key);
const DependentDetailsScreen({Key? key, this.contact, this.recentAction})
: super(key: key);
@override
_DependentDetailsScreenState createState() => _DependentDetailsScreenState();
@ -107,7 +118,8 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
bool get _isAccessAllowed {
if (_contact == null) return false;
String? flag = _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString();
String? flag =
_contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString();
return flag?.trim().toUpperCase() == 'Y';
}
@ -118,17 +130,29 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
String _getInitials(String? name) {
if (name == null || name.trim().isEmpty) return "D";
List<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList();
List<String> parts =
name
.trim()
.split(RegExp(r'\s+'))
.where((String p) => p.isNotEmpty)
.toList();
if (parts.isEmpty) return "D";
if (parts.length == 1) {
return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase();
return parts.first.length >= 2
? parts.first.substring(0, 2).toUpperCase()
: parts.first.toUpperCase();
}
return "${parts.first[0]}${parts.last[0]}".toUpperCase();
}
String _getFirstName(String? name) {
if (name == null || name.trim().isEmpty) return "";
List<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList();
List<String> parts =
name
.trim()
.split(RegExp(r'\s+'))
.where((String p) => p.isNotEmpty)
.toList();
if (parts.isEmpty) return "";
return parts.first;
}
@ -142,7 +166,10 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
if (match != null) {
try {
int millis = int.parse(match.group(1)!);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(
millis,
isUtc: true,
);
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
@ -150,7 +177,10 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) {
try {
int millis = int.parse(s);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(
millis,
isUtc: true,
);
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
@ -158,10 +188,34 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
DateTime? dt = DateTime.tryParse(s);
if (dt != null) return DateTime(dt.year, dt.month, dt.day);
List<String> formats = <String>['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy'];
// DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip
// that clock-time suffix first, since none of the date-only patterns below can match it
// with it still attached (parseStrict requires the whole string to match the pattern).
String dateOnly =
s
.replaceFirst(
RegExp(
r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$',
caseSensitive: false,
),
'',
)
.trim();
// MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an
// ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June.
List<String> formats = <String>[
'yyyy-MM-dd',
'MM/dd/yyyy',
'dd/MM/yyyy',
'dd-MM-yyyy',
'yyyy/MM/dd',
'dd-MMM-yyyy',
'dd MMM yyyy',
];
for (String fmt in formats) {
try {
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s);
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly);
return DateTime(parsed.year, parsed.month, parsed.day);
} catch (_) {}
}
@ -171,19 +225,24 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
String _formatDob(DateTime? dob, String? raw) {
if (dob != null) {
return DateFormat('dd MMM yyyy', 'en_US').format(dob);
return DateFormat('dd-MM-yyyy', 'en_US').format(dob);
}
if (raw != null && raw.trim().isNotEmpty) {
return raw.split(' ').first;
return raw.split(' ').first.replaceAll('/', '-');
}
return "-";
}
// Masks everything but the last 3 digits, e.g. "2561615457" -> "*******457" - used both for
// the on-screen Key Details display and the share message, so the full ID is never shown or
// sent in plain text anywhere.
String _formatNationalId(String? nationalId) {
if (nationalId == null || nationalId.trim().isEmpty) return "-";
String clean = nationalId.replaceAll('-', '').trim();
String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean;
return LocaleKeys.idEnding.tr(args: <String>[last4]);
if (clean.length <= 3) return clean;
String lastDigits = clean.substring(clean.length - 3);
String mask = List<String>.filled(clean.length - 3, '*').join();
return '$mask$lastDigits';
}
void _onToggleAccess() {
@ -193,9 +252,20 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
ConfirmBottomSheet.show(
context,
title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(),
message: isAllowed ? LocaleKeys.confirmRevokeMazayaAccess.tr(args: <String>[dependentName]) : LocaleKeys.confirmGrantMazayaAccess.tr(args: <String>[dependentName]),
icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline,
title:
isAllowed
? LocaleKeys.revokeMazayaAccess.tr()
: LocaleKeys.allowMazayaAccess.tr(),
message:
isAllowed
? LocaleKeys.confirmRevokeMazayaAccess.tr(
args: <String>[dependentName],
)
: LocaleKeys.confirmGrantMazayaAccess.tr(
args: <String>[dependentName],
),
icon:
isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline,
iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
@ -250,37 +320,51 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
if (dob != null) return DateFormat('dd-MM-yyyy').format(dob);
String raw = _contact?.dATEOFBIRTH?.trim() ?? "";
if (raw.isEmpty) return "";
if (raw.contains('T')) return raw.split('T').first;
if (raw.contains(' ')) return raw.split(' ').first;
return raw;
if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-');
if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-');
return raw.replaceAll('/', '-');
}
// Same message built for both "Share Download Link" and "Copy Link" - they must always
// carry identical content, so this is the single source of truth for it.
String _buildMazayaInviteMessage(String linkToShare) {
String? nationalId = _contact?.nATIONALIDENTIFIER?.trim();
String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
String nationalIdLine =
(nationalId == null || nationalId.isEmpty)
? LocaleKeys.nationalIdOrIqamaFallback.tr()
: "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}";
}
// Shares the invite text together with the language-appropriate steps PDF from assets.
// Falls back to sharing just the text if the PDF can't be loaded/copied for any reason.
Future<void> _shareMazayaInvite(bool isAr, String linkToShare) async {
// Text-only share (the invite message + link). Kept separate from the guide share below
// because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
// attached to the same share - splitting them guarantees the message itself is always visible.
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare) async {
String message = _buildMazayaInviteMessage(linkToShare);
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
// File-only share of the bundled access guide PDF - no accompanying text, so every target
// (including WhatsApp) just attaches the file with nothing to drop.
Future<void> _shareMazayaAccessGuide(bool isAr) async {
try {
ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr));
ByteData bytes = await rootBundle.load(
MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr),
);
Directory tempDir = await getTemporaryDirectory();
String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf";
String localFileName =
isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf";
File pdfFile = File('${tempDir.path}/$localFileName');
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
await pdfFile.writeAsBytes(
bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
);
await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)]));
} catch (_) {
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
} catch (_) {}
}
@override
@ -289,7 +373,11 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
String initials = _getInitials(_contact?.cONTACTNAME);
String fullName = _contact?.cONTACTNAME ?? "";
String firstName = _getFirstName(_contact?.cONTACTNAME);
String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty) ? _contact!.rELATIONSHIP!.trim() : (_contact?.cONTACTTYPE?.trim() ?? "");
String relation =
(_contact?.rELATIONSHIP != null &&
_contact!.rELATIONSHIP!.trim().isNotEmpty)
? _contact!.rELATIONSHIP!.trim()
: (_contact?.cONTACTTYPE?.trim() ?? "");
String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER);
String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH);
bool isAllowed = _isAccessAllowed;
@ -310,15 +398,39 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context),
child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20,
color: Color(0xFF1E293B),
),
),
),
const SizedBox(width: 8),
LocaleKeys.dependentDetails.tr().toText20(color: const Color(0xFF1E293B)),
LocaleKeys.dependentDetails.tr().toText20(
color: const Color(0xFF1E293B),
),
const Spacer(),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))),
onTap:
() => Navigator.popUntil(
context,
ModalRoute.withName(
ApiConsts.isDependetLogin
? AppRoutes.mazayaDiscover
: AppRoutes.dashboard,
),
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.home_outlined,
size: 24,
color: Color(0xFF1E293B),
),
),
),
],
),
@ -342,21 +454,39 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(color: _recentAction! ? const Color(0xFFE6F7F2) : const Color(0xFFFEECEB), shape: BoxShape.circle),
decoration: BoxDecoration(
color:
_recentAction!
? const Color(0xFFE6F7F2)
: const Color(0xFFFEECEB),
shape: BoxShape.circle,
),
child: Center(
child: CustomPaint(
size: const Size(36, 36),
painter: _ShieldIconPainter(color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24), isGranted: _recentAction!),
painter: _ShieldIconPainter(
color:
_recentAction!
? const Color(0xFF168A89)
: const Color(0xFFED1C24),
isGranted: _recentAction!,
),
),
),
),
),
const SizedBox(height: 10),
Center(
child: (_recentAction! ? LocaleKeys.mazayaAccessGranted.tr() : LocaleKeys.mazayaAccessRevoked.tr()).toText16(
weight: FontWeight.w500,
color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24),
),
child: (_recentAction!
? LocaleKeys.mazayaAccessGranted.tr()
: LocaleKeys.mazayaAccessRevoked.tr())
.toText16(
weight: FontWeight.w500,
color:
_recentAction!
? const Color(0xFF168A89)
: const Color(0xFFED1C24),
),
),
const SizedBox(height: 16),
],
@ -365,7 +495,14 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: const Color(0xFFE8F7F2), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2)),
decoration: BoxDecoration(
color: const Color(0xFFE8F7F2),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFBCE6D7),
width: 1.2,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
@ -373,8 +510,15 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
Container(
width: 52,
height: 52,
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Center(child: initials.toText17(color: const Color(0xFF168A89))),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: initials.toText17(
color: const Color(0xFF168A89),
),
),
),
const SizedBox(width: 14),
@ -384,14 +528,32 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
fullName.toText16(maxlines: 2, weight: FontWeight.w500, color: const Color(0xFF1E293B)),
if (relation.isNotEmpty) ...<Widget>[const SizedBox(height: 3), relation.toText12(maxLine: 1, weight: FontWeight.w500, color: const Color(0xFF168A89))],
fullName.toText16(
maxlines: 2,
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
if (relation.isNotEmpty) ...<Widget>[
const SizedBox(height: 3),
relation.toText12(
maxLine: 1,
weight: FontWeight.w500,
color: const Color(0xFF168A89),
),
],
],
),
),
// Verified badge on right - only when NHIC has actually verified this contact
if (_isNhicVerified) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24)],
if (_isNhicVerified) ...<Widget>[
const SizedBox(width: 8),
const Icon(
Icons.verified_rounded,
color: Color(0xFF059669),
size: 24,
),
],
],
),
),
@ -406,33 +568,62 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))],
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
if (_isNhicVerified) LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)),
LocaleKeys.keyDetails.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
if (_isNhicVerified)
LocaleKeys.verified.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF059669),
),
],
),
const SizedBox(height: 14),
const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)),
const Divider(
height: 1,
thickness: 1,
color: Color(0xFFF1F5F9),
),
const SizedBox(height: 14),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.nationalId.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
formattedId.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.nationalId.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
formattedId.toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.dateOfBirth.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
formattedDob.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.dateOfBirth.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
formattedDob.toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
],
),
],
@ -442,9 +633,15 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
const SizedBox(height: 24),
// Services & actions section header
LocaleKeys.servicesAndActions.tr().toText17(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.servicesAndActions.tr().toText17(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 4),
LocaleKeys.startDependentFlowsDesc.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
LocaleKeys.startDependentFlowsDesc.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
const SizedBox(height: 12),
@ -456,7 +653,13 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))],
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -465,29 +668,66 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.mazayaAccess.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.mazayaAccess.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(color: isAllowed ? const Color(0xFFECFDF5) : const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(12)),
child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.accessOff.tr()).toText12(
weight: FontWeight.w500,
color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 3,
),
decoration: BoxDecoration(
color:
isAllowed
? const Color(0xFFECFDF5)
: const Color(0xFFFEE2E2),
borderRadius: BorderRadius.circular(12),
),
child: (isAllowed
? LocaleKeys.active.tr()
: LocaleKeys.accessOff.tr())
.toText12(
weight: FontWeight.w500,
color:
isAllowed
? const Color(0xFF059669)
: const Color(0xFFEF4444),
),
),
],
),
const SizedBox(height: 6),
LocaleKeys.allowDependentMazayaOffersDesc.tr(args: <String>[firstName.isNotEmpty ? firstName : fullName]).toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
LocaleKeys.allowDependentMazayaOffersDesc
.tr(
args: <String>[
firstName.isNotEmpty ? firstName : fullName,
],
)
.toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
const SizedBox(height: 16),
const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)),
const Divider(
height: 1,
thickness: 1,
color: Color(0xFFF1F5F9),
),
const SizedBox(height: 14),
// Login method row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.loginMethod.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
LocaleKeys.nationalIdAndDob.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.loginMethod.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
LocaleKeys.nationalIdAndDob.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
],
),
const SizedBox(height: 12),
@ -496,8 +736,14 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.verification.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
LocaleKeys.sponsorOtp.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.verification.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
LocaleKeys.sponsorOtp.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
],
),
@ -511,19 +757,46 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
height: 48,
width: double.infinity,
decoration: BoxDecoration(
color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6),
color:
isAllowed
? const Color(0xFFFEF2F2)
: const Color(0xFF22B0A6),
borderRadius: BorderRadius.circular(12),
border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null,
border:
isAllowed
? Border.all(
color: const Color(0xFFFECACA),
width: 1.2,
)
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(width: 18, height: 18, child: CustomPaint(painter: _ShieldIconPainter(color: isAllowed ? const Color(0xFFDC2626) : Colors.white, isGranted: !isAllowed))),
const SizedBox(width: 8),
(isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr()).toText16(
weight: FontWeight.w500,
color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
SizedBox(
width: 18,
height: 18,
child: CustomPaint(
painter: _ShieldIconPainter(
color:
isAllowed
? const Color(0xFFDC2626)
: Colors.white,
isGranted: !isAllowed,
),
),
),
const SizedBox(width: 8),
(isAllowed
? LocaleKeys.revokeMazayaAccess.tr()
: LocaleKeys.allowMazayaAccess.tr())
.toText16(
weight: FontWeight.w500,
color:
isAllowed
? const Color(0xFFDC2626)
: Colors.white,
),
],
),
),
@ -531,7 +804,15 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
if (isAllowed) ...<Widget>[
const SizedBox(height: 12),
Center(child: LocaleKeys.revokingPreventsAccessDesc.tr().toText11(weight: FontWeight.w400, color: const Color(0xFF64748B), isCenter: true)),
Center(
child: LocaleKeys.revokingPreventsAccessDesc
.tr()
.toText11(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
isCenter: true,
),
),
],
],
),
@ -540,9 +821,38 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
),
),
),
// Share download link - same bottom-sticky button convention used elsewhere in
// the app (e.g. "Join Marathon"), shown once access is granted.
if (isAllowed) DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInvite(isAr, linkToShare)).insideContainer,
// Two separate share actions (not one combined text+file share) - same bottom-sticky
// container convention used elsewhere in the app (e.g. "Join Marathon"), shown once
// access is granted.
if (isAllowed)
Row(
children: <Widget>[
// Secondary - same bg/border/text colors as the name+relation card at the top
// of this screen, for visual consistency within this page.
Expanded(
child: DefaultButton(
LocaleKeys.shareAccessGuide.tr(),
() => _shareMazayaAccessGuide(isAr),
colors: const <Color>[
Color(0xFFE8F7F2),
Color(0xFFE8F7F2),
],
border: Border.all(
color: const Color(0xFFBCE6D7),
width: 1.2,
),
textColor: const Color(0xFF168A89),
),
),
const SizedBox(width: 12),
Expanded(
child: DefaultButton(
LocaleKeys.shareDownloadLink.tr(),
() => _shareMazayaInviteLink(isAr, linkToShare),
),
),
],
).insideContainer,
],
),
),

@ -1,5 +1,4 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -13,6 +12,7 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/dashboard/menu_entries.dart';
import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart';
import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart';
import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart';
import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart';
import 'package:mohem_flutter_app/ui/profile/dependent_details_screen.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
@ -191,6 +191,9 @@ class _FamilyMembersState extends State<FamilyMembers> {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
// Only shown when reached via the Mazaya "My Family" tab - this screen is also opened from
// the regular profile flow (fromMazaya == false), where the Mazaya bottom nav doesn't apply.
bottomNavigationBar: fromMazaya ? const MazayaBottomNav(currentTab: MazayaTab.myFamily) : null,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: SafeArea(
@ -200,19 +203,28 @@ class _FamilyMembersState extends State<FamilyMembers> {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: <Widget>[
InkWell(
!fromMazaya? InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context),
child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))),
),
):SizedBox(),
const SizedBox(width: 8),
(fromMazaya ? LocaleKeys.myDependents.tr() : LocaleKeys.profile_familyDetails.tr()).toText20(isBold: true, color: const Color(0xFF1E293B)),
// Same title style as the other Mazaya tabs' headers (Discover/Saved use
// AppBarWidget's title.toText24(isBold: true, color: MyColors.darkTextColor)) -
// matched here since this screen builds its own header instead of AppBarWidget.
fromMazaya
? LocaleKeys.myFamily.tr().toText24(isBold: true, color: MyColors.darkTextColor)
: LocaleKeys.profile_familyDetails.tr().toText20(isBold: true, color: const Color(0xFF1E293B)),
const Spacer(),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))),
),
// Home button hidden in the Mazaya flow too, matching AppBarWidget's
// showHomeButton: false on the other tabs - MazayaBottomNav's own Discover tab
// already serves as "home" for this module.
if (!fromMazaya)
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))),
),
],
),
),
@ -302,7 +314,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
}
// Same Shimmer.fromColors skeleton-card convention used elsewhere in the app (e.g.
// dashboard_screen_for_dependet.dart's retailer/category shimmers), shaped like the
// retailers_list_screen.dart's retailer/category shimmers), shaped like the
// real _buildDependentCard layout so the loading state doesn't jump around once it loads.
Widget _buildDependentsShimmer() {
return Shimmer.fromColors(
@ -396,7 +408,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B)),
),
),
if (_isNhicVerified(item)) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 20)],
if (_isNhicVerified(item)) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_rounded, color: Color(0xFF059669), size: 20)],
],
),
const SizedBox(height: 3),
@ -533,28 +545,27 @@ class _FamilyMembersState extends State<FamilyMembers> {
}
void showUpdateAlertDialog(BuildContext context, int relationId, int flag, String actionType) {
Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context));
Widget continueButton = TextButton(
child: LocaleKeys.ok.tr().toText14(),
onPressed: () {
Navigator.pop(context);
ConfirmBottomSheet.show(
context,
message: LocaleKeys.updateMember.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: () {
Navigator.pushNamed(context, AppRoutes.addUpdateFamilyMember, arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE")).then((_) => getEmployeeContacts());
},
);
AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.updateMember.tr().toText14(), actions: <Widget>[cancelButton, continueButton]);
showDialog(context: context, builder: (BuildContext context) => alert);
}
void showRemoveAlertDialog(BuildContext context, int relationId) {
Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context));
Widget continueButton = TextButton(
child: LocaleKeys.ok.tr().toText14(),
onPressed: () {
Navigator.pop(context);
ConfirmBottomSheet.show(
context,
message: LocaleKeys.removeThisMember.tr(),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
isDestructive: true,
onConfirm: () {
Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts());
},
);
AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.removeThisMember.tr().toText14(), actions: <Widget>[cancelButton, continueButton]);
showDialog(context: context, builder: (BuildContext context) => alert);
}
}

@ -1,10 +1,7 @@
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
@ -20,8 +17,9 @@ class MazayaAccessResultParams {
final String? downloadUrl;
final String? nationalId;
final String? dateOfBirth;
final String? message;
const MazayaAccessResultParams({required this.isGranted, required this.dependentName, this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth});
const MazayaAccessResultParams({required this.isGranted, required this.dependentName, this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth, this.message});
}
class _ShieldPainter extends CustomPainter {
@ -79,8 +77,10 @@ class MazayaAccessResultScreen extends StatelessWidget {
final String? downloadUrl;
final String? nationalId;
final String? dateOfBirth;
final String? message;
const MazayaAccessResultScreen({Key? key, this.isGranted = true, this.dependentName = "", this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth}) : super(key: key);
const MazayaAccessResultScreen({Key? key, this.isGranted = true, this.dependentName = "", this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth, this.message})
: super(key: key);
static String getDownloadLink() {
int channel = AppState().postParamsObject?.channel ?? 31;
@ -119,6 +119,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
String? effectiveUrl = downloadUrl;
String? effectiveNationalId = nationalId;
String? effectiveDateOfBirth = dateOfBirth;
String? effectiveMessage = message;
if (routeArgs is MazayaAccessResultParams) {
effectiveGranted = routeArgs.isGranted;
@ -127,6 +128,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
effectiveUrl = routeArgs.downloadUrl;
effectiveNationalId = routeArgs.nationalId;
effectiveDateOfBirth = routeArgs.dateOfBirth;
effectiveMessage = routeArgs.message ?? message;
} else if (routeArgs is Map<String, dynamic>) {
effectiveGranted = routeArgs['isGranted'] ?? isGranted;
effectiveName = routeArgs['dependentName'] ?? dependentName;
@ -134,16 +136,17 @@ class MazayaAccessResultScreen extends StatelessWidget {
effectiveUrl = routeArgs['downloadUrl'] ?? downloadUrl;
effectiveNationalId = routeArgs['nationalId'] ?? nationalId;
effectiveDateOfBirth = routeArgs['dateOfBirth'] ?? dateOfBirth;
effectiveMessage = routeArgs['message'] ?? message;
}
if (effectiveName.trim().isEmpty) {
String fallback = AppState().dependentName ?? "";
if (fallback.isNotEmpty) {
effectiveName = fallback;
} else {
effectiveName = isAr ? "التابع" : "Dependent";
}
}
effectiveName = isAr ? "التابع" : "Dependent";
// if (effectiveName.trim().isEmpty) {
// String fallback = AppState().dependentName ?? "";
// if (fallback.isNotEmpty) {
// effectiveName = fallback;
// } else {
// effectiveName = isAr ? "التابع" : "Dependent";
// }
// }
String linkToShare = effectiveUrl ?? getDownloadLink();
String initials = effectiveInitials ?? _getInitials(effectiveName);
@ -183,7 +186,9 @@ class MazayaAccessResultScreen extends StatelessWidget {
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
child:
effectiveGranted ? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) : _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials),
effectiveGranted
? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare)
: _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials, effectiveMessage),
),
),
_buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""),
@ -332,7 +337,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
);
}
Widget _buildRevokedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials) {
Widget _buildRevokedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, [String? message]) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
@ -354,7 +359,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
const SizedBox(height: 8),
// Description
LocaleKeys.mazayaAccessRevokedDesc.tr().toText14(isCenter: true, color: const Color(0xFF64748B)),
(message != null && message.trim().isNotEmpty ? message.trim() : LocaleKeys.mazayaAccessRevokedDesc.tr()).toText14(isCenter: true, color: const Color(0xFF64748B)),
const SizedBox(height: 22),
@ -407,7 +412,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
// name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
const SizedBox(height: 2),
LocaleKeys.dependentAccessBlocked.tr().toText12(isBold: true, color: const Color(0xFFED1C24)),
],
@ -440,27 +445,47 @@ class MazayaAccessResultScreen extends StatelessWidget {
if (parsed != null) {
return DateFormat('dd-MM-yyyy').format(parsed);
}
// DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") -
// DateTime.tryParse above doesn't understand that shape, so strip the clock-time suffix
// and parse the date part explicitly. MM/dd/yyyy before dd/MM/yyyy since the real source
// is confirmed month-first - an ambiguous case like "5/6/1990" must resolve to May 6.
String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim();
for (String fmt in <String>['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy-MM-dd', 'dd-MM-yyyy']) {
try {
DateTime dateOnlyParsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly);
return DateFormat('dd-MM-yyyy').format(dateOnlyParsed);
} catch (_) {}
}
} catch (_) {}
// Fallback: strip a time component off a plain "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-ddTHH:mm:ss"
// string we couldn't otherwise parse, rather than showing it as-is with the time attached.
if (s.contains('T')) return s.split('T').first;
if (s.contains(' ')) return s.split(' ').first;
return s;
if (s.contains('T')) return s.split('T').first.replaceAll('/', '-');
if (s.contains(' ')) return s.split(' ').first.replaceAll('/', '-');
return s.replaceAll('/', '-');
}
// Same message built for both "Share Download Link" and "Copy Link" - they must always
// carry identical content, so this is the single source of truth for it.
String _buildMazayaInviteMessage(String linkToShare, String nationalId, String dateOfBirth) {
String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}";
}
// Shares the invite text together with the language-appropriate steps PDF from assets.
// Falls back to sharing just the text if the PDF can't be loaded/copied for any reason.
Future<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
// Text-only share (the invite message + link). Kept separate from the guide share below
// because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
// attached to the same share - splitting them guarantees the message itself is always visible.
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth);
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
// File-only share of the bundled access guide PDF - no accompanying text, so every target
// (including WhatsApp) just attaches the file with nothing to drop.
Future<void> _shareMazayaAccessGuide(bool isAr) async {
try {
ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr));
Directory tempDir = await getTemporaryDirectory();
@ -469,15 +494,12 @@ class MazayaAccessResultScreen extends StatelessWidget {
File pdfFile = File('${tempDir.path}/$localFileName');
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
log("final: $message\n\nfile: ${pdfFile.path}");
await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)]));
} catch (_) {
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
} catch (_) {}
}
Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) {
if (granted) {
return Container(
@ -488,7 +510,17 @@ class MazayaAccessResultScreen extends StatelessWidget {
children: <Widget>[
DefaultButton(
LocaleKeys.shareDownloadLink.tr(),
() => _shareMazayaInvite(isAr, linkToShare, nationalId, dateOfBirth),
() => _shareMazayaInviteLink(isAr, linkToShare, nationalId, dateOfBirth),
trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded,
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
const SizedBox(height: 10),
DefaultButton(
LocaleKeys.shareAccessGuide.tr(),
() => _shareMazayaAccessGuide(isAr),
trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded,
fontSize: 14,
isTextExpanded: false,
@ -500,7 +532,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
LocaleKeys.copyLink.tr(),
() {
Clipboard.setData(ClipboardData(text: _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth)));
Utils.showToast(LocaleKeys.linkCopied.tr());
Utils.showToast(LocaleKeys.linkCopied.tr(), isSuccess: true);
},
colors: const <Color>[Colors.white, Colors.white],
border: Border.all(color: const Color(0xFFCBD5E1)),

@ -2,7 +2,8 @@ import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
// import 'package:flutter_html/flutter_html.dart';
import 'package:mohem_flutter_app/api/pending_transactions_api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart';
@ -59,8 +60,8 @@ class _AnnouncementDetailsState extends State<AnnouncementDetails> {
),
),
),
Html(
data: AppState().isArabic(context) ? getAnnouncementDetailsObj?.bodyAR : getAnnouncementDetailsObj?.bodyEN,
HtmlWidget(
AppState().isArabic(context) ? getAnnouncementDetailsObj?.bodyAR??"" : getAnnouncementDetailsObj?.bodyEN ??"",
).paddingOnly(top: 12),
],
).objectContainerView().paddingAll(21),

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save