You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mohemm-flutter-app/lib/ui/profile/dependent_details_screen.dart

862 lines
35 KiB
Dart

3 weeks ago
import 'dart:io';
3 weeks ago
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
3 weeks ago
import 'package:flutter/services.dart';
3 weeks ago
import 'package:mohem_flutter_app/api/profile_api_client.dart';
3 weeks ago
import 'package:mohem_flutter_app/app_state/app_state.dart';
3 weeks ago
import 'package:mohem_flutter_app/classes/utils.dart';
2 weeks ago
import 'package:mohem_flutter_app/classes/consts.dart';
3 weeks ago
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart';
import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
3 weeks ago
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
3 weeks ago
class _ShieldIconPainter extends CustomPainter {
final Color color;
final bool isGranted;
_ShieldIconPainter({required this.color, required this.isGranted});
@override
void paint(Canvas canvas, Size size) {
3 weeks ago
Paint paint =
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
3 weeks ago
double w = size.width;
double h = size.height;
Path path = Path();
path.moveTo(w * 0.5, h * 0.1);
path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38);
path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92);
path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38);
path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1);
path.close();
canvas.drawPath(path, paint);
if (isGranted) {
Path checkPath = Path();
checkPath.moveTo(w * 0.34, h * 0.49);
checkPath.lineTo(w * 0.46, h * 0.61);
checkPath.lineTo(w * 0.66, h * 0.39);
canvas.drawPath(checkPath, paint);
} else {
2 weeks ago
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,
);
3 weeks ago
}
}
@override
2 weeks ago
bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) =>
color != oldDelegate.color || isGranted != oldDelegate.isGranted;
3 weeks ago
}
class DependentDetailsScreen extends StatefulWidget {
final GetEmployeeContactsList? contact;
// Null = just viewing (arrived by tapping the card, no top badge). True/false = this screen
// is being opened right after a successful grant/revoke performed elsewhere (e.g. the
// dependents list's inline "Allow/Revoke Mazaya Access" link), so the matching badge shows
// on first build. Toggling from within this screen itself (_performToggleAccess) sets the
3 weeks ago
// equivalent local state directly instead of needing this.
final bool? recentAction;
3 weeks ago
2 weeks ago
const DependentDetailsScreen({Key? key, this.contact, this.recentAction})
: super(key: key);
3 weeks ago
@override
_DependentDetailsScreenState createState() => _DependentDetailsScreenState();
}
class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
GetEmployeeContactsList? _contact;
bool _argsLoaded = false;
// null = just viewing (arrived by tapping the card, no badge) - true = just granted here,
// false = just revoked here. Set once per toggle in _performToggleAccess; a tap-only visit
// never sets it, so the top badge only ever reflects an action just taken on this screen.
bool? _recentAction;
3 weeks ago
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_argsLoaded) {
if (widget.contact != null) {
_contact = widget.contact;
} else {
Object? args = ModalRoute.of(context)?.settings.arguments;
if (args is GetEmployeeContactsList) {
_contact = args;
} else if (args is Map && args['contact'] is GetEmployeeContactsList) {
_contact = args['contact'] as GetEmployeeContactsList;
}
}
_recentAction = widget.recentAction;
3 weeks ago
_argsLoaded = true;
}
}
bool get _isAccessAllowed {
if (_contact == null) return false;
2 weeks ago
String? flag =
_contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString();
3 weeks ago
return flag?.trim().toUpperCase() == 'Y';
}
bool get _isNhicVerified {
if (_contact == null) return false;
return _contact!.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y';
}
3 weeks ago
String _getInitials(String? name) {
if (name == null || name.trim().isEmpty) return "D";
2 weeks ago
List<String> parts =
name
.trim()
.split(RegExp(r'\s+'))
.where((String p) => p.isNotEmpty)
.toList();
3 weeks ago
if (parts.isEmpty) return "D";
if (parts.length == 1) {
2 weeks ago
return parts.first.length >= 2
? parts.first.substring(0, 2).toUpperCase()
: parts.first.toUpperCase();
3 weeks ago
}
return "${parts.first[0]}${parts.last[0]}".toUpperCase();
}
String _getFirstName(String? name) {
if (name == null || name.trim().isEmpty) return "";
2 weeks ago
List<String> parts =
name
.trim()
.split(RegExp(r'\s+'))
.where((String p) => p.isNotEmpty)
.toList();
3 weeks ago
if (parts.isEmpty) return "";
return parts.first;
}
DateTime? _parseBirthDate(String? dateStr) {
if (dateStr == null || dateStr.trim().isEmpty) return null;
String s = dateStr.trim();
RegExp dateRegex = RegExp(r'/?Date\((-?\d+)(?:[+-]\d+)?\)/?');
Match? match = dateRegex.firstMatch(s);
if (match != null) {
try {
int millis = int.parse(match.group(1)!);
2 weeks ago
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(
millis,
isUtc: true,
);
3 weeks ago
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) {
try {
int millis = int.parse(s);
2 weeks ago
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(
millis,
isUtc: true,
);
3 weeks ago
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
DateTime? dt = DateTime.tryParse(s);
if (dt != null) return DateTime(dt.year, dt.month, dt.day);
// DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip
// that clock-time suffix first, since none of the date-only patterns below can match it
// with it still attached (parseStrict requires the whole string to match the pattern).
2 weeks ago
String dateOnly =
s
.replaceFirst(
RegExp(
r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$',
caseSensitive: false,
),
'',
)
.trim();
// MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an
// ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June.
2 weeks ago
List<String> formats = <String>[
'yyyy-MM-dd',
'MM/dd/yyyy',
'dd/MM/yyyy',
'dd-MM-yyyy',
'yyyy/MM/dd',
'dd-MMM-yyyy',
'dd MMM yyyy',
];
3 weeks ago
for (String fmt in formats) {
try {
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly);
3 weeks ago
return DateTime(parsed.year, parsed.month, parsed.day);
} catch (_) {}
}
return null;
}
String _formatDob(DateTime? dob, String? raw) {
if (dob != null) {
return DateFormat('dd-MM-yyyy', 'en_US').format(dob);
3 weeks ago
}
if (raw != null && raw.trim().isNotEmpty) {
return raw.split(' ').first.replaceAll('/', '-');
3 weeks ago
}
return "-";
}
// Masks everything but the last 3 digits, e.g. "2561615457" -> "*******457" - used both for
// the on-screen Key Details display and the share message, so the full ID is never shown or
// sent in plain text anywhere.
3 weeks ago
String _formatNationalId(String? nationalId) {
if (nationalId == null || nationalId.trim().isEmpty) return "-";
String clean = nationalId.replaceAll('-', '').trim();
if (clean.length <= 3) return clean;
String lastDigits = clean.substring(clean.length - 3);
String mask = List<String>.filled(clean.length - 3, '*').join();
return '$mask$lastDigits';
3 weeks ago
}
void _onToggleAccess() {
if (_contact == null) return;
bool isAllowed = _isAccessAllowed;
String dependentName = _contact!.cONTACTNAME ?? "";
ConfirmBottomSheet.show(
context,
2 weeks ago
title:
isAllowed
? LocaleKeys.revokeMazayaAccess.tr()
: LocaleKeys.allowMazayaAccess.tr(),
message:
isAllowed
? LocaleKeys.confirmRevokeMazayaAccess.tr(
args: <String>[dependentName],
)
: LocaleKeys.confirmGrantMazayaAccess.tr(
args: <String>[dependentName],
),
icon:
isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline,
3 weeks ago
iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89),
confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(),
onConfirm: _performToggleAccess,
);
}
Future<void> _performToggleAccess() async {
if (_contact == null) return;
bool isAllowed = _isAccessAllowed;
String action = isAllowed ? "Revoke" : "Grant";
try {
Utils.showLoading(context);
await ProfileApiClient().processContactMazayaAccess(
accessAction: action,
contactRelationshipId: _contact!.cONTACTRELATIONSHIPID,
contactPersonId: _contact!.cONTACTPERSONID,
personId: _contact!.pERSONID,
patientIdentificationId: _contact!.nATIONALIDENTIFIER,
// DATE_OF_BIRTH, not the separate P_BIRTH_DATE response field - that one stays null
// for every contact until NHIC verification happens, so it's never a usable source here.
patientBirthDate: _contact!.dATEOFBIRTH,
nhicVerifiedFlag: _contact!.pNHICVERIFIEDFLAG,
3 weeks ago
);
setState(() {
_contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N";
_contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG;
// Top badge reflects whichever action was just taken on this screen, not just "was
// granted at some point" - a later revoke flips it so a stale grant badge never lingers.
_recentAction = action == "Grant";
3 weeks ago
});
Utils.hideLoading(context);
3 weeks ago
// Stay on this same screen either way - it already reflects the new access state via
// setState above (status badge, top banner, action button, and the share-link section
// all update in place), so tapping a dependent card and successfully toggling access
// both land on the exact same screen instead of a separate "success" screen. The top
// badge itself is the confirmation now, so no toast on top of it.
3 weeks ago
} catch (ex) {
Utils.hideLoading(context);
Utils.handleException(ex, context, null);
}
}
3 weeks ago
// Full (unmasked) DOB for the share message - distinct from _formatDob/_formatNationalId
// above, which are for the masked on-screen Key Details display.
String _formatDobForShare() {
DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH);
if (dob != null) return DateFormat('dd-MM-yyyy').format(dob);
String raw = _contact?.dATEOFBIRTH?.trim() ?? "";
if (raw.isEmpty) return "";
if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-');
if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-');
return raw.replaceAll('/', '-');
3 weeks ago
}
// Same message built for both "Share Download Link" and "Copy Link" - they must always
// carry identical content, so this is the single source of truth for it.
String _buildMazayaInviteMessage(String linkToShare) {
String? nationalId = _contact?.nATIONALIDENTIFIER?.trim();
2 weeks ago
String nationalIdLine =
(nationalId == null || nationalId.isEmpty)
? LocaleKeys.nationalIdOrIqamaFallback.tr()
: "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
3 weeks ago
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}";
}
// Shares the invite text together with the language-appropriate steps PDF from assets.
// Falls back to sharing just the text if the PDF can't be loaded/copied for any reason.
// Text-only share (the invite message + link). Kept separate from the guide share below
// because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
// attached to the same share - splitting them guarantees the message itself is always visible.
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare) async {
3 weeks ago
String message = _buildMazayaInviteMessage(linkToShare);
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
3 weeks ago
// File-only share of the bundled access guide PDF - no accompanying text, so every target
// (including WhatsApp) just attaches the file with nothing to drop.
Future<void> _shareMazayaAccessGuide(bool isAr) async {
3 weeks ago
try {
2 weeks ago
ByteData bytes = await rootBundle.load(
MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr),
);
3 weeks ago
Directory tempDir = await getTemporaryDirectory();
2 weeks ago
String localFileName =
isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf";
3 weeks ago
File pdfFile = File('${tempDir.path}/$localFileName');
2 weeks ago
await pdfFile.writeAsBytes(
bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
);
3 weeks ago
await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
} catch (_) {}
3 weeks ago
}
3 weeks ago
@override
Widget build(BuildContext context) {
DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH);
String initials = _getInitials(_contact?.cONTACTNAME);
String fullName = _contact?.cONTACTNAME ?? "";
String firstName = _getFirstName(_contact?.cONTACTNAME);
2 weeks ago
String relation =
(_contact?.rELATIONSHIP != null &&
_contact!.rELATIONSHIP!.trim().isNotEmpty)
? _contact!.rELATIONSHIP!.trim()
: (_contact?.cONTACTTYPE?.trim() ?? "");
3 weeks ago
String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER);
String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH);
bool isAllowed = _isAccessAllowed;
3 weeks ago
bool isAr = AppState().isArabic(context);
String linkToShare = MazayaAccessResultScreen.getDownloadLink();
3 weeks ago
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: SafeArea(
child: Container(
height: 56,
color: const Color(0xFFF8FAFC),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: <Widget>[
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context),
2 weeks ago
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20,
color: Color(0xFF1E293B),
),
),
3 weeks ago
),
const SizedBox(width: 8),
2 weeks ago
LocaleKeys.dependentDetails.tr().toText20(
color: const Color(0xFF1E293B),
),
3 weeks ago
const Spacer(),
InkWell(
borderRadius: BorderRadius.circular(20),
2 weeks ago
onTap:
() => Navigator.popUntil(
context,
ModalRoute.withName(
ApiConsts.isDependetLogin
? AppRoutes.mazayaDiscover
: AppRoutes.dashboard,
),
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.home_outlined,
size: 24,
color: Color(0xFF1E293B),
),
),
3 weeks ago
),
],
),
),
),
),
body: SafeArea(
3 weeks ago
child: Column(
children: <Widget>[
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
3 weeks ago
children: <Widget>[
// Shown only right after a grant/revoke action taken on this screen - not
// when just viewing a dependent by tapping into this screen (_recentAction
// is null in that case).
if (_recentAction != null) ...<Widget>[
3 weeks ago
Center(
child: Container(
width: 72,
height: 72,
2 weeks ago
decoration: BoxDecoration(
color:
_recentAction!
? const Color(0xFFE6F7F2)
: const Color(0xFFFEECEB),
shape: BoxShape.circle,
),
child: Center(
child: CustomPaint(
size: const Size(36, 36),
2 weeks ago
painter: _ShieldIconPainter(
color:
_recentAction!
? const Color(0xFF168A89)
: const Color(0xFFED1C24),
isGranted: _recentAction!,
),
),
),
3 weeks ago
),
),
3 weeks ago
const SizedBox(height: 10),
Center(
2 weeks ago
child: (_recentAction!
? LocaleKeys.mazayaAccessGranted.tr()
: LocaleKeys.mazayaAccessRevoked.tr())
.toText16(
weight: FontWeight.w500,
color:
_recentAction!
? const Color(0xFF168A89)
: const Color(0xFFED1C24),
),
),
3 weeks ago
const SizedBox(height: 16),
],
3 weeks ago
3 weeks ago
// Profile Card (Mint Green)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
2 weeks ago
decoration: BoxDecoration(
color: const Color(0xFFE8F7F2),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFBCE6D7),
width: 1.2,
),
),
3 weeks ago
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
3 weeks ago
children: <Widget>[
3 weeks ago
// Avatar circle with initials
Container(
width: 52,
height: 52,
2 weeks ago
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: initials.toText17(
color: const Color(0xFF168A89),
),
),
3 weeks ago
),
3 weeks ago
const SizedBox(width: 14),
// Dependent Name & Relation
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
2 weeks ago
fullName.toText16(
maxlines: 2,
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
if (relation.isNotEmpty) ...<Widget>[
const SizedBox(height: 3),
relation.toText12(
maxLine: 1,
weight: FontWeight.w500,
color: const Color(0xFF168A89),
),
],
3 weeks ago
],
3 weeks ago
),
3 weeks ago
),
// Verified badge on right - only when NHIC has actually verified this contact
2 weeks ago
if (_isNhicVerified) ...<Widget>[
const SizedBox(width: 8),
const Icon(
Icons.verified_rounded,
color: Color(0xFF059669),
size: 24,
),
],
3 weeks ago
],
),
),
3 weeks ago
const SizedBox(height: 16),
3 weeks ago
3 weeks ago
// Key details Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
2 weeks ago
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
3 weeks ago
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.keyDetails.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
if (_isNhicVerified)
LocaleKeys.verified.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF059669),
),
3 weeks ago
],
),
const SizedBox(height: 14),
2 weeks ago
const Divider(
height: 1,
thickness: 1,
color: Color(0xFFF1F5F9),
),
3 weeks ago
const SizedBox(height: 14),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.nationalId.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
formattedId.toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.dateOfBirth.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
formattedDob.toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
],
),
],
),
3 weeks ago
),
3 weeks ago
const SizedBox(height: 24),
3 weeks ago
3 weeks ago
// Services & actions section header
2 weeks ago
LocaleKeys.servicesAndActions.tr().toText17(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
const SizedBox(height: 4),
2 weeks ago
LocaleKeys.startDependentFlowsDesc.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
3 weeks ago
3 weeks ago
const SizedBox(height: 12),
// Mazaya access Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
2 weeks ago
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
3 weeks ago
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Header Row: Mazaya access + Badge
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.mazayaAccess.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
Container(
2 weeks ago
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 3,
3 weeks ago
),
2 weeks ago
decoration: BoxDecoration(
color:
isAllowed
? const Color(0xFFECFDF5)
: const Color(0xFFFEE2E2),
borderRadius: BorderRadius.circular(12),
),
child: (isAllowed
? LocaleKeys.active.tr()
: LocaleKeys.accessOff.tr())
.toText12(
weight: FontWeight.w500,
color:
isAllowed
? const Color(0xFF059669)
: const Color(0xFFEF4444),
),
3 weeks ago
),
],
3 weeks ago
),
3 weeks ago
const SizedBox(height: 6),
2 weeks ago
LocaleKeys.allowDependentMazayaOffersDesc
.tr(
args: <String>[
firstName.isNotEmpty ? firstName : fullName,
],
)
.toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
3 weeks ago
const SizedBox(height: 16),
2 weeks ago
const Divider(
height: 1,
thickness: 1,
color: Color(0xFFF1F5F9),
),
3 weeks ago
const SizedBox(height: 14),
// Login method row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.loginMethod.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
LocaleKeys.nationalIdAndDob.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
],
),
const SizedBox(height: 12),
// Verification row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2 weeks ago
LocaleKeys.verification.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
LocaleKeys.sponsorOtp.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
3 weeks ago
],
3 weeks ago
),
3 weeks ago
const SizedBox(height: 20),
3 weeks ago
3 weeks ago
// Action Button: Allow Mazaya Access (Teal) or Revoke Mazaya Access (Red Outline)
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: _onToggleAccess,
child: Container(
height: 48,
width: double.infinity,
decoration: BoxDecoration(
2 weeks ago
color:
isAllowed
? const Color(0xFFFEF2F2)
: const Color(0xFF22B0A6),
3 weeks ago
borderRadius: BorderRadius.circular(12),
2 weeks ago
border:
isAllowed
? Border.all(
color: const Color(0xFFFECACA),
width: 1.2,
)
: null,
3 weeks ago
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
2 weeks ago
SizedBox(
width: 18,
height: 18,
child: CustomPaint(
painter: _ShieldIconPainter(
color:
isAllowed
? const Color(0xFFDC2626)
: Colors.white,
isGranted: !isAllowed,
),
),
3 weeks ago
),
2 weeks ago
const SizedBox(width: 8),
(isAllowed
? LocaleKeys.revokeMazayaAccess.tr()
: LocaleKeys.allowMazayaAccess.tr())
.toText16(
weight: FontWeight.w500,
color:
isAllowed
? const Color(0xFFDC2626)
: Colors.white,
),
3 weeks ago
],
3 weeks ago
),
),
3 weeks ago
),
if (isAllowed) ...<Widget>[
const SizedBox(height: 12),
2 weeks ago
Center(
child: LocaleKeys.revokingPreventsAccessDesc
.tr()
.toText11(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
isCenter: true,
),
),
3 weeks ago
],
3 weeks ago
],
3 weeks ago
),
),
],
),
),
3 weeks ago
),
// Two separate share actions (not one combined text+file share) - same bottom-sticky
// container convention used elsewhere in the app (e.g. "Join Marathon"), shown once
// access is granted.
if (isAllowed)
Row(
children: <Widget>[
// Secondary - same bg/border/text colors as the name+relation card at the top
// of this screen, for visual consistency within this page.
Expanded(
child: DefaultButton(
LocaleKeys.shareAccessGuide.tr(),
() => _shareMazayaAccessGuide(isAr),
2 weeks ago
colors: const <Color>[
Color(0xFFE8F7F2),
Color(0xFFE8F7F2),
],
border: Border.all(
color: const Color(0xFFBCE6D7),
width: 1.2,
),
textColor: const Color(0xFF168A89),
),
),
const SizedBox(width: 12),
2 weeks ago
Expanded(
child: DefaultButton(
LocaleKeys.shareDownloadLink.tr(),
() => _shareMazayaInviteLink(isAr, linkToShare),
),
),
],
).insideContainer,
3 weeks ago
],
3 weeks ago
),
),
);
}
}