ecard download for android
This commit is contained in:
parent
653316dfa1
commit
cbb2f9f33d
@ -41,6 +41,7 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@ -133,4 +134,5 @@ dependencies {
|
|||||||
implementation "com.google.firebase:firebase-messaging:24.1.1"
|
implementation "com.google.firebase:firebase-messaging:24.1.1"
|
||||||
implementation("com.google.firebase:firebase-appcheck-playintegrity")
|
implementation("com.google.firebase:firebase-appcheck-playintegrity")
|
||||||
implementation 'androidx.browser:browser:1.3.0'
|
implementation 'androidx.browser:browser:1.3.0'
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="nh.ind.nhance.benefits">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="nh.ind.nhance.benefits">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
|
|||||||
@ -1,5 +1,32 @@
|
|||||||
package nh.ind.nhance.benefits
|
package nh.ind.nhance.benefits
|
||||||
|
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
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<String>("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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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/changePin.dart';
|
||||||
import 'package:nhance_app_pwa/pages/session/settingUpPinAndBiometric.dart';
|
import 'package:nhance_app_pwa/pages/session/settingUpPinAndBiometric.dart';
|
||||||
import 'package:nhance_app_pwa/pages/setPassword.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:nhance_app_pwa/pages/verify.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -207,6 +208,7 @@ void _applyAppFlavorFromDartDefine() {
|
|||||||
Future<void> startApp() async {
|
Future<void> startApp() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
_applyAppFlavorFromDartDefine();
|
_applyAppFlavorFromDartDefine();
|
||||||
|
await AppPermissionService.requestInitialPermissions();
|
||||||
|
|
||||||
await TokenService.clearLegacyWebAuthFromSharedPreferences();
|
await TokenService.clearLegacyWebAuthFromSharedPreferences();
|
||||||
|
|
||||||
|
|||||||
16
lib/pages/helpers/app_permission_service.dart
Normal file
16
lib/pages/helpers/app_permission_service.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
class AppPermissionService {
|
||||||
|
AppPermissionService._();
|
||||||
|
|
||||||
|
static Future<void> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/pages/helpers/custom_download_snackbar.dart
Normal file
34
lib/pages/helpers/custom_download_snackbar.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/pages/helpers/download_result.dart
Normal file
23
lib/pages/helpers/download_result.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
lib/pages/helpers/ecard_download_notification_service.dart
Normal file
72
lib/pages/helpers/ecard_download_notification_service.dart
Normal file
@ -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<void> 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<void> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/pages/helpers/ecard_download_service.dart
Normal file
24
lib/pages/helpers/ecard_download_service.dart
Normal file
@ -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<bool> ensureStorageAccess() {
|
||||||
|
return impl.ensureStorageAccessImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DownloadResult> downloadEcard({
|
||||||
|
required String url,
|
||||||
|
required String fileName,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) {
|
||||||
|
return impl.downloadEcardImpl(
|
||||||
|
url: url,
|
||||||
|
fileName: fileName,
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
lib/pages/helpers/ecard_download_service_io.dart
Normal file
116
lib/pages/helpers/ecard_download_service_io.dart
Normal file
@ -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<bool> 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<DownloadResult> downloadEcardImpl({
|
||||||
|
required String url,
|
||||||
|
required String fileName,
|
||||||
|
Map<String, String>? 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<Directory> _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<void> _scanFileForAndroidMediaIndex(String filePath) async {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
try {
|
||||||
|
await _mediaScanChannel.invokeMethod<void>('scanFile', {'path': filePath});
|
||||||
|
} catch (_) {
|
||||||
|
// Non-fatal: file is already saved even if scan fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/pages/helpers/ecard_download_service_stub.dart
Normal file
11
lib/pages/helpers/ecard_download_service_stub.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'download_result.dart';
|
||||||
|
|
||||||
|
Future<bool> ensureStorageAccessImpl() async => true;
|
||||||
|
|
||||||
|
Future<DownloadResult> downloadEcardImpl({
|
||||||
|
required String url,
|
||||||
|
required String fileName,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) async {
|
||||||
|
return DownloadResult.failure('Download is not supported on this platform');
|
||||||
|
}
|
||||||
18
lib/pages/helpers/ecard_download_service_web.dart
Normal file
18
lib/pages/helpers/ecard_download_service_web.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'download_result.dart';
|
||||||
|
import 'file_download_helper.dart';
|
||||||
|
|
||||||
|
Future<bool> ensureStorageAccessImpl() async => true;
|
||||||
|
|
||||||
|
Future<DownloadResult> downloadEcardImpl({
|
||||||
|
required String url,
|
||||||
|
required String fileName,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) async {
|
||||||
|
// Keep existing web flow unchanged.
|
||||||
|
return downloadFileFromUrl(
|
||||||
|
url,
|
||||||
|
fileName: fileName,
|
||||||
|
headers: headers,
|
||||||
|
openAfterDownload: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
10
lib/pages/helpers/file_download_common.dart
Normal file
10
lib/pages/helpers/file_download_common.dart
Normal file
@ -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;
|
||||||
|
}
|
||||||
25
lib/pages/helpers/file_download_helper.dart
Normal file
25
lib/pages/helpers/file_download_helper.dart
Normal file
@ -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<DownloadResult> downloadFileFromUrl(
|
||||||
|
String url, {
|
||||||
|
String? fileName,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
bool openAfterDownload = true,
|
||||||
|
}) {
|
||||||
|
return impl.downloadFileFromUrlImpl(
|
||||||
|
url,
|
||||||
|
fileName: fileName,
|
||||||
|
headers: headers,
|
||||||
|
openAfterDownload: kIsWeb ? false : openAfterDownload,
|
||||||
|
);
|
||||||
|
}
|
||||||
52
lib/pages/helpers/file_download_io.dart
Normal file
52
lib/pages/helpers/file_download_io.dart
Normal file
@ -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<DownloadResult> downloadFileFromUrlImpl(
|
||||||
|
String url, {
|
||||||
|
String? fileName,
|
||||||
|
Map<String, String>? 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<Directory> _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();
|
||||||
|
}
|
||||||
10
lib/pages/helpers/file_download_stub.dart
Normal file
10
lib/pages/helpers/file_download_stub.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'download_result.dart';
|
||||||
|
|
||||||
|
Future<DownloadResult> downloadFileFromUrlImpl(
|
||||||
|
String url, {
|
||||||
|
String? fileName,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
bool openAfterDownload = false,
|
||||||
|
}) async {
|
||||||
|
return DownloadResult.failure('Download is not supported on this platform');
|
||||||
|
}
|
||||||
48
lib/pages/helpers/file_download_web.dart
Normal file
48
lib/pages/helpers/file_download_web.dart
Normal file
@ -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<DownloadResult> downloadFileFromUrlImpl(
|
||||||
|
String url, {
|
||||||
|
String? fileName,
|
||||||
|
Map<String, String>? 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,9 @@ import 'package:nhance_app_pwa/pages/postEnrollment/service/svg_service.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../customAppBar/customFooter.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/responsive.dart';
|
||||||
import '../../customAppBar/tabs.dart';
|
import '../../customAppBar/tabs.dart';
|
||||||
import '../../customAppBar/toastHelper.dart';
|
import '../../customAppBar/toastHelper.dart';
|
||||||
@ -33,6 +36,7 @@ class policies extends StatefulWidget {
|
|||||||
|
|
||||||
class _policiesState extends State<policies> {
|
class _policiesState extends State<policies> {
|
||||||
late ApiService apiService;
|
late ApiService apiService;
|
||||||
|
final EcardDownloadService _ecardDownloadService = const EcardDownloadService();
|
||||||
final session = SessionManager();
|
final session = SessionManager();
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
bool isActive = true;
|
bool isActive = true;
|
||||||
@ -52,6 +56,7 @@ class _policiesState extends State<policies> {
|
|||||||
dynamic argumentsData;
|
dynamic argumentsData;
|
||||||
bool ECardHide = true;
|
bool ECardHide = true;
|
||||||
bool _isExpanded = false;
|
bool _isExpanded = false;
|
||||||
|
bool _isDownloadingEcard = false;
|
||||||
|
|
||||||
void _onTabChanged(int index) {
|
void _onTabChanged(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -70,6 +75,12 @@ class _policiesState extends State<policies> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
apiService = ApiService(context); // Initialize ApiService here
|
apiService = ApiService(context); // Initialize ApiService here
|
||||||
_loadToken();
|
_loadToken();
|
||||||
|
EcardDownloadNotificationService.ensureInitialized();
|
||||||
|
if (!kIsWeb) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await _ecardDownloadService.ensureStorageAccess();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -80,6 +91,7 @@ class _policiesState extends State<policies> {
|
|||||||
Future<void> _loadToken() async {
|
Future<void> _loadToken() async {
|
||||||
logDebug('_loadToken');
|
logDebug('_loadToken');
|
||||||
final String? token = await TokenService.getPostToken();
|
final String? token = await TokenService.getPostToken();
|
||||||
|
_token = token;
|
||||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
// Decode the JWT token received from the API response
|
// Decode the JWT token received from the API response
|
||||||
@ -94,37 +106,105 @@ class _policiesState extends State<policies> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getEcardDownload(id) async {
|
Future<void> getEcardDownload(id) async {
|
||||||
final clientPolicyID = widget.arguments?['client_policy_id'];
|
if (_isDownloadingEcard) return;
|
||||||
final policyNo = widget.arguments?['policy_no'];
|
if (!mounted) return;
|
||||||
final eCarDParams = {
|
setState(() => _isDownloadingEcard = true);
|
||||||
// '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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _launchURL(String url, BuildContext context) async {
|
|
||||||
logDebug('url $url');
|
|
||||||
try {
|
try {
|
||||||
final Uri uri = Uri.parse(url);
|
final clientPolicyID = widget.arguments?['client_policy_id'];
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
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) {
|
} 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<policies> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openUrl(String url) async {
|
Future<void> _openUrl(String url) async {
|
||||||
if (url.isNotEmpty && await canLaunch(url)) {
|
final uri = Uri.tryParse(url);
|
||||||
await launch(url,
|
if (uri == null) {
|
||||||
webOnlyWindowName: '_blank'); // Opens in a new tab (web only)
|
logDebug('Could not launch URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
} else {
|
} else {
|
||||||
// Handle invalid or null URL
|
|
||||||
logDebug('Could not launch URL');
|
logDebug('Could not launch URL');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,7 +485,9 @@ class _policiesState extends State<policies> {
|
|||||||
? 200
|
? 200
|
||||||
: 180,
|
: 180,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: _isDownloadingEcard
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
getEcardDownload(empPrimaryId);
|
getEcardDownload(empPrimaryId);
|
||||||
// if (argumentsData['eCardDownload'] !=
|
// if (argumentsData['eCardDownload'] !=
|
||||||
// null) {
|
// null) {
|
||||||
@ -703,7 +788,9 @@ class _policiesState extends State<policies> {
|
|||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click, // Web pointer
|
cursor: SystemMouseCursors.click, // Web pointer
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: _isDownloadingEcard
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
getEcardDownload(detail['id']);
|
getEcardDownload(detail['id']);
|
||||||
},
|
},
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Foundation
|
|||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
|
import flutter_local_notifications
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
import local_auth_darwin
|
import local_auth_darwin
|
||||||
@ -24,6 +25,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||||
|
|||||||
@ -75,6 +75,10 @@ dependencies:
|
|||||||
ribbon_widget: ^1.0.5
|
ribbon_widget: ^1.0.5
|
||||||
flutter_html: ^3.0.0
|
flutter_html: ^3.0.0
|
||||||
video_player: ^2.10.1
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <local_auth_windows/local_auth_plugin.h>
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
LocalAuthPluginRegisterWithRegistrar(
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|||||||
@ -7,11 +7,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
flutter_inappwebview_windows
|
flutter_inappwebview_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
local_auth_windows
|
local_auth_windows
|
||||||
|
permission_handler_windows
|
||||||
printing
|
printing
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
flutter_local_notifications_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user