wala_plus
FaizHashmi 20 hours ago
parent a165bf695d
commit de57bfd9eb

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

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

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

@ -623,6 +623,10 @@ class CodegenLoader extends AssetLoader{
"yes": "نعم",
"no": "لا",
"mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول",
"cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة",
"cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات",
"openSettings": "فتح الإعدادات",
"nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك",
"Attendance": "الحضور",
"audioCall": "مكالمة صوتية",
"editGroups": "تعديل المجموعة",
@ -1359,6 +1363,10 @@ static const Map<String,dynamic> _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",

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

@ -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<String, dynamic> toJson() {

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

@ -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<WalaPlusRetailerDetailsS
String title = _retailer?.name ?? LocaleKeys.offerDetails.tr();
bool shouldStartScanning = offerType?.trim().toLowerCase() == "discount";
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(
context,
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
// 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

@ -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: <Widget>[
@ -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: <String>[name]).toText14(
isCenter: true,
color: const Color(0xFF64748B),
),
LocaleKeys.mazayaAccessGrantedDesc.tr(args: <String>[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: <Widget>[
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: <Widget>[
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: <String>[name]).toText12(
color: const Color(0xFF64748B),
),
LocaleKeys.accessIsNowActiveDesc.tr(args: <String>[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>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))],
),
child: Row(
children: <Widget>[
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: <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.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: <Widget>[
@ -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: <Widget>[
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: <String>[name]).toText12(
color: const Color(0xFF64748B),
),
LocaleKeys.shareableAppDownloadLinkDesc.tr(args: <String>[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: <Widget>[
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: <Widget>[
@ -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: <Widget>[
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: <Widget>[
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>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))],
),
child: Row(
children: <Widget>[
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: <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),
),
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<void> _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<void> _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,

@ -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<AppInAppWebViewScreen> {
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,

@ -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) ...[

Loading…
Cancel
Save