|
|
|
@ -158,10 +158,17 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
|
|
|
|
DateTime? dt = DateTime.tryParse(s);
|
|
|
|
DateTime? dt = DateTime.tryParse(s);
|
|
|
|
if (dt != null) return DateTime(dt.year, dt.month, dt.day);
|
|
|
|
if (dt != null) return DateTime(dt.year, dt.month, dt.day);
|
|
|
|
|
|
|
|
|
|
|
|
List<String> formats = <String>['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy'];
|
|
|
|
// DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip
|
|
|
|
|
|
|
|
// that clock-time suffix first, since none of the date-only patterns below can match it
|
|
|
|
|
|
|
|
// with it still attached (parseStrict requires the whole string to match the pattern).
|
|
|
|
|
|
|
|
String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an
|
|
|
|
|
|
|
|
// ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June.
|
|
|
|
|
|
|
|
List<String> formats = <String>['yyyy-MM-dd', 'MM/dd/yyyy', 'dd/MM/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy'];
|
|
|
|
for (String fmt in formats) {
|
|
|
|
for (String fmt in formats) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s);
|
|
|
|
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly);
|
|
|
|
return DateTime(parsed.year, parsed.month, parsed.day);
|
|
|
|
return DateTime(parsed.year, parsed.month, parsed.day);
|
|
|
|
} catch (_) {}
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -171,19 +178,24 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
|
|
|
|
|
|
|
|
|
|
|
|
String _formatDob(DateTime? dob, String? raw) {
|
|
|
|
String _formatDob(DateTime? dob, String? raw) {
|
|
|
|
if (dob != null) {
|
|
|
|
if (dob != null) {
|
|
|
|
return DateFormat('dd MMM yyyy', 'en_US').format(dob);
|
|
|
|
return DateFormat('dd-MM-yyyy', 'en_US').format(dob);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (raw != null && raw.trim().isNotEmpty) {
|
|
|
|
if (raw != null && raw.trim().isNotEmpty) {
|
|
|
|
return raw.split(' ').first;
|
|
|
|
return raw.split(' ').first.replaceAll('/', '-');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "-";
|
|
|
|
return "-";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Masks everything but the last 3 digits, e.g. "2561615457" -> "*******457" - used both for
|
|
|
|
|
|
|
|
// the on-screen Key Details display and the share message, so the full ID is never shown or
|
|
|
|
|
|
|
|
// sent in plain text anywhere.
|
|
|
|
String _formatNationalId(String? nationalId) {
|
|
|
|
String _formatNationalId(String? nationalId) {
|
|
|
|
if (nationalId == null || nationalId.trim().isEmpty) return "-";
|
|
|
|
if (nationalId == null || nationalId.trim().isEmpty) return "-";
|
|
|
|
String clean = nationalId.replaceAll('-', '').trim();
|
|
|
|
String clean = nationalId.replaceAll('-', '').trim();
|
|
|
|
String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean;
|
|
|
|
if (clean.length <= 3) return clean;
|
|
|
|
return LocaleKeys.idEnding.tr(args: <String>[last4]);
|
|
|
|
String lastDigits = clean.substring(clean.length - 3);
|
|
|
|
|
|
|
|
String mask = List<String>.filled(clean.length - 3, '*').join();
|
|
|
|
|
|
|
|
return '$mask$lastDigits';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _onToggleAccess() {
|
|
|
|
void _onToggleAccess() {
|
|
|
|
@ -250,24 +262,34 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
|
|
|
|
if (dob != null) return DateFormat('dd-MM-yyyy').format(dob);
|
|
|
|
if (dob != null) return DateFormat('dd-MM-yyyy').format(dob);
|
|
|
|
String raw = _contact?.dATEOFBIRTH?.trim() ?? "";
|
|
|
|
String raw = _contact?.dATEOFBIRTH?.trim() ?? "";
|
|
|
|
if (raw.isEmpty) return "";
|
|
|
|
if (raw.isEmpty) return "";
|
|
|
|
if (raw.contains('T')) return raw.split('T').first;
|
|
|
|
if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-');
|
|
|
|
if (raw.contains(' ')) return raw.split(' ').first;
|
|
|
|
if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-');
|
|
|
|
return raw;
|
|
|
|
return raw.replaceAll('/', '-');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Same message built for both "Share Download Link" and "Copy Link" - they must always
|
|
|
|
// 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.
|
|
|
|
// carry identical content, so this is the single source of truth for it.
|
|
|
|
String _buildMazayaInviteMessage(String linkToShare) {
|
|
|
|
String _buildMazayaInviteMessage(String linkToShare) {
|
|
|
|
String? nationalId = _contact?.nATIONALIDENTIFIER?.trim();
|
|
|
|
String? nationalId = _contact?.nATIONALIDENTIFIER?.trim();
|
|
|
|
String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
|
|
|
|
String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
|
|
|
|
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}";
|
|
|
|
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.
|
|
|
|
// 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.
|
|
|
|
// Falls back to sharing just the text if the PDF can't be loaded/copied for any reason.
|
|
|
|
Future<void> _shareMazayaInvite(bool isAr, String linkToShare) async {
|
|
|
|
// Text-only share (the invite message + link). Kept separate from the guide share below
|
|
|
|
|
|
|
|
// because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
|
|
|
|
|
|
|
|
// attached to the same share - splitting them guarantees the message itself is always visible.
|
|
|
|
|
|
|
|
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare) async {
|
|
|
|
String message = _buildMazayaInviteMessage(linkToShare);
|
|
|
|
String message = _buildMazayaInviteMessage(linkToShare);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await SharePlus.instance.share(ShareParams(text: message));
|
|
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// File-only share of the bundled access guide PDF - no accompanying text, so every target
|
|
|
|
|
|
|
|
// (including WhatsApp) just attaches the file with nothing to drop.
|
|
|
|
|
|
|
|
Future<void> _shareMazayaAccessGuide(bool isAr) async {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr));
|
|
|
|
ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr));
|
|
|
|
Directory tempDir = await getTemporaryDirectory();
|
|
|
|
Directory tempDir = await getTemporaryDirectory();
|
|
|
|
@ -275,13 +297,9 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
|
|
|
|
File pdfFile = File('${tempDir.path}/$localFileName');
|
|
|
|
File pdfFile = File('${tempDir.path}/$localFileName');
|
|
|
|
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
|
|
|
|
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
|
|
|
|
|
|
|
|
|
|
|
|
await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)]));
|
|
|
|
await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
|
|
|
|
} catch (_) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await SharePlus.instance.share(ShareParams(text: message));
|
|
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
@ -540,9 +558,27 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
// Share download link - same bottom-sticky button convention used elsewhere in
|
|
|
|
// Two separate share actions (not one combined text+file share) - same bottom-sticky
|
|
|
|
// the app (e.g. "Join Marathon"), shown once access is granted.
|
|
|
|
// container convention used elsewhere in the app (e.g. "Join Marathon"), shown once
|
|
|
|
if (isAllowed) DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInvite(isAr, linkToShare)).insideContainer,
|
|
|
|
// access is granted.
|
|
|
|
|
|
|
|
if (isAllowed)
|
|
|
|
|
|
|
|
Row(
|
|
|
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
|
|
|
// Secondary - same bg/border/text colors as the name+relation card at the top
|
|
|
|
|
|
|
|
// of this screen, for visual consistency within this page.
|
|
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
|
|
child: DefaultButton(
|
|
|
|
|
|
|
|
LocaleKeys.shareAccessGuide.tr(),
|
|
|
|
|
|
|
|
() => _shareMazayaAccessGuide(isAr),
|
|
|
|
|
|
|
|
colors: const <Color>[Color(0xFFE8F7F2), Color(0xFFE8F7F2)],
|
|
|
|
|
|
|
|
border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2),
|
|
|
|
|
|
|
|
textColor: const Color(0xFF168A89),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
|
|
Expanded(child: DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInviteLink(isAr, linkToShare))),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
).insideContainer,
|
|
|
|
],
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|