wala_plus
FaizHashmi 2 weeks ago
parent a165bf695d
commit de57bfd9eb

@ -607,6 +607,10 @@
"yes": "نعم", "yes": "نعم",
"no": "لا", "no": "لا",
"mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", "mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول",
"cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة",
"cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات",
"openSettings": "فتح الإعدادات",
"nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك",
"Attendance": "الحضور", "Attendance": "الحضور",
"audioCall": "مكالمة صوتية", "audioCall": "مكالمة صوتية",
"editGroups": "تعديل المجموعة", "editGroups": "تعديل المجموعة",

@ -554,6 +554,10 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"mazayaInviteInstructions": "Please download the Mohemm app from the above link and use the following credentials to login", "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", "groupMembers": "Group Members",
"manageGroup": "Manage Group", "manageGroup": "Manage Group",
"admin": "Admin", "admin": "Admin",

@ -38,12 +38,7 @@ class OtpDialog {
TextEditingController digit3 = TextEditingController(text: ""); TextEditingController digit3 = TextEditingController(text: "");
TextEditingController digit4 = TextEditingController(text: ""); TextEditingController digit4 = TextEditingController(text: "");
Map verifyAccountFormValue = { Map verifyAccountFormValue = {'digit1': '', 'digit2': '', 'digit3': '', 'digit4': ''};
'digit1': '',
'digit2': '',
'digit3': '',
'digit4': '',
};
final focusD1 = FocusNode(); final focusD1 = FocusNode();
final focusD2 = FocusNode(); final focusD2 = FocusNode();
final focusD3 = FocusNode(); final focusD3 = FocusNode();
@ -62,16 +57,17 @@ class OtpDialog {
otpFieldClear.value = ""; otpFieldClear.value = "";
_startListeningForSms(); _startListeningForSms();
showDialog( showDialog(
context: context, context: context,
barrierColor: Colors.black.withOpacity(0.63), barrierColor: Colors.black.withOpacity(0.63),
builder: (context) { builder: (context) {
// projectProvider = Provider.of(context); // projectProvider = Provider.of(context);
return Dialog( return Dialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
insetPadding: const EdgeInsets.only(left: 21, right: 21), insetPadding: const EdgeInsets.only(left: 21, right: 21),
child: StatefulBuilder(builder: (context, setState) { child: StatefulBuilder(
builder: (context, setState) {
if (displayTime == '') { if (displayTime == '') {
startTimer(setState); startTimer(setState);
} }
@ -86,10 +82,10 @@ class OtpDialog {
type == 1 type == 1
? "assets/images/login/verify_sms.svg" ? "assets/images/login/verify_sms.svg"
: type == 2 : type == 2
? "assets/images/login/verify_whatsapp.svg" ? "assets/images/login/verify_whatsapp.svg"
: type == 3 : type == 3
? "assets/images/login/verify_face.svg" ? "assets/images/login/verify_face.svg"
: 'assets/images/login/verify_thumb.svg', : 'assets/images/login/verify_thumb.svg',
height: 50, height: 50,
width: 50, width: 50,
), ),
@ -144,92 +140,61 @@ class OtpDialog {
10.height, 10.height,
isCodeExpired isCodeExpired
? Row( ? Row(
children: [ children: [
Expanded( Expanded(child: LocaleKeys.codeExpire.tr().toText16(color: MyColors.redColor)),
child: LocaleKeys.codeExpire.tr().toText16( 12.width,
color: MyColors.redColor, Image.asset("assets/icons/ic_alarm.png", width: 20, height: 20, color: MyColors.redColor),
), ],
), )
12.width,
Image.asset(
"assets/icons/ic_alarm.png",
width: 20,
height: 20,
color: MyColors.redColor,
),
],
)
: RichText( : RichText(
text: TextSpan( text: TextSpan(
text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48),
children: <TextSpan>[ children: <TextSpan>[
TextSpan( TextSpan(text: displayTime, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48)),
text: displayTime, ],
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48),
),
],
),
), ),
),
], ],
); );
}, },
valueListenable: otpFieldClear, valueListenable: otpFieldClear,
), ),
18.height, 18.height,
DefaultButton( DefaultButton(isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), () async {
isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), if (isCodeExpired) {
() async { hideSMSBox(context);
if (isCodeExpired) { onResendCode();
hideSMSBox(context); } else {
onResendCode(); stopTimer = true;
} else { hideSMSBox(context);
stopTimer = true; }
hideSMSBox(context); }, colors: isCodeExpired ? null : [MyColors.pinkDarkColor, MyColors.pinkDarkColor]),
}
},
colors: isCodeExpired
? null
: [
MyColors.pinkDarkColor,
MyColors.pinkDarkColor,
],
),
], ],
), ),
); );
}), },
); ),
}).then((_) { );
stopTimer = true; },
_stopListeningForSms(); ).then((_) {
}); stopTimer = true;
_stopListeningForSms();
});
} }
InputDecoration buildInputDecoration(BuildContext context) { InputDecoration buildInputDecoration(BuildContext context) {
return InputDecoration( return InputDecoration(
counterText: " ", counterText: " ",
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10)), borderSide: BorderSide(color: Colors.black)),
borderRadius: BorderRadius.all(Radius.circular(10)), focusedBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).primaryColor)),
borderSide: BorderSide(color: Colors.black), errorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Colors.red)),
),
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( // focusedErrorBorder: OutlineInputBorder(
// borderRadius: const BorderRadius.all(Radius.circular(10.0)), // borderRadius: const BorderRadius.all(Radius.circular(10.0)),
// borderSide: BorderSide(color: Colors.red), // borderSide: BorderSide(color: Colors.red),
// // borderSide: BorderSide(color: Theme.of(context).colorScheme.error), // // borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
// ), // ),
focusedErrorBorder: OutlineInputBorder( focusedErrorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).colorScheme.error)),
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
),
); );
} }
@ -300,21 +265,23 @@ class OtpDialog {
void _startListeningForSms() { void _startListeningForSms() {
if (!Platform.isAndroid) return; if (!Platform.isAndroid) return;
try { try {
SmsVerification.startListeningSms().then((String? message) { SmsVerification.startListeningSms()
if (stopTimer || isCodeExpired) return; .then((String? message) {
if (message != null && message.isNotEmpty) { if (stopTimer || isCodeExpired) return;
debugPrint("SMS OTP Auto-Verify received message: $message"); if (message != null && message.isNotEmpty) {
String? otpCode = extractOtpCode(message); debugPrint("SMS OTP Auto-Verify received message: $message");
if (otpCode != null && otpCode.length == 4) { String? otpCode = extractOtpCode(message);
debugPrint("Auto-filling OTP: $otpCode"); if (otpCode != null && otpCode.length == 4) {
_pinPutController.text = otpCode; debugPrint("Auto-filling OTP: $otpCode");
_pinPutController.selection = TextSelection.collapsed(offset: otpCode.length); _pinPutController.text = otpCode;
_onOtpCallBack(otpCode, true); _pinPutController.selection = TextSelection.collapsed(offset: otpCode.length);
} _onOtpCallBack(otpCode, true);
} }
}).catchError((e) { }
debugPrint("Error in startListeningSms: $e"); })
}); .catchError((e) {
debugPrint("Error in startListeningSms: $e");
});
} catch (e) { } catch (e) {
debugPrint("Error starting SMS listening: $e"); debugPrint("Error starting SMS listening: $e");
} }
@ -334,10 +301,7 @@ class OtpDialog {
String normalized = _normalizeDigits(message); String normalized = _normalizeDigits(message);
// 1. Try matching with OTP context keyword if available // 1. Try matching with OTP context keyword if available
RegExp contextRegex = RegExp( RegExp contextRegex = RegExp(r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b', caseSensitive: false);
r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b',
caseSensitive: false,
);
Match? contextMatch = contextRegex.firstMatch(normalized); Match? contextMatch = contextRegex.firstMatch(normalized);
if (contextMatch != null && contextMatch.group(1) != null) { if (contextMatch != null && contextMatch.group(1) != null) {
return contextMatch.group(1); return contextMatch.group(1);

@ -623,6 +623,10 @@ class CodegenLoader extends AssetLoader{
"yes": "نعم", "yes": "نعم",
"no": "لا", "no": "لا",
"mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", "mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول",
"cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة",
"cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات",
"openSettings": "فتح الإعدادات",
"nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك",
"Attendance": "الحضور", "Attendance": "الحضور",
"audioCall": "مكالمة صوتية", "audioCall": "مكالمة صوتية",
"editGroups": "تعديل المجموعة", "editGroups": "تعديل المجموعة",
@ -1359,6 +1363,10 @@ static const Map<String,dynamic> _en_US = {
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"mazayaInviteInstructions": "Please download the Mohemm app from the above link and use the following credentials to login", "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", "groupMembers": "Group Members",
"manageGroup": "Manage Group", "manageGroup": "Manage Group",
"admin": "Admin", "admin": "Admin",

@ -595,6 +595,10 @@ abstract class LocaleKeys {
static const yes = 'yes'; static const yes = 'yes';
static const no = 'no'; static const no = 'no';
static const mazayaInviteInstructions = 'mazayaInviteInstructions'; 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 Attendance = 'Attendance';
static const audioCall = 'audioCall'; static const audioCall = 'audioCall';
static const editGroups = 'editGroups'; static const editGroups = 'editGroups';

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
class GetEmployeeContactsList { class GetEmployeeContactsList {
String? cONTACTNAME; String? cONTACTNAME;
@ -39,7 +40,16 @@ class GetEmployeeContactsList {
nATIONALIDENTIFIER = json['NATIONAL_IDENTIFIER']?.toString() ?? nATIONALIDENTIFIER = json['NATIONAL_IDENTIFIER']?.toString() ??
json['NATIONAL_ID']?.toString() ?? json['NATIONAL_ID']?.toString() ??
json['CIVIL_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<String, dynamic> toJson() { Map<String, dynamic> toJson() {

@ -757,7 +757,6 @@ class WalaPlusRetailerLocation {
double? km = double.tryParse(distance!.trim()); double? km = double.tryParse(distance!.trim());
if (km == null) return null; if (km == null) return null;
String formatted = km.toStringAsFixed(2); String formatted = km.toStringAsFixed(2);
log("formatted: ${[formatted]}");
return LocaleKeys.kmAway.tr(args: [formatted]); return LocaleKeys.kmAway.tr(args: [formatted]);
} }

@ -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/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_bar_widget.dart';
import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.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/button/default_button.dart';
import 'package:mohem_flutter_app/widgets/info_chip_widget.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'; import 'package:url_launcher/url_launcher.dart';
class WalaPlusRetailerDetailsScreen extends StatefulWidget { class WalaPlusRetailerDetailsScreen extends StatefulWidget {
@ -143,12 +145,68 @@ class _WalaPlusRetailerDetailsScreenState extends State<WalaPlusRetailerDetailsS
String title = _retailer?.name ?? LocaleKeys.offerDetails.tr(); String title = _retailer?.name ?? LocaleKeys.offerDetails.tr();
bool shouldStartScanning = offerType?.trim().toLowerCase() == "discount"; bool shouldStartScanning = offerType?.trim().toLowerCase() == "discount";
log("shouldStartScanning: ${shouldStartScanning.toString()} for offerType: ${offerType.toString()}"); log("shouldStartScanning: ${shouldStartScanning.toString()} for offerType: ${offerType.toString()}");
// Ask for camera permission app-side, before the WebView (and the page's own
// getUserMedia() call) even exists. By the time the page asks, the OS decision is
// already made, so onPermissionRequest can answer instantly - no mid-flight dialog
// racing the page's own timeout for its original getUserMedia() call.
// Discount (QR) offers need the camera to work at all, so if the user denies here,
// don't open the transaction URL - promo offers never reach this check.
if (shouldStartScanning) {
try {
PermissionStatus status = await Permission.camera.status;
log("permission status: ${status.toString()}");
// Permanently denied: Permission.camera.request() won't show a dialog at all in this
// state (Android just returns denied silently) - only the app settings screen can fix it.
if (status.isPermanentlyDenied) {
_showCameraPermissionSheet(LocaleKeys.cameraPermissionPermanentlyDenied.tr());
return;
}
if (!status.isGranted) {
status = await Permission.camera.request();
}
if (status.isPermanentlyDenied) {
_showCameraPermissionSheet(LocaleKeys.cameraPermissionPermanentlyDenied.tr());
return;
}
if (!status.isGranted) {
_showCameraPermissionSheet(LocaleKeys.cameraPermissionRequiredToScan.tr());
return;
}
} catch (e) {
debugPrint("Error requesting camera permission before opening transaction URL: $e");
_showCameraPermissionSheet(LocaleKeys.cameraPermissionRequiredToScan.tr());
return;
}
}
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (BuildContext context) => AppInAppWebViewScreen(url: url, title: title, onPageLoaded: shouldStartScanning ? _clickStartScanningButton : null)), MaterialPageRoute(builder: (BuildContext context) => 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 // 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). // 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 // Two buttons matter here: "Start Scanning" (the initial trigger) and "Try Again" (shown

@ -1,8 +1,10 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart';
@ -19,14 +21,7 @@ class MazayaAccessResultParams {
final String? nationalId; final String? nationalId;
final String? dateOfBirth; final String? dateOfBirth;
const MazayaAccessResultParams({ const MazayaAccessResultParams({required this.isGranted, required this.dependentName, this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth});
required this.isGranted,
required this.dependentName,
this.dependentInitials,
this.downloadUrl,
this.nationalId,
this.dateOfBirth,
});
} }
class _ShieldPainter extends CustomPainter { class _ShieldPainter extends CustomPainter {
@ -37,12 +32,13 @@ class _ShieldPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
Paint paint = Paint() Paint paint =
..color = color Paint()
..style = PaintingStyle.stroke ..color = color
..strokeWidth = 2.4 ..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round ..strokeWidth = 2.4
..strokeJoin = StrokeJoin.round; ..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
double w = size.width; double w = size.width;
double h = size.height; double h = size.height;
@ -67,22 +63,13 @@ class _ShieldPainter extends CustomPainter {
canvas.drawPath(checkPath, paint); canvas.drawPath(checkPath, paint);
} else { } else {
// Cross path // Cross path
canvas.drawLine( canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint);
Offset(w * 0.38, h * 0.38), canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint);
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 @override
bool shouldRepaint(covariant _ShieldPainter oldDelegate) => bool shouldRepaint(covariant _ShieldPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted;
color != oldDelegate.color || isGranted != oldDelegate.isGranted;
} }
class MazayaAccessResultScreen extends StatelessWidget { class MazayaAccessResultScreen extends StatelessWidget {
@ -93,28 +80,11 @@ class MazayaAccessResultScreen extends StatelessWidget {
final String? nationalId; final String? nationalId;
final String? dateOfBirth; final String? dateOfBirth;
const MazayaAccessResultScreen({ const MazayaAccessResultScreen({Key? key, this.isGranted = true, this.dependentName = "", this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth}) : super(key: key);
Key? key,
this.isGranted = true,
this.dependentName = "",
this.dependentInitials,
this.downloadUrl,
this.nationalId,
this.dateOfBirth,
}) : super(key: key);
static String getDownloadLink() { static String getDownloadLink() {
int channel = AppState().postParamsObject?.channel ?? 31; int channel = AppState().postParamsObject?.channel ?? 31;
bool isIos = Platform.isIOS; return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html";
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";
}
} }
String _getInitials(String? name) { String _getInitials(String? name) {
@ -134,6 +104,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
// Resolve arguments if passed via ModalRoute // Resolve arguments if passed via ModalRoute
Object? routeArgs = ModalRoute.of(context)?.settings.arguments; Object? routeArgs = ModalRoute.of(context)?.settings.arguments;
log("MazayaAccessResultScreen routeArgs: $routeArgs");
bool effectiveGranted = isGranted; bool effectiveGranted = isGranted;
String effectiveName = dependentName; String effectiveName = dependentName;
String? effectiveInitials = dependentInitials; String? effectiveInitials = dependentInitials;
@ -182,32 +153,15 @@ class MazayaAccessResultScreen extends StatelessWidget {
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20,
color: Color(0xFF1E293B),
),
),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
LocaleKeys.mazayaTitle.tr().toText20( LocaleKeys.mazayaTitle.tr().toText20(isBold: true, color: const Color(0xFF1E293B)),
isBold: true,
color: const Color(0xFF1E293B),
),
const Spacer(), const Spacer(),
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.close_rounded, size: 24, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.close_rounded,
size: 24,
color: Color(0xFF1E293B),
),
),
), ),
], ],
), ),
@ -220,9 +174,8 @@ class MazayaAccessResultScreen extends StatelessWidget {
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
child: effectiveGranted child:
? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) effectiveGranted ? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) : _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials),
: _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials),
), ),
), ),
_buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""), _buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""),
@ -232,14 +185,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
); );
} }
Widget _buildGrantedContent( Widget _buildGrantedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, String linkToShare) {
BuildContext context,
bool isAr,
String fontFamily,
String name,
String initials,
String linkToShare,
) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
@ -249,37 +195,19 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle),
color: Color(0xFFE6F7F2), child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFF168A89), isGranted: true))),
shape: BoxShape.circle,
),
child: Center(
child: CustomPaint(
size: const Size(40, 40),
painter: _ShieldPainter(
color: const Color(0xFF168A89),
isGranted: true,
),
),
),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
// Title // Title
LocaleKeys.mazayaAccessGranted.tr().toText22( LocaleKeys.mazayaAccessGranted.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)),
isBold: true,
isCentered: true,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 8), const SizedBox(height: 8),
// Description // Description
LocaleKeys.mazayaAccessGrantedDesc.tr(args: <String>[name]).toText14( LocaleKeys.mazayaAccessGrantedDesc.tr(args: <String>[name]).toText14(isCenter: true, color: const Color(0xFF64748B)),
isCenter: true,
color: const Color(0xFF64748B),
),
const SizedBox(height: 22), const SizedBox(height: 22),
@ -287,35 +215,19 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFCEEFE7), width: 1)),
color: const Color(0xFFE8F8F0),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFCEEFE7), width: 1),
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Padding( const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.check_circle_outline_rounded, color: Color(0xFF168A89), size: 20)),
padding: EdgeInsets.only(top: 2),
child: Icon(
Icons.check_circle_outline_rounded,
color: Color(0xFF168A89),
size: 20,
),
),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
LocaleKeys.accessIsNowActive.tr().toText14( LocaleKeys.accessIsNowActive.tr().toText14(isBold: true, color: const Color(0xFF168A89)),
isBold: true,
color: const Color(0xFF168A89),
),
const SizedBox(height: 3), const SizedBox(height: 3),
LocaleKeys.accessIsNowActiveDesc.tr(args: <String>[name]).toText12( LocaleKeys.accessIsNowActiveDesc.tr(args: <String>[name]).toText12(color: const Color(0xFF64748B)),
color: const Color(0xFF64748B),
),
], ],
), ),
), ),
@ -333,53 +245,28 @@ class MazayaAccessResultScreen extends StatelessWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFF1F5F9)), border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[ boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))],
BoxShadow(
color: Color(0x08000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Container( Container(
width: 42, width: 42,
height: 42, height: 42,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle),
color: Color(0xFFE6F7F2), child: Center(child: initials.toText14(isBold: true, color: const Color(0xFF168A89))),
shape: BoxShape.circle,
),
child: Center(
child: initials.toText14(
isBold: true,
color: const Color(0xFF168A89),
),
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
name.toText14( name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
isBold: true,
maxlines: 1,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 2), const SizedBox(height: 2),
LocaleKeys.dependentAccessActive.tr().toText12( LocaleKeys.dependentAccessActive.tr().toText12(isBold: true, color: const Color(0xFF168A89)),
isBold: true,
color: const Color(0xFF168A89),
),
], ],
), ),
), ),
const Icon( const Icon(Icons.lock_open_rounded, color: Color(0xFF168A89), size: 20),
Icons.lock_open_rounded,
color: Color(0xFF168A89),
size: 20,
),
], ],
), ),
), ),
@ -390,11 +277,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE2E8F0))),
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -404,31 +287,17 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: 36, width: 36,
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(10)),
color: const Color(0xFFE8F8F0), child: const Center(child: Icon(Icons.smartphone_rounded, color: Color(0xFF168A89), size: 20)),
borderRadius: BorderRadius.circular(10),
),
child: const Center(
child: Icon(
Icons.smartphone_rounded,
color: Color(0xFF168A89),
size: 20,
),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
LocaleKeys.shareableAppDownloadLink.tr().toText14( LocaleKeys.shareableAppDownloadLink.tr().toText14(isBold: true, color: const Color(0xFF1E293B)),
isBold: true,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 2), const SizedBox(height: 2),
LocaleKeys.shareableAppDownloadLinkDesc.tr(args: <String>[name]).toText12( LocaleKeys.shareableAppDownloadLinkDesc.tr(args: <String>[name]).toText12(color: const Color(0xFF64748B)),
color: const Color(0xFF64748B),
),
], ],
), ),
), ),
@ -438,23 +307,13 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
decoration: BoxDecoration( decoration: BoxDecoration(color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0))),
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
LocaleKeys.downloadLink.tr().toText11( LocaleKeys.downloadLink.tr().toText11(isBold: true, color: const Color(0xFF168A89)),
isBold: true,
color: const Color(0xFF168A89),
),
const SizedBox(height: 3), const SizedBox(height: 3),
linkToShare.toText12( linkToShare.toText12(maxLine: 1, color: const Color(0xFF334155)),
maxLine: 1,
color: const Color(0xFF334155),
),
], ],
), ),
), ),
@ -465,13 +324,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
); );
} }
Widget _buildRevokedContent( Widget _buildRevokedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials) {
BuildContext context,
bool isAr,
String fontFamily,
String name,
String initials,
) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
@ -481,37 +334,19 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFFEECEB), shape: BoxShape.circle),
color: Color(0xFFFEECEB), child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFFED1C24), isGranted: false))),
shape: BoxShape.circle,
),
child: Center(
child: CustomPaint(
size: const Size(40, 40),
painter: _ShieldPainter(
color: const Color(0xFFED1C24),
isGranted: false,
),
),
),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
// Title // Title
LocaleKeys.mazayaAccessRevoked.tr().toText22( LocaleKeys.mazayaAccessRevoked.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)),
isBold: true,
isCentered: true,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 8), const SizedBox(height: 8),
// Description // Description
LocaleKeys.mazayaAccessRevokedDesc.tr().toText14( LocaleKeys.mazayaAccessRevokedDesc.tr().toText14(isCenter: true, color: const Color(0xFF64748B)),
isCenter: true,
color: const Color(0xFF64748B),
),
const SizedBox(height: 22), const SizedBox(height: 22),
@ -519,35 +354,19 @@ class MazayaAccessResultScreen extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(color: const Color(0xFFFFF5F5), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFCDD2), width: 1)),
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFFFCDD2), width: 1),
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Padding( const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.phone_outlined, color: Color(0xFFED1C24), size: 20)),
padding: EdgeInsets.only(top: 2),
child: Icon(
Icons.phone_outlined,
color: Color(0xFFED1C24),
size: 20,
),
),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
LocaleKeys.contactYourSponsor.tr().toText14( LocaleKeys.contactYourSponsor.tr().toText14(isBold: true, color: const Color(0xFFED1C24)),
isBold: true,
color: const Color(0xFFED1C24),
),
const SizedBox(height: 3), const SizedBox(height: 3),
LocaleKeys.contactYourSponsorDesc.tr().toText12( LocaleKeys.contactYourSponsorDesc.tr().toText12(color: const Color(0xFF64748B)),
color: const Color(0xFF64748B),
),
], ],
), ),
), ),
@ -565,53 +384,28 @@ class MazayaAccessResultScreen extends StatelessWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFF1F5F9)), border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[ boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))],
BoxShadow(
color: Color(0x08000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Container( Container(
width: 42, width: 42,
height: 42, height: 42,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFFFF1F0), shape: BoxShape.circle),
color: Color(0xFFFFF1F0), child: Center(child: initials.toText14(isBold: true, color: const Color(0xFFED1C24))),
shape: BoxShape.circle,
),
child: Center(
child: initials.toText14(
isBold: true,
color: const Color(0xFFED1C24),
),
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
name.toText14( name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
isBold: true,
maxlines: 1,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 2), const SizedBox(height: 2),
LocaleKeys.dependentAccessBlocked.tr().toText12( LocaleKeys.dependentAccessBlocked.tr().toText12(isBold: true, color: const Color(0xFFED1C24)),
isBold: true,
color: const Color(0xFFED1C24),
),
], ],
), ),
), ),
const Icon( const Icon(Icons.lock_outline_rounded, color: Color(0xFFED1C24), size: 20),
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 // Builds the invite text (link + localized instructions + credentials) and shares it
// together with the language-appropriate steps PDF from assets. Falls back to sharing // 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. // just the text if the PDF can't be loaded/copied for any reason.
Future<void> _shareMazayaInvite( Future<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
bool isAr, String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
String linkToShare, String message = "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}";
String nationalId,
String dateOfBirth,
) async {
String message =
"$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n${LocaleKeys.nationalId.tr()}: $nationalId\n${LocaleKeys.dateOfBirth.tr()}: $dateOfBirth";
try { try {
String fileName = isAr ? "mazaya_invite_steps_ar.pdf" : "mazaya_invite_steps_en.pdf"; String assetFileName = isAr ? "Mazaya Access Guide Arabic.pdf" : "Mazaya Access Guide English.pdf";
ByteData bytes = await rootBundle.load("assets/pdf/$fileName"); ByteData bytes = await rootBundle.load("assets/pdf/$assetFileName");
Directory tempDir = await getTemporaryDirectory(); 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)); 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)])); await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)]));
} catch (_) { } catch (_) {
try { try {
@ -646,15 +466,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
} }
} }
Widget _buildBottomButtons( Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) {
BuildContext context,
bool isAr,
String fontFamily,
bool granted,
String linkToShare,
String nationalId,
String dateOfBirth,
) {
if (granted) { if (granted) {
return Container( return Container(
color: Colors.white, color: Colors.white,

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@ -110,18 +112,28 @@ class _AppInAppWebViewScreenState extends State<AppInAppWebViewScreen> {
bool isGranted = false; bool isGranted = false;
try { try {
PermissionStatus status = await Permission.camera.status; PermissionStatus status = await Permission.camera.status;
if (!status.isGranted) {
status = await Permission.camera.request();
}
isGranted = status.isGranted; 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) { } 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( return PermissionResponse(
resources: permissionRequest.resources, resources: permissionRequest.resources,
action: isGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY, action: isGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY,

@ -108,12 +108,7 @@ class ConfirmBottomSheet extends StatelessWidget {
children: [ children: [
// Drag handle // Drag handle
Center( Center(
child: Container( child: Container(width: 44, height: 4.5, margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5))),
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) ...[ if (icon != null) ...[

Loading…
Cancel
Save