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/mazaya_access_result_screen...

548 lines
22 KiB
Dart

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';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class MazayaAccessResultParams {
final bool isGranted;
final String dependentName;
final String? dependentInitials;
final String? downloadUrl;
final String? nationalId;
final String? dateOfBirth;
const MazayaAccessResultParams({required this.isGranted, required this.dependentName, this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth});
}
class _ShieldPainter extends CustomPainter {
final Color color;
final bool isGranted;
_ShieldPainter({required this.color, required this.isGranted});
@override
void paint(Canvas canvas, Size size) {
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;
// Shield path
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) {
// Checkmark path
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 {
// 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);
}
}
@override
bool shouldRepaint(covariant _ShieldPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted;
}
class MazayaAccessResultScreen extends StatelessWidget {
final bool isGranted;
final String dependentName;
final String? dependentInitials;
final String? downloadUrl;
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);
static String getDownloadLink() {
int channel = AppState().postParamsObject?.channel ?? 31;
return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html";
}
// Channel 33 (HMG) gets the HMG-branded guide, everything else gets the CS-branded one -
// same channel check as getDownloadLink() - crossed with the current app language.
static String getShareGuidePdfAssetPath(bool isAr) {
int channel = AppState().postParamsObject?.channel ?? 31;
String brand = channel == 33 ? "HMG" : "CS";
String lang = isAr ? "ar" : "en";
return "assets/pdf/Mazaya Access Guide $brand $lang.pdf";
}
String _getInitials(String? name) {
if (name == null || name.trim().isEmpty) return "D";
List<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList();
if (parts.isEmpty) return "D";
if (parts.length == 1) {
return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase();
}
return "${parts.first[0]}${parts.last[0]}".toUpperCase();
}
@override
Widget build(BuildContext context) {
bool isAr = AppState().isArabic(context);
String fontFamily = isAr ? 'Cairo' : 'Poppins';
// Resolve arguments if passed via ModalRoute
Object? routeArgs = ModalRoute.of(context)?.settings.arguments;
bool effectiveGranted = isGranted;
String effectiveName = dependentName;
String? effectiveInitials = dependentInitials;
String? effectiveUrl = downloadUrl;
String? effectiveNationalId = nationalId;
String? effectiveDateOfBirth = dateOfBirth;
if (routeArgs is MazayaAccessResultParams) {
effectiveGranted = routeArgs.isGranted;
effectiveName = routeArgs.dependentName;
effectiveInitials = routeArgs.dependentInitials;
effectiveUrl = routeArgs.downloadUrl;
effectiveNationalId = routeArgs.nationalId;
effectiveDateOfBirth = routeArgs.dateOfBirth;
} else if (routeArgs is Map<String, dynamic>) {
effectiveGranted = routeArgs['isGranted'] ?? isGranted;
effectiveName = routeArgs['dependentName'] ?? dependentName;
effectiveInitials = routeArgs['dependentInitials'] ?? dependentInitials;
effectiveUrl = routeArgs['downloadUrl'] ?? downloadUrl;
effectiveNationalId = routeArgs['nationalId'] ?? nationalId;
effectiveDateOfBirth = routeArgs['dateOfBirth'] ?? dateOfBirth;
}
if (effectiveName.trim().isEmpty) {
String fallback = AppState().dependentName ?? "";
if (fallback.isNotEmpty) {
effectiveName = fallback;
} else {
effectiveName = isAr ? "التابع" : "Dependent";
}
}
String linkToShare = effectiveUrl ?? getDownloadLink();
String initials = effectiveInitials ?? _getInitials(effectiveName);
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: SafeArea(
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: <Widget>[
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))),
),
const SizedBox(width: 4),
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))),
),
],
),
),
),
),
body: SafeArea(
child: Column(
children: <Widget>[
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),
),
),
_buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""),
],
),
),
);
}
Widget _buildGrantedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, String linkToShare) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 8),
// Shield checkmark badge
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))),
),
const SizedBox(height: 18),
// Title
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)),
const SizedBox(height: 22),
// Access is now active banner
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)),
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 SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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)),
],
),
),
],
),
),
const SizedBox(height: 14),
// Dependent Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
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))],
),
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))),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
const SizedBox(height: 2),
LocaleKeys.dependentAccessActive.tr().toText12(isBold: true, color: const Color(0xFF168A89)),
],
),
),
const Icon(Icons.lock_open_rounded, color: Color(0xFF168A89), size: 20),
],
),
),
const SizedBox(height: 14),
// Shareable App-Download Link Card
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))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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)),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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)),
],
),
),
],
),
const SizedBox(height: 12),
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))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
LocaleKeys.downloadLink.tr().toText11(isBold: true, color: const Color(0xFF168A89)),
const SizedBox(height: 3),
linkToShare.toText12(maxLine: 1, color: const Color(0xFF334155)),
],
),
),
],
),
),
],
);
}
Widget _buildRevokedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 8),
// Shield bad badge
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))),
),
const SizedBox(height: 18),
// Title
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)),
const SizedBox(height: 22),
// Contact your sponsor banner
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)),
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 SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
LocaleKeys.contactYourSponsor.tr().toText14(isBold: true, color: const Color(0xFFED1C24)),
const SizedBox(height: 3),
LocaleKeys.contactYourSponsorDesc.tr().toText12(color: const Color(0xFF64748B)),
],
),
),
],
),
),
const SizedBox(height: 14),
// Dependent Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
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))],
),
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))),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)),
const SizedBox(height: 2),
LocaleKeys.dependentAccessBlocked.tr().toText12(isBold: true, color: const Color(0xFFED1C24)),
],
),
),
const Icon(Icons.lock_outline_rounded, color: Color(0xFFED1C24), size: 20),
],
),
),
],
);
}
// 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;
}
// 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, String dateOfBirth) {
String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}";
}
// 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.
Future<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth);
try {
ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr));
Directory tempDir = await getTemporaryDirectory();
// 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 {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
}
Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) {
if (granted) {
return Container(
color: Colors.white,
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
DefaultButton(
LocaleKeys.shareDownloadLink.tr(),
() => _shareMazayaInvite(isAr, linkToShare, nationalId, dateOfBirth),
trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded,
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
const SizedBox(height: 10),
DefaultButton(
LocaleKeys.copyLink.tr(),
() {
Clipboard.setData(ClipboardData(text: _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth)));
Utils.showToast(LocaleKeys.linkCopied.tr());
},
colors: const <Color>[Colors.white, Colors.white],
border: Border.all(color: const Color(0xFFCBD5E1)),
textColor: const Color(0xFF168A89),
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
const SizedBox(height: 10),
DefaultButton(
LocaleKeys.done.tr(),
() => Navigator.pop(context),
colors: const <Color>[Colors.white, Colors.white],
border: Border.all(color: const Color(0xFFCBD5E1)),
textColor: const Color(0xFF334155),
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
],
),
);
} else {
return Container(
color: Colors.white,
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: DefaultButton(
LocaleKeys.returnToSignIn.tr(),
() => Navigator.pop(context),
colors: const <Color>[Colors.white, Colors.white],
border: Border.all(color: const Color(0xFFCEEFE7), width: 1.2),
textColor: const Color(0xFF168A89),
iconData: isAr ? Icons.arrow_forward_rounded : Icons.arrow_back_rounded,
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
);
}
}
}