From 7da04ab49d698d1da5a87a5e9a35ba3c7d6ab9f2 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:58:43 +0630 Subject: [PATCH] update --- .../example/cb_prestige_qr/MainActivity.kt | 30 +- ios/Runner/AppDelegate.swift | 44 ++- lib/config.dart | 5 + lib/core/services/device_id_service.dart | 16 + .../data_sources/scan_remote_data_source.dart | 4 +- .../scan_remote_data_source_fake.dart | 5 +- .../scan_remote_data_source_http.dart | 98 ++++++ .../repositories/scan_repository_impl.dart | 16 +- .../domain/entities/scan_submit_payload.dart | 53 ++++ .../domain/repositories/scan_repository.dart | 2 +- .../presentation/manager/scan_controller.dart | 39 ++- .../scan/presentation/pages/scan_page.dart | 28 +- .../presentation/pages/scan_result_page.dart | 286 ++++++++++++++++++ lib/features/scan/scan_providers.dart | 6 +- pubspec.lock | 66 +--- pubspec.yaml | 1 + 16 files changed, 609 insertions(+), 90 deletions(-) create mode 100644 lib/config.dart create mode 100644 lib/core/services/device_id_service.dart create mode 100644 lib/features/scan/data/data_sources/scan_remote_data_source_http.dart create mode 100644 lib/features/scan/domain/entities/scan_submit_payload.dart create mode 100644 lib/features/scan/presentation/pages/scan_result_page.dart diff --git a/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt b/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt index a5d0849..83128fe 100644 --- a/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt @@ -1,5 +1,33 @@ package com.example.cb_prestige_qr +import android.provider.Settings import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + "cb_prestige_qr/device" + ).setMethodCallHandler { call, result -> + when (call.method) { + "getDeviceId" -> { + val deviceId = Settings.Secure.getString( + contentResolver, + Settings.Secure.ANDROID_ID + ) + + if (deviceId.isNullOrBlank()) { + result.error("device_id_unavailable", "Unable to retrieve device ID.", null) + } else { + result.success(deviceId) + } + } + else -> result.notImplemented() + } + } + } +} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index ed1c097..a87d441 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,16 +1,42 @@ import Flutter import UIKit -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + private let deviceChannelName = "cb_prestige_qr/device" + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + + let channel = FlutterMethodChannel( + name: deviceChannelName, + binaryMessenger: engineBridge.binaryMessenger + ) + channel.setMethodCallHandler { call, result in + switch call.method { + case "getDeviceId": + if let identifier = UIDevice.current.identifierForVendor?.uuidString, + !identifier.isEmpty { + result(identifier) + } else { + result( + FlutterError( + code: "device_id_unavailable", + message: "Unable to retrieve device ID.", + details: nil + ) + ) + } + default: + result(FlutterMethodNotImplemented) + } + } + } +} diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..4ad1c42 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,5 @@ +class AppConfig { + const AppConfig._(); + + static const apiBaseUrl = 'http://192.168.100.41:3000'; +} diff --git a/lib/core/services/device_id_service.dart b/lib/core/services/device_id_service.dart new file mode 100644 index 0000000..0fc4711 --- /dev/null +++ b/lib/core/services/device_id_service.dart @@ -0,0 +1,16 @@ +import 'package:flutter/services.dart'; + +class DeviceIdService { + static const _channel = MethodChannel('cb_prestige_qr/device'); + + Future getDeviceId() async { + // final deviceId = await _channel.invokeMethod('getDeviceId'); + // if (deviceId == null || deviceId.trim().isEmpty) { + // throw PlatformException( + // code: 'device_id_unavailable', + // message: 'Unable to retrieve device ID.', + // ); + // } + return "demo_device_id"; + } +} diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source.dart b/lib/features/scan/data/data_sources/scan_remote_data_source.dart index 3ffed98..9fd1039 100644 --- a/lib/features/scan/data/data_sources/scan_remote_data_source.dart +++ b/lib/features/scan/data/data_sources/scan_remote_data_source.dart @@ -1,6 +1,8 @@ +import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; + abstract interface class ScanRemoteDataSource { Future> submitScan({ - required String rawValue, + required ScanSubmitPayload payload, }); } diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart index b3ac074..77d9075 100644 --- a/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart +++ b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart @@ -1,12 +1,13 @@ import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; class FakeScanRemoteDataSource implements ScanRemoteDataSource { @override Future> submitScan({ - required String rawValue, + required ScanSubmitPayload payload, }) async { return { - 'rawValue': rawValue, + 'rawValue': payload.token, 'scannedAt': DateTime.now().toIso8601String(), }; } diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart b/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart new file mode 100644 index 0000000..b7fd3b8 --- /dev/null +++ b/lib/features/scan/data/data_sources/scan_remote_data_source_http.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cb_prestige_qr/config.dart'; +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; + +class HttpScanRemoteDataSource implements ScanRemoteDataSource { + HttpScanRemoteDataSource({HttpClient? httpClient}) + : _httpClient = httpClient ?? HttpClient(); + + final HttpClient _httpClient; + + static const _endpoint = String.fromEnvironment( + 'QR_SCAN_PATH', + defaultValue: '/api/qr/scan', + ); + + @override + Future> submitScan({ + required ScanSubmitPayload payload, + }) async { + final uri = _resolveUri(); + final request = await _httpClient.postUrl(uri); + request.headers.contentType = ContentType.json; + request.write(jsonEncode(payload.toJson())); + + final response = await request.close(); + final responseBody = await response.transform(utf8.decoder).join(); + + if (response.statusCode != HttpStatus.ok) { + throw HttpException( + _buildStatusMessage(response.statusCode, responseBody), + uri: uri, + ); + } + + if (responseBody.trim().isEmpty) { + return { + 'rawValue': payload.token, + 'scannedAt': DateTime.now().toIso8601String(), + }; + } + + final decoded = jsonDecode(responseBody); + if (decoded is! Map) { + throw const FormatException('QR scan response must be a JSON object.'); + } + + final data = decoded.map( + (key, value) => MapEntry(key.toString(), value), + ); + final isOk = data['OK'] == true || data['ok'] == true; + if (!isOk) { + throw HttpException( + 'Submit failed. Response ok flag is false.', + uri: uri, + ); + } + + return data; + } + + Uri _resolveUri() { + final endpointUri = Uri.tryParse(_endpoint); + if (endpointUri != null && endpointUri.hasScheme) { + return endpointUri; + } + + if (AppConfig.apiBaseUrl.trim().isEmpty) { + throw const FormatException( + 'API_BASE_URL is not configured. Pass --dart-define=API_BASE_URL=https://your-host.', + ); + } + + return Uri.parse(AppConfig.apiBaseUrl).resolve(_endpoint); + } + + String _buildStatusMessage(int statusCode, String responseBody) { + final normalizedBody = responseBody.trim(); + final bodySuffix = normalizedBody.isEmpty ? '' : ' Response: $normalizedBody'; + + return switch (statusCode) { + HttpStatus.badRequest => + 'Submit failed with status 400 (Bad Request).$bodySuffix', + HttpStatus.unauthorized => + 'Submit failed with status 401 (Unauthorized).$bodySuffix', + HttpStatus.forbidden => + 'Submit failed with status 403 (Forbidden).$bodySuffix', + HttpStatus.notFound => + 'Submit failed with status 404 (Not Found).$bodySuffix', + HttpStatus.internalServerError => + 'Submit failed with status 500 (Server Error).$bodySuffix', + _ => + 'Submit failed. Expected status 200 but got $statusCode.$bodySuffix', + }; + } +} diff --git a/lib/features/scan/data/repositories/scan_repository_impl.dart b/lib/features/scan/data/repositories/scan_repository_impl.dart index 5a41875..c4e1197 100644 --- a/lib/features/scan/data/repositories/scan_repository_impl.dart +++ b/lib/features/scan/data/repositories/scan_repository_impl.dart @@ -1,6 +1,7 @@ import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; import 'package:cb_prestige_qr/features/scan/data/models/scanned_qr_model.dart'; import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; +import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart'; import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; class ScanRepositoryImpl implements ScanRepository { @@ -10,8 +11,17 @@ class ScanRepositoryImpl implements ScanRepository { @override Future processRawValue(String rawValue) async { - final payload = await _remoteDataSource.submitScan(rawValue: rawValue); - return ScannedQrModel.fromJson(payload).toEntity(); + return ScannedQr(rawValue: rawValue, scannedAt: DateTime.now()); + } + + @override + Future submitScan(String rawValue) async { + final request = ScanSubmitPayload.fromRawValue(rawValue: rawValue); + await _remoteDataSource.submitScan(payload: request); + final normalizedResponse = { + 'rawValue': request.token, + 'scannedAt': DateTime.now().toIso8601String(), + }; + return ScannedQrModel.fromJson(normalizedResponse).toEntity(); } } - diff --git a/lib/features/scan/domain/entities/scan_submit_payload.dart b/lib/features/scan/domain/entities/scan_submit_payload.dart new file mode 100644 index 0000000..d771de6 --- /dev/null +++ b/lib/features/scan/domain/entities/scan_submit_payload.dart @@ -0,0 +1,53 @@ +class ScanSubmitPayload { + const ScanSubmitPayload({ + required this.token, + required this.merchantId, + required this.merchantName, + required this.merchantBranchId, + required this.scannedByDeviceId, + }); + + factory ScanSubmitPayload.fromRawValue({ + required String rawValue, + }) { + final payload = ScanSubmitPayload( + token: rawValue.trim(), + merchantId: 'merchant-001', + merchantName: 'cashier_001', + merchantBranchId: 'branch-001', + scannedByDeviceId: 'device-001', + ); + payload._validate(); + return payload; + } + + final String token; + final String merchantId; + final String merchantName; + final String merchantBranchId; + final String scannedByDeviceId; + + Map toJson() { + return { + 'token': token, + 'merchantId': merchantId, + 'merchantName': merchantName, + 'merchantBranchId': merchantBranchId, + 'scannedByDeviceId': scannedByDeviceId, + }; + } + + void _validate() { + final missing = [ + if (token.trim().isEmpty) 'token', + if (merchantId.trim().isEmpty) 'merchantId', + if (merchantName.trim().isEmpty) 'merchantName', + if (merchantBranchId.trim().isEmpty) 'merchantBranchId', + if (scannedByDeviceId.trim().isEmpty) 'scannedByDeviceId', + ]; + if (missing.isEmpty) return; + throw FormatException( + 'Scanned QR is missing required fields: ${missing.join(', ')}.', + ); + } +} diff --git a/lib/features/scan/domain/repositories/scan_repository.dart b/lib/features/scan/domain/repositories/scan_repository.dart index bc4d8e0..b892393 100644 --- a/lib/features/scan/domain/repositories/scan_repository.dart +++ b/lib/features/scan/domain/repositories/scan_repository.dart @@ -2,5 +2,5 @@ import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; abstract interface class ScanRepository { Future processRawValue(String rawValue); + Future submitScan(String rawValue); } - diff --git a/lib/features/scan/presentation/manager/scan_controller.dart b/lib/features/scan/presentation/manager/scan_controller.dart index aaae2ea..14c05ea 100644 --- a/lib/features/scan/presentation/manager/scan_controller.dart +++ b/lib/features/scan/presentation/manager/scan_controller.dart @@ -7,12 +7,14 @@ class ScanState { const ScanState({ this.isScanning = true, this.isProcessing = false, + this.isSubmitting = false, this.scannedValue, this.errorMessage, }); final bool isScanning; final bool isProcessing; + final bool isSubmitting; final String? scannedValue; final String? errorMessage; @@ -21,16 +23,20 @@ class ScanState { ScanState copyWith({ bool? isScanning, bool? isProcessing, + bool? isSubmitting, Object? scannedValue = _sentinel, Object? errorMessage = _sentinel, }) { return ScanState( isScanning: isScanning ?? this.isScanning, isProcessing: isProcessing ?? this.isProcessing, - scannedValue: - identical(scannedValue, _sentinel) ? this.scannedValue : scannedValue as String?, - errorMessage: - identical(errorMessage, _sentinel) ? this.errorMessage : errorMessage as String?, + isSubmitting: isSubmitting ?? this.isSubmitting, + scannedValue: identical(scannedValue, _sentinel) + ? this.scannedValue + : scannedValue as String?, + errorMessage: identical(errorMessage, _sentinel) + ? this.errorMessage + : errorMessage as String?, ); } } @@ -64,7 +70,30 @@ class ScanController extends _$ScanController { } } + Future submitScannedValue(String value) async { + if (state.isSubmitting) return; + state = state.copyWith(isSubmitting: true, errorMessage: null); + + try { + await ref.read(scanRepositoryProvider).submitScan(value); + state = state.copyWith( + isSubmitting: false, + scannedValue: null, + errorMessage: null, + ); + } catch (e) { + state = state.copyWith(isSubmitting: false, errorMessage: e.toString()); + rethrow; + } + } + void resumeScanning() { - state = const ScanState(isScanning: true, isProcessing: false, scannedValue: null, errorMessage: null); + state = const ScanState( + isScanning: true, + isProcessing: false, + isSubmitting: false, + scannedValue: null, + errorMessage: null, + ); } } diff --git a/lib/features/scan/presentation/pages/scan_page.dart b/lib/features/scan/presentation/pages/scan_page.dart index 88a7463..8308b61 100644 --- a/lib/features/scan/presentation/pages/scan_page.dart +++ b/lib/features/scan/presentation/pages/scan_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cb_prestige_qr/core/presentation/widgets/start_loading_overlay.dart'; import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_result_page.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -15,6 +16,7 @@ class ScanPage extends ConsumerStatefulWidget { class _ScanPageState extends ConsumerState { late final MobileScannerController _scannerController; + String? _openedResultValue; @override void initState() { @@ -47,6 +49,22 @@ class _ScanPageState extends ConsumerState { } }); + ref.listen(scanControllerProvider.select((s) => s.scannedValue), ( + previous, + next, + ) { + if (next == null || next == _openedResultValue) return; + _openedResultValue = next; + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => ScanResultPage(rawValue: next)), + ); + _openedResultValue = null; + }); + }); + final scanState = ref.watch(scanControllerProvider); void onDetect(BarcodeCapture capture) { @@ -85,9 +103,7 @@ class _ScanPageState extends ConsumerState { child: Center(child: CircularProgressIndicator()), ), ), - if (!scanState.isProcessing && - (scanState.scannedValue != null || - scanState.errorMessage != null)) + if (!scanState.isProcessing && scanState.errorMessage != null) Positioned.fill( child: ColoredBox( color: const Color(0x66000000), @@ -115,11 +131,7 @@ class _ScanPageState extends ConsumerState { ).textTheme.titleLarge, ), const SizedBox(height: 12), - SelectableText( - scanState.errorMessage ?? - scanState.scannedValue ?? - '', - ), + SelectableText(scanState.errorMessage ?? ''), const SizedBox(height: 16), Row( children: [ diff --git a/lib/features/scan/presentation/pages/scan_result_page.dart b/lib/features/scan/presentation/pages/scan_result_page.dart new file mode 100644 index 0000000..e5e35bc --- /dev/null +++ b/lib/features/scan/presentation/pages/scan_result_page.dart @@ -0,0 +1,286 @@ +import 'dart:convert'; + +import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; + +class ScanResultPage extends ConsumerWidget { + const ScanResultPage({super.key, required this.rawValue}); + + final String rawValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final scanState = ref.watch(scanControllerProvider); + final isSubmitting = scanState.isSubmitting; + final previewValue = _buildPreviewValue(rawValue); + + return PopScope( + canPop: !isSubmitting, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) return; + ref.read(scanControllerProvider.notifier).resumeScanning(); + }, + child: Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('QR Result'), + backgroundColor: theme.scaffoldBackgroundColor, + surfaceTintColor: theme.scaffoldBackgroundColor, + elevation: 0, + scrolledUnderElevation: 0, + ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(14), + ), + alignment: Alignment.center, + child: Icon( + Icons.qr_code_2_rounded, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Scanned QR', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + 'Review before submitting', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + SelectableText( + previewValue, + style: theme.textTheme.bodyMedium?.copyWith( + height: 1.4, + ), + ), + ], + ), + ), + ), + if (scanState.errorMessage != null) ...[ + const SizedBox(height: 12), + Text( + scanState.errorMessage!, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.error, + fontWeight: FontWeight.w600, + ), + ), + ], + const SizedBox(height: 18), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: isSubmitting + ? null + : () { + ref + .read(scanControllerProvider.notifier) + .resumeScanning(); + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: isSubmitting + ? null + : () async { + try { + final navigator = Navigator.of(context); + await ref + .read(scanControllerProvider.notifier) + .submitScannedValue(rawValue); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('QR submitted successfully.'), + ), + ); + ref + .read(navIndexNotifierProvider.notifier) + .setIndex(0); + ref + .read(scanControllerProvider.notifier) + .resumeScanning(); + navigator.pop(); + navigator.pop(); + } catch (_) { + if (!context.mounted) return; + final errorMessage = + ref + .read(scanControllerProvider) + .errorMessage ?? + 'Unable to submit QR.'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: colorScheme.errorContainer, + content: Text( + errorMessage, + style: TextStyle( + color: colorScheme.onErrorContainer, + ), + ), + ), + ); + } + }, + child: isSubmitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2.4, + ), + ) + : const Text('Submit'), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +String _buildPreviewValue(String rawValue) { + final decodedJson = + _tryDecodeJsonObject(rawValue) ?? + _tryDecodeJwtWithPackage(rawValue) ?? + _tryExtractAndDecodeJwt(rawValue) ?? + _tryDecodeFromUri(rawValue) ?? + _tryDecodeBase64Json(rawValue, urlSafe: true) ?? + _tryDecodeBase64Json(rawValue, urlSafe: false); + if (decodedJson != null) { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(decodedJson); + } + + return _tryDecodeBase64Text(rawValue, urlSafe: true) ?? + _tryDecodeBase64Text(rawValue, urlSafe: false) ?? + rawValue; +} + +Map? _tryDecodeJsonObject(String value) { + try { + final decoded = jsonDecode(value); + if (decoded is! Map) return null; + return decoded.map((key, value) => MapEntry(key.toString(), value)); + } catch (_) { + return null; + } +} + +Map? _tryDecodeJwtWithPackage(String value) { + try { + return JwtDecoder.tryDecode(value.trim()); + } catch (_) { + return null; + } +} + +Map? _tryExtractAndDecodeJwt(String value) { + final match = RegExp( + r'([A-Za-z0-9\-_]+=*\.[A-Za-z0-9\-_]+=*\.[A-Za-z0-9\-_]+=*)', + ).firstMatch(value); + if (match == null) return null; + return _tryDecodeJwtWithPackage(match.group(1)!); +} + +Map? _tryDecodeFromUri(String value) { + final uri = Uri.tryParse(value.trim()); + if (uri == null) return null; + + for (final entry in uri.queryParameters.entries) { + final decoded = + _tryDecodeJwtWithPackage(entry.value) ?? + _tryExtractAndDecodeJwt(entry.value) ?? + _tryDecodeJsonObject(entry.value) ?? + _tryDecodeBase64Json(entry.value, urlSafe: true) ?? + _tryDecodeBase64Json(entry.value, urlSafe: false); + if (decoded != null) return decoded; + } + + return null; +} + +Map? _tryDecodeBase64Json(String value, {required bool urlSafe}) { + try { + final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); + final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); + final decoded = utf8.decode(bytes); + final payload = jsonDecode(decoded); + if (payload is! Map) return null; + return payload.map((key, value) => MapEntry(key.toString(), value)); + } catch (_) { + return null; + } +} + +String? _tryDecodeBase64Text(String value, {required bool urlSafe}) { + try { + final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value); + final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized); + final decoded = utf8.decode(bytes).trim(); + if (decoded.isEmpty || decoded == value) return null; + return decoded; + } catch (_) { + return null; + } +} diff --git a/lib/features/scan/scan_providers.dart b/lib/features/scan/scan_providers.dart index dd05067..c43b7e2 100644 --- a/lib/features/scan/scan_providers.dart +++ b/lib/features/scan/scan_providers.dart @@ -1,15 +1,15 @@ import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; -import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source_fake.dart'; +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source_http.dart'; import 'package:cb_prestige_qr/features/scan/data/repositories/scan_repository_impl.dart'; import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; import 'package:cb_prestige_qr/features/scan/domain/use_cases/process_scan_use_case.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'scan_providers.g.dart'; +part 'scan_providers.g.dart'; @riverpod ScanRemoteDataSource scanRemoteDataSource(Ref ref) { - return FakeScanRemoteDataSource(); + return HttpScanRemoteDataSource(); } @riverpod diff --git a/pubspec.lock b/pubspec.lock index 03ac6bf..7109eab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -278,14 +278,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.7" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" - url: "https://pub.dev" - source: hosted - version: "2.0.34" flutter_riverpod: dependency: transitive description: @@ -392,14 +384,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" io: dependency: transitive description: @@ -416,6 +400,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.11.0" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: @@ -448,46 +440,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - local_auth: - dependency: "direct main" - description: - name: local_auth - sha256: ae6f382f638108c6becd134318d7c3f0a93875383a54010f61d7c97ac05d5137 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - local_auth_android: - dependency: transitive - description: - name: local_auth_android - sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c - url: "https://pub.dev" - source: hosted - version: "2.0.7" - local_auth_darwin: - dependency: transitive - description: - name: local_auth_darwin - sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 - url: "https://pub.dev" - source: hosted - version: "2.0.3" - local_auth_platform_interface: - dependency: transitive - description: - name: local_auth_platform_interface - sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122 - url: "https://pub.dev" - source: hosted - version: "1.1.0" - local_auth_windows: - dependency: transitive - description: - name: local_auth_windows - sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 - url: "https://pub.dev" - source: hosted - version: "2.0.1" logging: dependency: transitive description: @@ -1007,4 +959,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.11.3 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0d48d33..5806c05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: carousel_slider: ^5.1.2 mobile_scanner: ^7.2.0 shared_preferences: ^2.5.5 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: