From cbb2f9f33d0a68f4ad7b54a3fa3c84bf7a7aa5c9 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Thu, 28 May 2026 15:49:09 +0530 Subject: [PATCH] ecard download for android --- android/app/build.gradle | 2 + android/app/src/main/AndroidManifest.xml | 4 +- .../nh/ind/nhance/benefits/MainActivity.kt | 29 +++- lib/main.dart | 2 + lib/pages/helpers/app_permission_service.dart | 16 ++ .../helpers/custom_download_snackbar.dart | 34 ++++ lib/pages/helpers/download_result.dart | 23 +++ .../ecard_download_notification_service.dart | 72 ++++++++ lib/pages/helpers/ecard_download_service.dart | 24 +++ .../helpers/ecard_download_service_io.dart | 116 +++++++++++++ .../helpers/ecard_download_service_stub.dart | 11 ++ .../helpers/ecard_download_service_web.dart | 18 ++ lib/pages/helpers/file_download_common.dart | 10 ++ lib/pages/helpers/file_download_helper.dart | 25 +++ lib/pages/helpers/file_download_io.dart | 52 ++++++ lib/pages/helpers/file_download_stub.dart | 10 ++ lib/pages/helpers/file_download_web.dart | 48 ++++++ lib/pages/postEnrollment/policies.dart | 157 ++++++++++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 2 + 22 files changed, 627 insertions(+), 37 deletions(-) create mode 100644 lib/pages/helpers/app_permission_service.dart create mode 100644 lib/pages/helpers/custom_download_snackbar.dart create mode 100644 lib/pages/helpers/download_result.dart create mode 100644 lib/pages/helpers/ecard_download_notification_service.dart create mode 100644 lib/pages/helpers/ecard_download_service.dart create mode 100644 lib/pages/helpers/ecard_download_service_io.dart create mode 100644 lib/pages/helpers/ecard_download_service_stub.dart create mode 100644 lib/pages/helpers/ecard_download_service_web.dart create mode 100644 lib/pages/helpers/file_download_common.dart create mode 100644 lib/pages/helpers/file_download_helper.dart create mode 100644 lib/pages/helpers/file_download_io.dart create mode 100644 lib/pages/helpers/file_download_stub.dart create mode 100644 lib/pages/helpers/file_download_web.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 15dbe91..b9d1725 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,6 +41,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 + coreLibraryDesugaringEnabled true } kotlinOptions { @@ -133,4 +134,5 @@ dependencies { implementation "com.google.firebase:firebase-messaging:24.1.1" implementation("com.google.firebase:firebase-appcheck-playintegrity") implementation 'androidx.browser:browser:1.3.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f6fe962..408af73 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,11 @@ - + + + diff --git a/android/app/src/main/kotlin/nh/ind/nhance/benefits/MainActivity.kt b/android/app/src/main/kotlin/nh/ind/nhance/benefits/MainActivity.kt index b0157a7..daccc4a 100755 --- a/android/app/src/main/kotlin/nh/ind/nhance/benefits/MainActivity.kt +++ b/android/app/src/main/kotlin/nh/ind/nhance/benefits/MainActivity.kt @@ -1,5 +1,32 @@ package nh.ind.nhance.benefits +import android.media.MediaScannerConnection +import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.plugin.common.MethodChannel -class MainActivity: FlutterFragmentActivity() +class MainActivity: FlutterFragmentActivity() { + private val mediaScanChannel = "nhance/media_scan" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, mediaScanChannel) + .setMethodCallHandler { call, result -> + if (call.method == "scanFile") { + val path = call.argument("path") + if (path.isNullOrBlank()) { + result.error("invalid_path", "Path is required", null) + return@setMethodCallHandler + } + MediaScannerConnection.scanFile( + applicationContext, + arrayOf(path), + null + ) { _, _ -> } + result.success(true) + } else { + result.notImplemented() + } + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 21b63a6..aa59282 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,6 +40,7 @@ import 'package:nhance_app_pwa/pages/session/SetPinBiometric.dart'; import 'package:nhance_app_pwa/pages/session/changePin.dart'; import 'package:nhance_app_pwa/pages/session/settingUpPinAndBiometric.dart'; import 'package:nhance_app_pwa/pages/setPassword.dart'; +import 'package:nhance_app_pwa/pages/helpers/app_permission_service.dart'; import 'package:nhance_app_pwa/pages/verify.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -207,6 +208,7 @@ void _applyAppFlavorFromDartDefine() { Future startApp() async { WidgetsFlutterBinding.ensureInitialized(); _applyAppFlavorFromDartDefine(); + await AppPermissionService.requestInitialPermissions(); await TokenService.clearLegacyWebAuthFromSharedPreferences(); diff --git a/lib/pages/helpers/app_permission_service.dart b/lib/pages/helpers/app_permission_service.dart new file mode 100644 index 0000000..8e51340 --- /dev/null +++ b/lib/pages/helpers/app_permission_service.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AppPermissionService { + AppPermissionService._(); + + static Future requestInitialPermissions() async { + if (kIsWeb || !Platform.isAndroid) return; + + await Permission.notification.request(); + // Keep app-open flow lightweight: no redirection to Android settings page. + await Permission.storage.request(); + } +} diff --git a/lib/pages/helpers/custom_download_snackbar.dart b/lib/pages/helpers/custom_download_snackbar.dart new file mode 100644 index 0000000..50273b6 --- /dev/null +++ b/lib/pages/helpers/custom_download_snackbar.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class CustomDownloadSnackbar { + static void show( + BuildContext context, { + required String message, + Duration duration = const Duration(seconds: 2), + }) { + final messenger = ScaffoldMessenger.of(context); + messenger + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.fromLTRB(16, 0, 16, 24), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + duration: duration, + backgroundColor: const Color(0xFF1F1F1F), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + dismissDirection: DismissDirection.down, + ), + ); + } +} diff --git a/lib/pages/helpers/download_result.dart b/lib/pages/helpers/download_result.dart new file mode 100644 index 0000000..c5b22e9 --- /dev/null +++ b/lib/pages/helpers/download_result.dart @@ -0,0 +1,23 @@ +class DownloadResult { + final bool success; + final String? message; + final String? savedPath; + + const DownloadResult({ + required this.success, + this.message, + this.savedPath, + }); + + factory DownloadResult.success({String? message, String? savedPath}) { + return DownloadResult( + success: true, + message: message ?? 'Download complete', + savedPath: savedPath, + ); + } + + factory DownloadResult.failure(String message) { + return DownloadResult(success: false, message: message); + } +} diff --git a/lib/pages/helpers/ecard_download_notification_service.dart b/lib/pages/helpers/ecard_download_notification_service.dart new file mode 100644 index 0000000..a067455 --- /dev/null +++ b/lib/pages/helpers/ecard_download_notification_service.dart @@ -0,0 +1,72 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:open_filex/open_filex.dart'; + +class EcardDownloadNotificationService { + EcardDownloadNotificationService._(); + + static final FlutterLocalNotificationsPlugin _plugin = + FlutterLocalNotificationsPlugin(); + static bool _initialized = false; + + static Future ensureInitialized() async { + if (_initialized || kIsWeb) return; + + const androidSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings(); + const settings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await _plugin.initialize( + settings, + onDidReceiveNotificationResponse: (NotificationResponse response) async { + final filePath = response.payload; + if (filePath == null || filePath.isEmpty) return; + await OpenFilex.open(filePath); + }, + ); + + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + await _plugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions(alert: true, badge: true, sound: true); + + _initialized = true; + } + + static Future showDownloadCompleted({ + required String filePath, + required String fileName, + }) async { + if (kIsWeb) return; + await ensureInitialized(); + + const androidDetails = AndroidNotificationDetails( + 'ecard_downloads_channel', + 'E-Card Downloads', + channelDescription: 'Notifications for completed eCard downloads', + importance: Importance.max, + priority: Priority.high, + styleInformation: DefaultStyleInformation(true, true), + ); + const iosDetails = DarwinNotificationDetails(); + + await _plugin.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'eCard downloaded', + 'Tap to open $fileName', + const NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ), + payload: filePath, + ); + } +} diff --git a/lib/pages/helpers/ecard_download_service.dart b/lib/pages/helpers/ecard_download_service.dart new file mode 100644 index 0000000..c78a428 --- /dev/null +++ b/lib/pages/helpers/ecard_download_service.dart @@ -0,0 +1,24 @@ +import 'download_result.dart'; +import 'ecard_download_service_stub.dart' + if (dart.library.io) 'ecard_download_service_io.dart' + if (dart.library.html) 'ecard_download_service_web.dart' as impl; + +class EcardDownloadService { + const EcardDownloadService(); + + Future ensureStorageAccess() { + return impl.ensureStorageAccessImpl(); + } + + Future downloadEcard({ + required String url, + required String fileName, + Map? headers, + }) { + return impl.downloadEcardImpl( + url: url, + fileName: fileName, + headers: headers, + ); + } +} diff --git a/lib/pages/helpers/ecard_download_service_io.dart b/lib/pages/helpers/ecard_download_service_io.dart new file mode 100644 index 0000000..8b21711 --- /dev/null +++ b/lib/pages/helpers/ecard_download_service_io.dart @@ -0,0 +1,116 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter/services.dart'; + +import 'download_result.dart'; + +final Dio _dio = Dio(); +const MethodChannel _mediaScanChannel = MethodChannel('nhance/media_scan'); + +Future ensureStorageAccessImpl() async { + if (Platform.isAndroid) { + // Request notification permission (best-effort for Android 13+). + await Permission.notification.request(); + + // Prefer broad access for writing to public Download directory. + if (await Permission.manageExternalStorage.isGranted) return true; + final manage = await Permission.manageExternalStorage.request(); + if (manage.isGranted) return true; + + // Fallback for older devices. + final storage = await Permission.storage.request(); + return storage.isGranted; + } + return true; +} + +Future downloadEcardImpl({ + required String url, + required String fileName, + Map? headers, +}) async { + try { + final hasPermission = await ensureStorageAccessImpl(); + if (!hasPermission) { + return DownloadResult.failure( + 'Storage permission required', + ); + } + + final Directory downloadDir = await _resolveDownloadDirectory(); + if (!await downloadDir.exists()) { + await downloadDir.create(recursive: true); + } + + final safeFileName = _sanitizeFileName(fileName); + final filePath = '${downloadDir.path}/$safeFileName'; + final file = File(filePath); + if (await file.exists()) { + await file.delete(); + } + + await _dio.download( + url, + filePath, + options: Options( + headers: headers, + responseType: ResponseType.bytes, + ), + deleteOnError: true, + ); + + if (!await file.exists()) { + return DownloadResult.failure('Download failed'); + } + final length = await file.length(); + if (length == 0) { + await file.delete(); + return DownloadResult.failure('Download failed'); + } + await _scanFileForAndroidMediaIndex(filePath); + + return DownloadResult.success( + savedPath: filePath, + message: 'File saved under Download/$safeFileName', + ); + } on FileSystemException { + return DownloadResult.failure('Storage permission issue'); + } on DioException catch (e) { + final hasStatusCode = e.response?.statusCode != null; + final message = hasStatusCode + ? 'Download failed (${e.response!.statusCode})' + : 'Download failed'; + return DownloadResult.failure(message); + } catch (_) { + return DownloadResult.failure('Download failed'); + } +} + +String _sanitizeFileName(String fileName) { + final replaced = fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); + if (replaced.isEmpty) return 'ecard.pdf'; + return replaced.toLowerCase().endsWith('.pdf') ? replaced : '$replaced.pdf'; +} + +Future _resolveDownloadDirectory() async { + if (Platform.isAndroid) { + final dir = Directory('/storage/emulated/0/Download'); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + return getApplicationDocumentsDirectory(); +} + +Future _scanFileForAndroidMediaIndex(String filePath) async { + if (!Platform.isAndroid) return; + try { + await _mediaScanChannel.invokeMethod('scanFile', {'path': filePath}); + } catch (_) { + // Non-fatal: file is already saved even if scan fails. + } +} diff --git a/lib/pages/helpers/ecard_download_service_stub.dart b/lib/pages/helpers/ecard_download_service_stub.dart new file mode 100644 index 0000000..5224c8b --- /dev/null +++ b/lib/pages/helpers/ecard_download_service_stub.dart @@ -0,0 +1,11 @@ +import 'download_result.dart'; + +Future ensureStorageAccessImpl() async => true; + +Future downloadEcardImpl({ + required String url, + required String fileName, + Map? headers, +}) async { + return DownloadResult.failure('Download is not supported on this platform'); +} diff --git a/lib/pages/helpers/ecard_download_service_web.dart b/lib/pages/helpers/ecard_download_service_web.dart new file mode 100644 index 0000000..c410fa8 --- /dev/null +++ b/lib/pages/helpers/ecard_download_service_web.dart @@ -0,0 +1,18 @@ +import 'download_result.dart'; +import 'file_download_helper.dart'; + +Future ensureStorageAccessImpl() async => true; + +Future downloadEcardImpl({ + required String url, + required String fileName, + Map? headers, +}) async { + // Keep existing web flow unchanged. + return downloadFileFromUrl( + url, + fileName: fileName, + headers: headers, + openAfterDownload: false, + ); +} diff --git a/lib/pages/helpers/file_download_common.dart b/lib/pages/helpers/file_download_common.dart new file mode 100644 index 0000000..913547c --- /dev/null +++ b/lib/pages/helpers/file_download_common.dart @@ -0,0 +1,10 @@ +String fileNameFromUrl(String url, {String defaultName = 'ecard.pdf'}) { + try { + final segment = Uri.parse(url).pathSegments.lastWhere( + (s) => s.isNotEmpty, + orElse: () => '', + ); + if (segment.contains('.')) return Uri.decodeComponent(segment); + } catch (_) {} + return defaultName; +} diff --git a/lib/pages/helpers/file_download_helper.dart b/lib/pages/helpers/file_download_helper.dart new file mode 100644 index 0000000..f60a80f --- /dev/null +++ b/lib/pages/helpers/file_download_helper.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart' show kIsWeb; + +import 'download_result.dart'; +import 'file_download_stub.dart' + if (dart.library.io) 'file_download_io.dart' + if (dart.library.html) 'file_download_web.dart' as impl; + +export 'download_result.dart'; + +/// Downloads without opening a browser tab. +/// Web: saves via browser download. Mobile: saves locally and opens the +/// system file viewer (not an in-app WebView or browser tab). +Future downloadFileFromUrl( + String url, { + String? fileName, + Map? headers, + bool openAfterDownload = true, +}) { + return impl.downloadFileFromUrlImpl( + url, + fileName: fileName, + headers: headers, + openAfterDownload: kIsWeb ? false : openAfterDownload, + ); +} diff --git a/lib/pages/helpers/file_download_io.dart b/lib/pages/helpers/file_download_io.dart new file mode 100644 index 0000000..ed760f6 --- /dev/null +++ b/lib/pages/helpers/file_download_io.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:open_filex/open_filex.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'download_result.dart'; +import 'file_download_common.dart'; + +Future downloadFileFromUrlImpl( + String url, { + String? fileName, + Map? headers, + bool openAfterDownload = false, +}) async { + try { + final response = await http.get(Uri.parse(url), headers: headers); + if (response.statusCode != 200) { + return DownloadResult.failure( + 'Download failed (${response.statusCode})', + ); + } + + final name = fileName ?? fileNameFromUrl(url); + final dir = await _resolveSaveDirectory(); + final file = File('${dir.path}/$name'); + await file.writeAsBytes(response.bodyBytes, flush: true); + + if (openAfterDownload) { + await OpenFilex.open(file.path); + } + + return DownloadResult.success( + savedPath: file.path, + message: openAfterDownload ? 'E-Card downloaded' : 'E-Card saved', + ); + } catch (e) { + return DownloadResult.failure('Could not download file'); + } +} + +Future _resolveSaveDirectory() async { + if (Platform.isAndroid) { + final downloads = await getDownloadsDirectory(); + if (downloads != null) return downloads; + } + if (Platform.isIOS) { + return getApplicationDocumentsDirectory(); + } + final downloads = await getDownloadsDirectory(); + return downloads ?? await getApplicationDocumentsDirectory(); +} diff --git a/lib/pages/helpers/file_download_stub.dart b/lib/pages/helpers/file_download_stub.dart new file mode 100644 index 0000000..4838f46 --- /dev/null +++ b/lib/pages/helpers/file_download_stub.dart @@ -0,0 +1,10 @@ +import 'download_result.dart'; + +Future downloadFileFromUrlImpl( + String url, { + String? fileName, + Map? headers, + bool openAfterDownload = false, +}) async { + return DownloadResult.failure('Download is not supported on this platform'); +} diff --git a/lib/pages/helpers/file_download_web.dart b/lib/pages/helpers/file_download_web.dart new file mode 100644 index 0000000..1cdb7ad --- /dev/null +++ b/lib/pages/helpers/file_download_web.dart @@ -0,0 +1,48 @@ +import 'dart:html' as html; + +import 'package:http/http.dart' as http; + +import 'download_result.dart'; +import 'file_download_common.dart'; + +Future downloadFileFromUrlImpl( + String url, { + String? fileName, + Map? headers, + bool openAfterDownload = false, +}) async { + try { + final name = fileName ?? fileNameFromUrl(url); + final response = await http.get(Uri.parse(url), headers: headers); + if (response.statusCode != 200) { + return DownloadResult.failure( + 'Download failed (${response.statusCode})', + ); + } + + final blob = html.Blob([response.bodyBytes]); + final objectUrl = html.Url.createObjectUrlFromBlob(blob); + final anchor = html.AnchorElement(href: objectUrl) + ..download = name + ..style.display = 'none'; + html.document.body?.append(anchor); + anchor.click(); + anchor.remove(); + html.Url.revokeObjectUrl(objectUrl); + + return DownloadResult.success(message: 'E-Card downloaded'); + } catch (_) { + try { + final name = fileName ?? fileNameFromUrl(url); + final anchor = html.AnchorElement(href: url) + ..download = name + ..style.display = 'none'; + html.document.body?.append(anchor); + anchor.click(); + anchor.remove(); + return DownloadResult.success(message: 'E-Card download started'); + } catch (e) { + return DownloadResult.failure('Could not download file'); + } + } +} diff --git a/lib/pages/postEnrollment/policies.dart b/lib/pages/postEnrollment/policies.dart index 2b49606..73e4642 100755 --- a/lib/pages/postEnrollment/policies.dart +++ b/lib/pages/postEnrollment/policies.dart @@ -11,6 +11,9 @@ import 'package:nhance_app_pwa/pages/postEnrollment/service/svg_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../customAppBar/customFooter.dart'; +import '../helpers/custom_download_snackbar.dart'; +import '../helpers/ecard_download_service.dart'; +import '../helpers/ecard_download_notification_service.dart'; import '../../customAppBar/responsive.dart'; import '../../customAppBar/tabs.dart'; import '../../customAppBar/toastHelper.dart'; @@ -33,6 +36,7 @@ class policies extends StatefulWidget { class _policiesState extends State { late ApiService apiService; + final EcardDownloadService _ecardDownloadService = const EcardDownloadService(); final session = SessionManager(); int _currentIndex = 0; bool isActive = true; @@ -52,6 +56,7 @@ class _policiesState extends State { dynamic argumentsData; bool ECardHide = true; bool _isExpanded = false; + bool _isDownloadingEcard = false; void _onTabChanged(int index) { setState(() { @@ -70,6 +75,12 @@ class _policiesState extends State { super.initState(); apiService = ApiService(context); // Initialize ApiService here _loadToken(); + EcardDownloadNotificationService.ensureInitialized(); + if (!kIsWeb) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _ecardDownloadService.ensureStorageAccess(); + }); + } } @override @@ -80,6 +91,7 @@ class _policiesState extends State { Future _loadToken() async { logDebug('_loadToken'); final String? token = await TokenService.getPostToken(); + _token = token; final SharedPreferences prefs = await SharedPreferences.getInstance(); if (token != null && token.isNotEmpty) { // Decode the JWT token received from the API response @@ -94,37 +106,105 @@ class _policiesState extends State { } Future getEcardDownload(id) async { - final clientPolicyID = widget.arguments?['client_policy_id']; - final policyNo = widget.arguments?['policy_no']; - final eCarDParams = { - // 'id': empPrimaryId, - 'id': id, - 'emp_code': empCodeString, - 'client_policy_id': clientPolicyID, - 'policy_no': policyNo - }; - final response = await apiService.getEcardRequest(eCarDParams); - logDebug('check 1'); - final ecardDownloadUrl = response['data']['eCardDownload']; - final message = response['data']['message']; - if (ecardDownloadUrl != null) { - logDebug('✅ Link: $ecardDownloadUrl'); - await _launchURL(ecardDownloadUrl,context); // Only launch if status is success - // ToastHelper.showSuccessToast(context, message); - } else { - logDebug('❌ Error: $message'); - ToastHelper.showErrorToast(context, message); - } - } + if (_isDownloadingEcard) return; + if (!mounted) return; + setState(() => _isDownloadingEcard = true); - - Future _launchURL(String url, BuildContext context) async { - logDebug('url $url'); try { - final Uri uri = Uri.parse(url); - await launchUrl(uri, mode: LaunchMode.externalApplication); + final clientPolicyID = widget.arguments?['client_policy_id']; + final policyNo = widget.arguments?['policy_no']; + final eCarDParams = { + 'id': id, + 'emp_code': empCodeString, + 'client_policy_id': clientPolicyID, + 'policy_no': policyNo, + }; + final response = await apiService.getEcardRequest(eCarDParams); + if (!mounted) return; + + final data = response['data']; + if (data is! Map) { + ToastHelper.showErrorToast(context, 'Unable to download E-Card'); + return; + } + + final ecardDownloadUrl = data['eCardDownload']; + final message = data['message']?.toString(); + if (ecardDownloadUrl == null || ecardDownloadUrl.toString().isEmpty) { + logDebug('❌ Error: $message'); + ToastHelper.showErrorToast( + context, + message ?? 'E-Card is not available', + ); + return; + } + + final url = ecardDownloadUrl.toString(); + logDebug('✅ E-Card URL: $url'); + final safePolicyNo = policyNo?.toString().replaceAll(RegExp(r'[^\w\-.]'), '_'); + final fileName = safePolicyNo == null || safePolicyNo.isEmpty + ? 'ecard.pdf' + : 'ecard_$safePolicyNo.pdf'; + + if (!kIsWeb) { + CustomDownloadSnackbar.show( + context, + message: 'Downloading eCard...', + ); + } + + final result = await _ecardDownloadService.downloadEcard( + url: url, + fileName: fileName, + headers: { + 'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y', + if (_token != null && _token.toString().isNotEmpty) + 'Authorization': 'Bearer ${_token.toString()}', + }, + ); + if (!mounted) return; + + if (result.success) { + if (kIsWeb) { + ToastHelper.showSuccessToast( + context, + result.message ?? 'E-Card downloaded', + ); + } else { + CustomDownloadSnackbar.show( + context, + message: result.message ?? 'eCard downloaded successfully', + ); + if (result.savedPath != null && result.savedPath!.isNotEmpty) { + await EcardDownloadNotificationService.showDownloadCompleted( + filePath: result.savedPath!, + fileName: fileName, + ); + } + } + } else { + if (kIsWeb) { + ToastHelper.showErrorToast( + context, + result.message ?? 'Could not download E-Card', + ); + } else { + CustomDownloadSnackbar.show( + context, + message: result.message ?? 'Download failed', + ); + } + } } catch (e) { - logDebug('Could not launch URL: $e'); + logDebug('E-Card download error: $e'); + if (mounted) { + ToastHelper.showErrorToast( + context, + 'Unable to download E-Card. Please try again.', + ); + } + } finally { + if (mounted) setState(() => _isDownloadingEcard = false); } } @@ -151,12 +231,15 @@ class _policiesState extends State { } } - void _openUrl(String url) async { - if (url.isNotEmpty && await canLaunch(url)) { - await launch(url, - webOnlyWindowName: '_blank'); // Opens in a new tab (web only) + Future _openUrl(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) { + logDebug('Could not launch URL'); + return; + } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); } else { - // Handle invalid or null URL logDebug('Could not launch URL'); } } @@ -402,7 +485,9 @@ class _policiesState extends State { ? 200 : 180, child: TextButton( - onPressed: () { + onPressed: _isDownloadingEcard + ? null + : () { getEcardDownload(empPrimaryId); // if (argumentsData['eCardDownload'] != // null) { @@ -703,7 +788,9 @@ class _policiesState extends State { child: MouseRegion( cursor: SystemMouseCursors.click, // Web pointer child: GestureDetector( - onTap: () { + onTap: _isDownloadingEcard + ? null + : () { getEcardDownload(detail['id']); }, child: Tooltip( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 073fa95..ac0f1f6 100755 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import file_picker import file_selector_macos import flutter_inappwebview_macos +import flutter_local_notifications import flutter_secure_storage_macos import google_sign_in_ios import local_auth_darwin @@ -24,6 +25,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index 830a0c3..26a7b01 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,10 @@ dependencies: ribbon_widget: ^1.0.5 flutter_html: ^3.0.0 video_player: ^2.10.1 + path_provider: ^2.1.5 + open_filex: ^4.7.0 + flutter_local_notifications: ^19.4.2 + permission_handler: ^12.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5e0f997..56d9857 100755 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1b55847..dda9a85 100755 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,11 +7,13 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_inappwebview_windows flutter_secure_storage_windows local_auth_windows + permission_handler_windows printing url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES)