diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index efbc78a..f84ed69 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -607,6 +607,10 @@ "yes": "نعم", "no": "لا", "mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", + "cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة", + "cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات", + "openSettings": "فتح الإعدادات", + "nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك", "Attendance": "الحضور", "audioCall": "مكالمة صوتية", "editGroups": "تعديل المجموعة", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index d681ad7..14c598d 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -554,6 +554,10 @@ "yes": "Yes", "no": "No", "mazayaInviteInstructions": "Please download the Mohemm 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", + "nationalIdOrIqamaFallback": "Your National ID or Iqama Number", "groupMembers": "Group Members", "manageGroup": "Manage Group", "admin": "Admin", diff --git a/assets/pdf/Mazaya Access Guide Arabic.pdf b/assets/pdf/Mazaya Access Guide Arabic.pdf new file mode 100644 index 0000000..0df5aab Binary files /dev/null and b/assets/pdf/Mazaya Access Guide Arabic.pdf differ diff --git a/assets/pdf/Mazaya Access Guide English.pdf b/assets/pdf/Mazaya Access Guide English.pdf new file mode 100644 index 0000000..0df5aab Binary files /dev/null and b/assets/pdf/Mazaya Access Guide English.pdf differ diff --git a/assets/pdf/mazaya_invite_steps_ar.pdf b/assets/pdf/mazaya_invite_steps_ar.pdf deleted file mode 100644 index 4603bd3..0000000 Binary files a/assets/pdf/mazaya_invite_steps_ar.pdf and /dev/null differ diff --git a/assets/pdf/mazaya_invite_steps_en.pdf b/assets/pdf/mazaya_invite_steps_en.pdf deleted file mode 100644 index 4603bd3..0000000 Binary files a/assets/pdf/mazaya_invite_steps_en.pdf and /dev/null differ diff --git a/lib/dialogs/otp_dialog.dart b/lib/dialogs/otp_dialog.dart index a5ab8ec..5932f89 100644 --- a/lib/dialogs/otp_dialog.dart +++ b/lib/dialogs/otp_dialog.dart @@ -38,12 +38,7 @@ class OtpDialog { TextEditingController digit3 = TextEditingController(text: ""); TextEditingController digit4 = TextEditingController(text: ""); - Map verifyAccountFormValue = { - 'digit1': '', - 'digit2': '', - 'digit3': '', - 'digit4': '', - }; + Map verifyAccountFormValue = {'digit1': '', 'digit2': '', 'digit3': '', 'digit4': ''}; final focusD1 = FocusNode(); final focusD2 = FocusNode(); final focusD3 = FocusNode(); @@ -62,16 +57,17 @@ class OtpDialog { otpFieldClear.value = ""; _startListeningForSms(); showDialog( - context: context, - barrierColor: Colors.black.withOpacity(0.63), - builder: (context) { - // projectProvider = Provider.of(context); - return Dialog( - backgroundColor: Colors.white, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - insetPadding: const EdgeInsets.only(left: 21, right: 21), - child: StatefulBuilder(builder: (context, setState) { + context: context, + barrierColor: Colors.black.withOpacity(0.63), + builder: (context) { + // projectProvider = Provider.of(context); + return Dialog( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + insetPadding: const EdgeInsets.only(left: 21, right: 21), + child: StatefulBuilder( + builder: (context, setState) { if (displayTime == '') { startTimer(setState); } @@ -86,10 +82,10 @@ class OtpDialog { type == 1 ? "assets/images/login/verify_sms.svg" : type == 2 - ? "assets/images/login/verify_whatsapp.svg" - : type == 3 - ? "assets/images/login/verify_face.svg" - : 'assets/images/login/verify_thumb.svg', + ? "assets/images/login/verify_whatsapp.svg" + : type == 3 + ? "assets/images/login/verify_face.svg" + : 'assets/images/login/verify_thumb.svg', height: 50, width: 50, ), @@ -144,92 +140,61 @@ class OtpDialog { 10.height, isCodeExpired ? Row( - children: [ - Expanded( - child: LocaleKeys.codeExpire.tr().toText16( - color: MyColors.redColor, - ), - ), - 12.width, - Image.asset( - "assets/icons/ic_alarm.png", - width: 20, - height: 20, - color: MyColors.redColor, - ), - ], - ) + children: [ + Expanded(child: LocaleKeys.codeExpire.tr().toText16(color: MyColors.redColor)), + 12.width, + Image.asset("assets/icons/ic_alarm.png", width: 20, height: 20, color: MyColors.redColor), + ], + ) : RichText( - text: TextSpan( - text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), - children: [ - TextSpan( - text: displayTime, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48), - ), - ], - ), + text: TextSpan( + text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), + children: [ + TextSpan(text: displayTime, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48)), + ], ), + ), ], ); }, valueListenable: otpFieldClear, ), 18.height, - DefaultButton( - isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), - () async { - if (isCodeExpired) { - hideSMSBox(context); - onResendCode(); - } else { - stopTimer = true; - hideSMSBox(context); - } - }, - colors: isCodeExpired - ? null - : [ - MyColors.pinkDarkColor, - MyColors.pinkDarkColor, - ], - ), + DefaultButton(isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), () async { + if (isCodeExpired) { + hideSMSBox(context); + onResendCode(); + } else { + stopTimer = true; + hideSMSBox(context); + } + }, colors: isCodeExpired ? null : [MyColors.pinkDarkColor, MyColors.pinkDarkColor]), ], ), ); - }), - ); - }).then((_) { - stopTimer = true; - _stopListeningForSms(); - }); + }, + ), + ); + }, + ).then((_) { + stopTimer = true; + _stopListeningForSms(); + }); } InputDecoration buildInputDecoration(BuildContext context) { return InputDecoration( counterText: " ", - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - borderSide: BorderSide(color: Colors.black), - ), - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Theme.of(context).primaryColor), - ), - errorBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Colors.red), - ), + enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10)), borderSide: BorderSide(color: Colors.black)), + focusedBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).primaryColor)), + errorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Colors.red)), // focusedErrorBorder: OutlineInputBorder( // borderRadius: const BorderRadius.all(Radius.circular(10.0)), // borderSide: BorderSide(color: Colors.red), // // borderSide: BorderSide(color: Theme.of(context).colorScheme.error), // ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Theme.of(context).colorScheme.error), - ), + focusedErrorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).colorScheme.error)), ); } @@ -300,21 +265,23 @@ class OtpDialog { void _startListeningForSms() { if (!Platform.isAndroid) return; try { - SmsVerification.startListeningSms().then((String? message) { - if (stopTimer || isCodeExpired) return; - if (message != null && message.isNotEmpty) { - debugPrint("SMS OTP Auto-Verify received message: $message"); - String? otpCode = extractOtpCode(message); - if (otpCode != null && otpCode.length == 4) { - debugPrint("Auto-filling OTP: $otpCode"); - _pinPutController.text = otpCode; - _pinPutController.selection = TextSelection.collapsed(offset: otpCode.length); - _onOtpCallBack(otpCode, true); - } - } - }).catchError((e) { - debugPrint("Error in startListeningSms: $e"); - }); + SmsVerification.startListeningSms() + .then((String? message) { + if (stopTimer || isCodeExpired) return; + if (message != null && message.isNotEmpty) { + debugPrint("SMS OTP Auto-Verify received message: $message"); + String? otpCode = extractOtpCode(message); + if (otpCode != null && otpCode.length == 4) { + debugPrint("Auto-filling OTP: $otpCode"); + _pinPutController.text = otpCode; + _pinPutController.selection = TextSelection.collapsed(offset: otpCode.length); + _onOtpCallBack(otpCode, true); + } + } + }) + .catchError((e) { + debugPrint("Error in startListeningSms: $e"); + }); } catch (e) { debugPrint("Error starting SMS listening: $e"); } @@ -334,10 +301,7 @@ class OtpDialog { String normalized = _normalizeDigits(message); // 1. Try matching with OTP context keyword if available - RegExp contextRegex = RegExp( - r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b', - caseSensitive: false, - ); + RegExp contextRegex = RegExp(r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b', caseSensitive: false); Match? contextMatch = contextRegex.firstMatch(normalized); if (contextMatch != null && contextMatch.group(1) != null) { return contextMatch.group(1); diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index c10b099..c144c50 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -623,6 +623,10 @@ class CodegenLoader extends AssetLoader{ "yes": "نعم", "no": "لا", "mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", + "cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة", + "cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات", + "openSettings": "فتح الإعدادات", + "nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك", "Attendance": "الحضور", "audioCall": "مكالمة صوتية", "editGroups": "تعديل المجموعة", @@ -1374,6 +1378,10 @@ static const Map _en_US = { "yes": "Yes", "no": "No", "mazayaInviteInstructions": "Please download the Mohemm 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", + "nationalIdOrIqamaFallback": "Your National ID or Iqama Number", "groupMembers": "Group Members", "manageGroup": "Manage Group", "admin": "Admin", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index fef9831..4f88cff 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -595,6 +595,10 @@ abstract class LocaleKeys { static const yes = 'yes'; static const no = 'no'; static const mazayaInviteInstructions = 'mazayaInviteInstructions'; + static const cameraPermissionRequiredToScan = 'cameraPermissionRequiredToScan'; + static const cameraPermissionPermanentlyDenied = 'cameraPermissionPermanentlyDenied'; + static const openSettings = 'openSettings'; + static const nationalIdOrIqamaFallback = 'nationalIdOrIqamaFallback'; static const Attendance = 'Attendance'; static const audioCall = 'audioCall'; static const editGroups = 'editGroups'; diff --git a/lib/models/get_employee_contacts.model.dart b/lib/models/get_employee_contacts.model.dart index 63964f6..8e6ff24 100644 --- a/lib/models/get_employee_contacts.model.dart +++ b/lib/models/get_employee_contacts.model.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; class GetEmployeeContactsList { String? cONTACTNAME; @@ -39,7 +40,16 @@ class GetEmployeeContactsList { nATIONALIDENTIFIER = json['NATIONAL_IDENTIFIER']?.toString() ?? json['NATIONAL_ID']?.toString() ?? json['CIVIL_ID']?.toString() ?? - json['IQAMA_NUMBER']?.toString(); + json['IQAMA_NUMBER']?.toString() ?? + json['CONTACT_NATIONAL_IDENTIFIER']?.toString() ?? + json['CONTACT_NATIONAL_ID']?.toString() ?? + json['CONTACT_IDENTIFICATION_ID']?.toString() ?? + json['IDENTIFICATION_ID']?.toString() ?? + json['NATIONALID']?.toString(); + if (nATIONALIDENTIFIER == null || nATIONALIDENTIFIER!.trim().isEmpty) { + // TODO: remove once the real key for a contact's national ID is confirmed from a live response. + debugPrint("GetEmployeeContactsList: no national id match found, raw contact json: $json"); + } } Map toJson() { diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart index dd062f1..ceb7581 100644 --- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -757,7 +757,6 @@ class WalaPlusRetailerLocation { double? km = double.tryParse(distance!.trim()); if (km == null) return null; String formatted = km.toStringAsFixed(2); - log("formatted: ${[formatted]}"); return LocaleKeys.kmAway.tr(args: [formatted]); } diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/landing/wala_plus_retailer_details_screen.dart index a7c8372..a1db7dd 100644 --- a/lib/ui/landing/wala_plus_retailer_details_screen.dart +++ b/lib/ui/landing/wala_plus_retailer_details_screen.dart @@ -18,8 +18,10 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/info_chip_widget.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; class WalaPlusRetailerDetailsScreen extends StatefulWidget { @@ -143,12 +145,68 @@ class _WalaPlusRetailerDetailsScreenState extends State AppInAppWebViewScreen(url: url, title: title, onPageLoaded: shouldStartScanning ? _clickStartScanningButton : null)), ); } + // showModalBottomSheet is dismissible by tapping outside by default (isDismissible: true), + // so no extra wiring is needed for that. "Open Settings" is the only action - there's nothing + // useful to retry from here, the user has to leave the app to fix this. + void _showCameraPermissionSheet(String message) { + ConfirmBottomSheet.show( + context, + message: message, + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + icon: Icons.videocam_off_outlined, + isDestructive: false, + onConfirm: () { + openAppSettings(); + }, + ); + } + // Vue's @click handlers are component methods, not something on window, so we find the // actual button by its text and click it (lets Vue's own handler fire). // Two buttons matter here: "Start Scanning" (the initial trigger) and "Try Again" (shown diff --git a/lib/ui/profile/mazaya_access_result_screen.dart b/lib/ui/profile/mazaya_access_result_screen.dart index 988b986..e8e6d71 100644 --- a/lib/ui/profile/mazaya_access_result_screen.dart +++ b/lib/ui/profile/mazaya_access_result_screen.dart @@ -1,8 +1,10 @@ +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'; @@ -19,14 +21,7 @@ class MazayaAccessResultParams { final String? nationalId; final String? dateOfBirth; - 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}); } class _ShieldPainter extends CustomPainter { @@ -37,12 +32,13 @@ class _ShieldPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - Paint paint = Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = 2.4 - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round; + Paint paint = + Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.4 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; double w = size.width; double h = size.height; @@ -67,22 +63,13 @@ class _ShieldPainter extends CustomPainter { canvas.drawPath(checkPath, paint); } else { // Cross path - 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 _ShieldPainter oldDelegate) => - color != oldDelegate.color || isGranted != oldDelegate.isGranted; + bool shouldRepaint(covariant _ShieldPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted; } class MazayaAccessResultScreen extends StatelessWidget { @@ -93,28 +80,11 @@ class MazayaAccessResultScreen extends StatelessWidget { final String? nationalId; final String? dateOfBirth; - 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}) : super(key: key); static String getDownloadLink() { int channel = AppState().postParamsObject?.channel ?? 31; - bool isIos = Platform.isIOS; - if (channel == 33) { - return isIos - ? "https://apps.apple.com/sa/app/al-habib-mohem/id1480157255" - : "https://play.google.com/store/apps/details?id=com.cloudsolutions.alhabibmohemm&hl=en"; - } else { - return isIos - ? "https://apps.apple.com/sa/app/mohemm/id1468856602" - : "https://play.google.com/store/apps/details?id=hmg.cloudSolutions.mohem&hl=en"; - } + return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html"; } String _getInitials(String? name) { @@ -134,6 +104,7 @@ class MazayaAccessResultScreen extends StatelessWidget { // Resolve arguments if passed via ModalRoute Object? routeArgs = ModalRoute.of(context)?.settings.arguments; + log("MazayaAccessResultScreen routeArgs: $routeArgs"); bool effectiveGranted = isGranted; String effectiveName = dependentName; String? effectiveInitials = dependentInitials; @@ -182,32 +153,15 @@ class MazayaAccessResultScreen extends StatelessWidget { 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: 4), - LocaleKeys.mazayaTitle.tr().toText20( - isBold: true, - color: const Color(0xFF1E293B), - ), + LocaleKeys.mazayaTitle.tr().toText20(isBold: true, color: const Color(0xFF1E293B)), const Spacer(), InkWell( borderRadius: BorderRadius.circular(20), onTap: () => Navigator.pop(context), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.close_rounded, - size: 24, - color: Color(0xFF1E293B), - ), - ), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.close_rounded, size: 24, color: Color(0xFF1E293B))), ), ], ), @@ -220,9 +174,8 @@ class MazayaAccessResultScreen extends StatelessWidget { Expanded( 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), + child: + effectiveGranted ? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) : _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials), ), ), _buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""), @@ -232,14 +185,7 @@ class MazayaAccessResultScreen extends StatelessWidget { ); } - Widget _buildGrantedContent( - BuildContext context, - bool isAr, - String fontFamily, - String name, - String initials, - String linkToShare, - ) { + Widget _buildGrantedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, String linkToShare) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -249,37 +195,19 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: 80, height: 80, - decoration: const BoxDecoration( - color: Color(0xFFE6F7F2), - shape: BoxShape.circle, - ), - child: Center( - child: CustomPaint( - size: const Size(40, 40), - painter: _ShieldPainter( - color: const Color(0xFF168A89), - isGranted: true, - ), - ), - ), + decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle), + child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFF168A89), isGranted: true))), ), const SizedBox(height: 18), // Title - LocaleKeys.mazayaAccessGranted.tr().toText22( - isBold: true, - isCentered: true, - color: const Color(0xFF1E293B), - ), + LocaleKeys.mazayaAccessGranted.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)), const SizedBox(height: 8), // Description - LocaleKeys.mazayaAccessGrantedDesc.tr(args: [name]).toText14( - isCenter: true, - color: const Color(0xFF64748B), - ), + LocaleKeys.mazayaAccessGrantedDesc.tr(args: [name]).toText14(isCenter: true, color: const Color(0xFF64748B)), const SizedBox(height: 22), @@ -287,35 +215,19 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFFE8F8F0), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFFCEEFE7), width: 1), - ), + decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFCEEFE7), width: 1)), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.only(top: 2), - child: Icon( - Icons.check_circle_outline_rounded, - color: Color(0xFF168A89), - size: 20, - ), - ), + const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.check_circle_outline_rounded, color: Color(0xFF168A89), size: 20)), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.accessIsNowActive.tr().toText14( - isBold: true, - color: const Color(0xFF168A89), - ), + LocaleKeys.accessIsNowActive.tr().toText14(isBold: true, color: const Color(0xFF168A89)), const SizedBox(height: 3), - LocaleKeys.accessIsNowActiveDesc.tr(args: [name]).toText12( - color: const Color(0xFF64748B), - ), + LocaleKeys.accessIsNowActiveDesc.tr(args: [name]).toText12(color: const Color(0xFF64748B)), ], ), ), @@ -333,53 +245,28 @@ class MazayaAccessResultScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFF1F5F9)), - boxShadow: const [ - BoxShadow( - color: Color(0x08000000), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))], ), child: Row( children: [ Container( width: 42, height: 42, - decoration: const BoxDecoration( - color: Color(0xFFE6F7F2), - shape: BoxShape.circle, - ), - child: Center( - child: initials.toText14( - isBold: true, - color: const Color(0xFF168A89), - ), - ), + decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle), + child: Center(child: initials.toText14(isBold: true, color: const Color(0xFF168A89))), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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.dependentAccessActive.tr().toText12( - isBold: true, - color: const Color(0xFF168A89), - ), + LocaleKeys.dependentAccessActive.tr().toText12(isBold: true, color: const Color(0xFF168A89)), ], ), ), - const Icon( - Icons.lock_open_rounded, - color: Color(0xFF168A89), - size: 20, - ), + const Icon(Icons.lock_open_rounded, color: Color(0xFF168A89), size: 20), ], ), ), @@ -390,11 +277,7 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: double.infinity, padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFFE2E8F0)), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE2E8F0))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -404,31 +287,17 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: 36, height: 36, - decoration: BoxDecoration( - color: const Color(0xFFE8F8F0), - borderRadius: BorderRadius.circular(10), - ), - child: const Center( - child: Icon( - Icons.smartphone_rounded, - color: Color(0xFF168A89), - size: 20, - ), - ), + decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(10)), + child: const Center(child: Icon(Icons.smartphone_rounded, color: Color(0xFF168A89), size: 20)), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.shareableAppDownloadLink.tr().toText14( - isBold: true, - color: const Color(0xFF1E293B), - ), + LocaleKeys.shareableAppDownloadLink.tr().toText14(isBold: true, color: const Color(0xFF1E293B)), const SizedBox(height: 2), - LocaleKeys.shareableAppDownloadLinkDesc.tr(args: [name]).toText12( - color: const Color(0xFF64748B), - ), + LocaleKeys.shareableAppDownloadLinkDesc.tr(args: [name]).toText12(color: const Color(0xFF64748B)), ], ), ), @@ -438,23 +307,13 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFE2E8F0)), - ), + decoration: BoxDecoration(color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.downloadLink.tr().toText11( - isBold: true, - color: const Color(0xFF168A89), - ), + LocaleKeys.downloadLink.tr().toText11(isBold: true, color: const Color(0xFF168A89)), const SizedBox(height: 3), - linkToShare.toText12( - maxLine: 1, - color: const Color(0xFF334155), - ), + linkToShare.toText12(maxLine: 1, color: const Color(0xFF334155)), ], ), ), @@ -465,13 +324,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) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -481,37 +334,19 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: 80, height: 80, - decoration: const BoxDecoration( - color: Color(0xFFFEECEB), - shape: BoxShape.circle, - ), - child: Center( - child: CustomPaint( - size: const Size(40, 40), - painter: _ShieldPainter( - color: const Color(0xFFED1C24), - isGranted: false, - ), - ), - ), + decoration: const BoxDecoration(color: Color(0xFFFEECEB), shape: BoxShape.circle), + child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFFED1C24), isGranted: false))), ), const SizedBox(height: 18), // Title - LocaleKeys.mazayaAccessRevoked.tr().toText22( - isBold: true, - isCentered: true, - color: const Color(0xFF1E293B), - ), + LocaleKeys.mazayaAccessRevoked.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)), const SizedBox(height: 8), // Description - LocaleKeys.mazayaAccessRevokedDesc.tr().toText14( - isCenter: true, - color: const Color(0xFF64748B), - ), + LocaleKeys.mazayaAccessRevokedDesc.tr().toText14(isCenter: true, color: const Color(0xFF64748B)), const SizedBox(height: 22), @@ -519,35 +354,19 @@ class MazayaAccessResultScreen extends StatelessWidget { Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFFFFCDD2), width: 1), - ), + decoration: BoxDecoration(color: const Color(0xFFFFF5F5), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFCDD2), width: 1)), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.only(top: 2), - child: Icon( - Icons.phone_outlined, - color: Color(0xFFED1C24), - size: 20, - ), - ), + const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.phone_outlined, color: Color(0xFFED1C24), size: 20)), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.contactYourSponsor.tr().toText14( - isBold: true, - color: const Color(0xFFED1C24), - ), + LocaleKeys.contactYourSponsor.tr().toText14(isBold: true, color: const Color(0xFFED1C24)), const SizedBox(height: 3), - LocaleKeys.contactYourSponsorDesc.tr().toText12( - color: const Color(0xFF64748B), - ), + LocaleKeys.contactYourSponsorDesc.tr().toText12(color: const Color(0xFF64748B)), ], ), ), @@ -565,53 +384,28 @@ class MazayaAccessResultScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFF1F5F9)), - boxShadow: const [ - BoxShadow( - color: Color(0x08000000), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))], ), child: Row( children: [ Container( width: 42, height: 42, - decoration: const BoxDecoration( - color: Color(0xFFFFF1F0), - shape: BoxShape.circle, - ), - child: Center( - child: initials.toText14( - isBold: true, - color: const Color(0xFFED1C24), - ), - ), + decoration: const BoxDecoration(color: Color(0xFFFFF1F0), shape: BoxShape.circle), + child: Center(child: initials.toText14(isBold: true, color: const Color(0xFFED1C24))), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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), - ), + LocaleKeys.dependentAccessBlocked.tr().toText12(isBold: true, color: const Color(0xFFED1C24)), ], ), ), - const Icon( - Icons.lock_outline_rounded, - color: Color(0xFFED1C24), - size: 20, - ), + const Icon(Icons.lock_outline_rounded, color: Color(0xFFED1C24), size: 20), ], ), ), @@ -619,25 +413,51 @@ class MazayaAccessResultScreen extends StatelessWidget { ); } + // Display-only DOB formatting for the share message: "dd-MM-yyyy", no time component. + // The raw value can come in as the API's "/Date(ms+tz)/" wrapper or a plain ISO datetime string + // with a time component - either way we only want the date part. + String _formatDob(String raw) { + String s = raw.trim(); + if (s.isEmpty) return ""; + + try { + RegExp aspNetDateRegex = RegExp(r'/Date\((-?\d+)([+-]\d{4})?\)/'); + Match? match = aspNetDateRegex.firstMatch(s); + if (match != null) { + int ms = int.parse(match.group(1)!); + return DateFormat('dd-MM-yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)); + } + + DateTime? parsed = DateTime.tryParse(s); + if (parsed != null) { + return DateFormat('dd-MM-yyyy').format(parsed); + } + } 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; + } + // Builds the invite text (link + localized instructions + credentials) and shares it // 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 _shareMazayaInvite( - bool isAr, - String linkToShare, - String nationalId, - String dateOfBirth, - ) async { - String message = - "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n${LocaleKeys.nationalId.tr()}: $nationalId\n${LocaleKeys.dateOfBirth.tr()}: $dateOfBirth"; + Future _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async { + String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId"; + String message = "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}"; try { - String fileName = isAr ? "mazaya_invite_steps_ar.pdf" : "mazaya_invite_steps_en.pdf"; - ByteData bytes = await rootBundle.load("assets/pdf/$fileName"); + String assetFileName = isAr ? "Mazaya Access Guide Arabic.pdf" : "Mazaya Access Guide English.pdf"; + ByteData bytes = await rootBundle.load("assets/pdf/$assetFileName"); Directory tempDir = await getTemporaryDirectory(); - File pdfFile = File('${tempDir.path}/$fileName'); + // Write to a space-free filename locally - the asset path above is what actually matters. + 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)); + log("final: $message\n\nfile: ${pdfFile.path}"); await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)])); } catch (_) { try { @@ -646,15 +466,7 @@ class MazayaAccessResultScreen extends StatelessWidget { } } - Widget _buildBottomButtons( - BuildContext context, - bool isAr, - String fontFamily, - bool granted, - String linkToShare, - String nationalId, - String dateOfBirth, - ) { + Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) { if (granted) { return Container( color: Colors.white, diff --git a/lib/widgets/app_in_app_webview_screen.dart b/lib/widgets/app_in_app_webview_screen.dart index 1c4cf60..169e087 100644 --- a/lib/widgets/app_in_app_webview_screen.dart +++ b/lib/widgets/app_in_app_webview_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -110,18 +112,28 @@ class _AppInAppWebViewScreenState extends State { bool isGranted = false; try { PermissionStatus status = await Permission.camera.status; - if (!status.isGranted) { - status = await Permission.camera.request(); - } isGranted = status.isGranted; + // IMPORTANT: never await Permission.camera.request() here. The native WebView + // is blocked waiting on THIS callback's response to the page's getUserMedia() + // call - if we hold it open for however long the human takes to respond to the + // OS dialog (can be 30s+), Chromium abandons the original getUserMedia() call + // as timed out before we ever answer. So: answer immediately with whatever the + // status already is, and if not granted, kick off the slow dialog in the + // background. Once that resolves, retry the click trigger so the page makes a + // FRESH getUserMedia() call - which resolves instantly next time since the OS + // permission is already durably granted by then (no dialog, no timeout risk). + if (!isGranted) { + unawaited( + Permission.camera.request().then((PermissionStatus result) { + if (result.isGranted) { + widget.onPageLoaded?.call(controller); + } + }), + ); + } } catch (e) { - debugPrint("Error requesting camera permission: $e"); + debugPrint("Error checking camera permission: $e"); } - // Retry the page-load click trigger now that the permission result is known - - // e.g. the page may only render the real "Start Scanning" button once camera - // access is resolved. Safe to call again: it's a no-op if already clicked or - // already watching (see _clickStartScanningButton's window-scoped guards). - widget.onPageLoaded?.call(controller); return PermissionResponse( resources: permissionRequest.resources, action: isGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY, diff --git a/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart b/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart index cd09ecb..ebc7488 100644 --- a/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart +++ b/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart @@ -108,12 +108,7 @@ class ConfirmBottomSheet extends StatelessWidget { children: [ // Drag handle Center( - child: Container( - width: 44, - height: 4.5, - margin: const EdgeInsets.only(bottom: 18), - decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5)), - ), + child: Container(width: 44, height: 4.5, margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5))), ), if (icon != null) ...[