From 0255a8d1dd7baa4b019b62d3a8477bf35df968a1 Mon Sep 17 00:00:00 2001 From: Gian Marco Di Francesco <44344581+giandifra@users.noreply.github.com> Date: Mon, 13 May 2024 10:37:11 +0200 Subject: [PATCH] Release into main (#10) * add nfc reader * improve scanner * add nfc listener * Text update * improve ui * add ios compatibility * update gitignore * update version * add missing dependency * remove unnecessary files from git * add missing dependency * center scan widgets * add missing file * fix version --------- Co-authored-by: Lorenz Cuno Klopfenstein --- .fvm/flutter_sdk | 1 - .fvm/fvm_config.json | 4 - .gitignore | 2 + android/app/src/main/AndroidManifest.xml | 10 +- assets/images/piggy-bank.png | Bin 15933 -> 11590 bytes assets/lang/en.json | 14 +- assets/lang/it.json | 14 +- ios/Runner.xcodeproj/project.pbxproj | 24 +- lib/app.dart | 87 +- lib/main_dev.dart | 2 +- lib/src/application/app_notifier.dart | 94 +- lib/src/application/app_notifier.g.dart | 56 ++ lib/src/new_home/ui/new_home.dart | 60 +- lib/src/new_home/ui/nfc_widget.dart | 44 + lib/src/nfc/application/nfc_notifier.dart | 172 ++++ .../nfc/application/nfc_notifier.freezed.dart | 938 ++++++++++++++++++ lib/src/nfc/application/nfc_notifier.g.dart | 25 + lib/src/nfc/ui/nfc_session_dialog.dart | 58 ++ lib/src/nfc/utils.dart | 18 + .../scanner/application/scanner_state.dart | 78 ++ .../application/scanner_state.freezed.dart | 726 ++++++++++++++ .../scanner/application/scanner_state.g.dart | 26 + lib/src/scanner/ui/scan_screen.dart | 286 ++++++ lib/src/screens/home/home_screen.dart | 16 +- lib/src/screens/home/scan_screen.dart | 126 --- .../screens/home/widgets/totem_dialog.dart | 178 ++-- lib/src/screens/intro/intro.dart | 79 +- lib/src/screens/settings/settings.dart | 9 +- pubspec.yaml | 5 +- 29 files changed, 2802 insertions(+), 350 deletions(-) delete mode 120000 .fvm/flutter_sdk delete mode 100644 .fvm/fvm_config.json create mode 100644 lib/src/application/app_notifier.g.dart create mode 100644 lib/src/new_home/ui/nfc_widget.dart create mode 100644 lib/src/nfc/application/nfc_notifier.dart create mode 100644 lib/src/nfc/application/nfc_notifier.freezed.dart create mode 100644 lib/src/nfc/application/nfc_notifier.g.dart create mode 100644 lib/src/nfc/ui/nfc_session_dialog.dart create mode 100644 lib/src/nfc/utils.dart create mode 100644 lib/src/scanner/application/scanner_state.dart create mode 100644 lib/src/scanner/application/scanner_state.freezed.dart create mode 100644 lib/src/scanner/application/scanner_state.g.dart create mode 100644 lib/src/scanner/ui/scan_screen.dart delete mode 100644 lib/src/screens/home/scan_screen.dart diff --git a/.fvm/flutter_sdk b/.fvm/flutter_sdk deleted file mode 120000 index 392a1aa..0000000 --- a/.fvm/flutter_sdk +++ /dev/null @@ -1 +0,0 @@ -/Users/gianmarcodifrancesco/fvm/versions/3.16.7 \ No newline at end of file diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index c8ce885..0000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.16.7", - "flavors": {} -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0dc076e..9681e55 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,5 @@ build/ **/android/key.properties **/assets/map_point.json **/pocket-keystore.jks + +.fvm/ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9a337a8..82e8c63 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ to allow setting breakpoints, to provide hot reload, etc. --> + @@ -31,7 +32,7 @@ + + + + + + + diff --git a/assets/images/piggy-bank.png b/assets/images/piggy-bank.png index b851948fec67a0401f3ff2193387bcd64b4332d6..29a1b44ed11a1557ed6c56368a5ff9a1701eeba2 100644 GIT binary patch literal 11590 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 diff --git a/assets/lang/en.json b/assets/lang/en.json index 8cecd20..4f1bc9e 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -80,6 +80,7 @@ "introDesc5": "You will be able to earn discounts on goods and service in exchange for your vouchers.", "introDesc6": "Vouchers are stores on this device. If you need to uninstall the app or change device, remember to export your vouchers or you will lose them.", "introDesc7": "For completing the tutorial, you got your first WOM", + "introAction7": "Riscatta subito", "introDoneText": "OK", "introSkipText": "Skip", "introTitle1": "Welcome to", @@ -230,6 +231,15 @@ "enableHomeTutorialDesc": "Go through the introduction tutorial on the app homepage.", "languageSettingsTitle": "Application language", "languageSettingsDesc": "Choose the language that will be used by the app’s interface.", - "exportYourWomTitle": "Export your WOM", - "exportYourWomDesc": "Create a backup of your wom and import it to another device" + "exportYourWomTitle": "Export your vouchers", + "exportYourWomDesc": "Create a backup of your WOM vouchers and import them to another device", + "scanStateEmpty": "None of the scanned QR Codes is recognized. The scanner is processing…", + "scanStateSingle": "One WOM QR Code found.", + "scanStateMultiple": "Multiple WOM QR Codes found. Focus on one.", + "scanContinueToScan": "Continue scan", + "scanning": "Scanning…", + "scanGetWom": "Redeem", + "scanPay": "Pay", + "scanImportMigration": "Import", + "scanExchangeImport": "Receive" } diff --git a/assets/lang/it.json b/assets/lang/it.json index 718956a..feaee16 100644 --- a/assets/lang/it.json +++ b/assets/lang/it.json @@ -80,6 +80,7 @@ "introDesc5": "Ottieni sconti su beni e servizi in cambio dei WOM raccolti", "introDesc6": "I WOM sono conservati solo su questa applicazione, se devi disinstallarla o cambiare dispositivo ricordati di esportarli per non perderli", "introDesc7": "Per aver completato il tutorial hai ottenuto i tuoi primi WOM", + "introAction7": "Riscatta subito", "introDoneText": "OK", "introSkipText": "Salta", "introTitle1": "Ti diamo il benvenuto in", @@ -137,7 +138,7 @@ "settings_pay_demo_desc": "Prova a pagare con i WOM", "settings_info_title": "Info", "settings_info_desc": "Visita il nostro sito", - "settings_show_intro_title": "Mostra il tutorial iniziale?", + "settings_show_intro_title": "Mostra il tutorial iniziale", "settings_show_intro_desc": "Scopri cosa sono i WOM", "no_sources": "Nessuna sorgente", "missing_location_error": "Non riesco a rilevare la tua posizione", @@ -231,5 +232,14 @@ "languageSettingsTitle": "Lingua dell’app", "languageSettingsDesc": "Seleziona la lingua dell’applicazione.", "exportYourWomTitle": "Esporta i tuoi WOM", - "exportYourWomDesc": "Crea un backup dei tuoi wom e importalo in un altro dispositivo" + "exportYourWomDesc": "Crea un backup dei tuoi wom e importalo in un altro dispositivo", + "scanStateEmpty": "Nessuno dei QR Code scansionati è supportato. Scansione in corso…", + "scanStateSingle": "Un QR Code WOM individuato.", + "scanStateMultiple": "Più QR Code WOM individuati. Fai zoom su uno di essi.", + "scanContinueToScan": "Continua scansione", + "scanning": "Scansione in corso…", + "scanGetWom": "Riscatta", + "scanPay": "Paga", + "scanImportMigration": "Importa", + "scanExchangeImport": "Ricevi" } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5e1e9a6..304fbbf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = ""; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -406,7 +406,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -529,7 +529,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = ""; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -539,7 +539,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -556,7 +556,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = wom; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -566,7 +566,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -638,7 +638,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = "wom-dev"; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -649,7 +649,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -722,7 +722,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = wom; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -732,7 +732,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -802,7 +802,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEEP_PROTOCOL = wom; DEVELOPMENT_TEAM = 5QA9U9924L; ENABLE_BITCODE = NO; @@ -812,7 +812,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = social.wom.pocket; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/app.dart b/lib/app.dart index 5393df6..a10dcf7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:dart_wom_connector/dart_wom_connector.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feature_discovery/feature_discovery.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; @@ -10,8 +13,12 @@ import 'package:wom_pocket/src/application/app_state.dart'; import 'package:wom_pocket/src/migration/application/import_notifier.dart'; import 'package:wom_pocket/src/migration/ui/import_screen.dart'; import 'package:wom_pocket/src/models/deep_link_model.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; import 'package:wom_pocket/src/my_logger.dart'; +import 'package:wom_pocket/src/nfc/application/nfc_notifier.dart'; +import 'package:wom_pocket/src/nfc/utils.dart'; import 'package:wom_pocket/src/screens/home/home_screen.dart'; +import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; import 'package:wom_pocket/src/screens/pin/pin_screen.dart'; import 'package:wom_pocket/src/screens/intro/intro.dart'; @@ -25,7 +32,6 @@ bool fakeModeVar = false; String? fakeData; class App extends ConsumerWidget { - App({Key? key}) : super(key: key); @override @@ -93,48 +99,69 @@ class GateWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(appNotifierProvider); - ref.listen>( + ref.listen>( deepLinkNotifierProvider, (previous, next) { logger.i("APP BLOC LISTENER ----> state is: $next"); if (next is AsyncData) { final data = next.value; - if (data != null) { - // _pinBloc = PinBloc(state.value); - // _pinBloc = PinBloc(state.deepLinkModel); - // var blocProviderPin = BlocProvider( - // create: (context) => _pinBloc, - // child: PinScreen(), - // ); - if (data.type == TransactionType.MIGRATION_IMPORT) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ProviderScope( - overrides: [ - deeplinkProvider.overrideWithValue(data), - importNotifierProvider - ], - child: ImportScreen(), + if (data == null) return; + final totemData = validateTotemQrCodeWithRegex(data); + if (totemData != null) { + launchTotemDialog(context, totemData); + } else { + try { + logger.i("AppNotifier uri : $data"); + final deepLink = DeepLinkModel.fromUri(Uri.parse(data)); + + if (deepLink.type == TransactionType.MIGRATION_IMPORT) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProviderScope( + overrides: [ + deeplinkProvider.overrideWithValue(deepLink), + importNotifierProvider + ], + child: ImportScreen(), + ), ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ProviderScope( - overrides: [deeplinkProvider.overrideWithValue(data)], - child: PinScreen(), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProviderScope( + overrides: [deeplinkProvider.overrideWithValue(deepLink)], + child: PinScreen(), + ), ), - ), - ); + ); + } + } on PlatformException catch (ex, st) { + logger.e('AppRepository: error getting deep link', + error: ex, stackTrace: st); + } on FormatException catch (ex, st) { + logger.e('Error getting deep link', error: ex, stackTrace: st); + } catch (ex, st) { + logger.e('Error getting deep link', error: ex, stackTrace: st); } } } }, ); + if (Platform.isAndroid) { + ref.listen>(nfcBackgroundNotifierProvider, + (previous, next) async { + print('getNfcIntentProvider new intent'); + final currentState = next; + if (currentState is AsyncData && currentState.value != null) { + await launchTotemDialog(context, currentState.requireValue!); + ref.read(nfcBackgroundNotifierProvider.notifier).unlock(); + } + }); + } logger.i("APP BLOC BUILDER ----> state is: $state"); if (state is AsyncData) { diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 6d5579f..9e02305 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -52,7 +52,7 @@ Future main() async { noBoxingByDefault: true, printEmojis: false, ), - filter: ReleaseFilter(), + // filter: ReleaseFilter(), output: DevOutput(), ); logger.i('DEV VERSION'); diff --git a/lib/src/application/app_notifier.dart b/lib/src/application/app_notifier.dart index aa33b76..58653d3 100644 --- a/lib/src/application/app_notifier.dart +++ b/lib/src/application/app_notifier.dart @@ -1,9 +1,11 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:nfc_background/nfc_background.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uni_links/uni_links.dart'; import 'package:wom_pocket/src/models/deep_link_model.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; import 'package:wom_pocket/src/my_logger.dart'; import 'package:wom_pocket/src/new_home/application/wom_stats_notifier.dart'; import 'package:wom_pocket/src/services/app_repository.dart'; @@ -11,57 +13,121 @@ import 'package:wom_pocket/src/utils/utils.dart'; import 'app_state.dart'; +part 'app_notifier.g.dart'; + bool isFirstOpen = false; +@riverpod +NfcBackground getNFCBackground(GetNFCBackgroundRef ref) { + return NfcBackground(); +} + +@riverpod +Stream getNfcIntent(GetNfcIntentRef ref) async* { + final stream = ref.watch(getNFCBackgroundProvider).backgroundIntentStream; + await for (final link in stream) { + logger.i("Subscription stream uri : $link"); + final totemData = validateTotemQrCodeWithRegex(link); + if (totemData != null) { + yield totemData; + } + } +} + +@riverpod +class NfcBackgroundNotifier extends _$NfcBackgroundNotifier { + bool _lock = false; + + @override + Future build() async { + print('NfcBackgroundNotifier build'); + ref.listen>(getNfcIntentProvider, + (previous, next) async { + print('getNfcIntentProvider new intent'); + + final currentState = next; + if (!_lock && currentState is AsyncData) { + _lock = true; + state = AsyncData(currentState.requireValue); + } + }); + + // final initial = await ref.watch(getNFCBackgroundProvider).getInitialData(); + // if (initial != null) { + // final totemData = validateTotemQrCodeWithRegex(initial); + // if (totemData != null) { + // _lock = true; + // return totemData; + // } + // } + return null; + } + + unlock() { + _lock = false; + } +} + final _deepLinkStreamNotifierProvider = - StreamProvider((ref) async* { - await for (final s in uriLinkStream) { + StreamProvider((ref) async* { + await for (final s in linkStream) { logger.i("Subscription stream uri : $s"); - final deepLinkModel = DeepLinkModel.fromUri(s); - yield deepLinkModel; + // final deepLinkModel = DeepLinkModel.fromUri(s); + yield s; } }); +/*@freezed +class DeepLinkState with DeepLinkState { + const factory DeepLinkState.single({ + DeepLinkModel? deepL, + TotemData? totemData, + }) = _DeepLinkState; +}*/ + final deepLinkNotifierProvider = - AsyncNotifierProvider( + AsyncNotifierProvider( DeepLinkNotifier.new); -class DeepLinkNotifier extends AsyncNotifier { +class DeepLinkNotifier extends AsyncNotifier { @override - FutureOr build() async { - // final streamDeepLink = ref.watch(_deepLinkStreamNotifierProvider); - + FutureOr build() async { ref.listen(_deepLinkStreamNotifierProvider, (previous, next) { if (next is AsyncData) { final deepLink = next.valueOrNull; if (deepLink != null) { + logger.i("deeplink: $deepLink"); state = AsyncData(deepLink); } } }); - final initialDeepLink = await getDeepLink(); + + + final initialDeepLink = await getInitialLink(); if (initialDeepLink != null) { + logger.i("initialDeepLink: $initialDeepLink"); await Future.delayed(Duration(milliseconds: 250)); return initialDeepLink; } return null; } - Future getDeepLink() async { +/* Future getDeepLink() async { DeepLinkModel? deepLinkModel; try { Uri? initialUri = await getInitialUri(); logger.i("AppNotifier uri : $initialUri"); deepLinkModel = DeepLinkModel.fromUri(initialUri); } on PlatformException catch (ex, st) { - logger.e('AppRepository: error getting deep link', error: ex, stackTrace: st); + logger.e('AppRepository: error getting deep link', + error: ex, stackTrace: st); } on FormatException catch (ex, st) { logger.e('Error getting deep link', error: ex, stackTrace: st); } catch (ex, st) { logger.e('Error getting deep link', error: ex, stackTrace: st); } return deepLinkModel; - } + }*/ } final appNotifierProvider = diff --git a/lib/src/application/app_notifier.g.dart b/lib/src/application/app_notifier.g.dart new file mode 100644 index 0000000..af33762 --- /dev/null +++ b/lib/src/application/app_notifier.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getNFCBackgroundHash() => r'd13e770cf050f355b097c0fa67dc5defbfcdafaf'; + +/// See also [getNFCBackground]. +@ProviderFor(getNFCBackground) +final getNFCBackgroundProvider = AutoDisposeProvider.internal( + getNFCBackground, + name: r'getNFCBackgroundProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$getNFCBackgroundHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef GetNFCBackgroundRef = AutoDisposeProviderRef; +String _$getNfcIntentHash() => r'9bdb9b09c237adb2353994c3f169d35371c7fc3d'; + +/// See also [getNfcIntent]. +@ProviderFor(getNfcIntent) +final getNfcIntentProvider = AutoDisposeStreamProvider.internal( + getNfcIntent, + name: r'getNfcIntentProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$getNfcIntentHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef GetNfcIntentRef = AutoDisposeStreamProviderRef; +String _$nfcBackgroundNotifierHash() => + r'a10eb51d3cc82eb9f798418713034a6129742a88'; + +/// See also [NfcBackgroundNotifier]. +@ProviderFor(NfcBackgroundNotifier) +final nfcBackgroundNotifierProvider = AutoDisposeAsyncNotifierProvider< + NfcBackgroundNotifier, TotemData?>.internal( + NfcBackgroundNotifier.new, + name: r'nfcBackgroundNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$nfcBackgroundNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$NfcBackgroundNotifier = AutoDisposeAsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/new_home/ui/new_home.dart b/lib/src/new_home/ui/new_home.dart index f97a2ca..8c6c055 100644 --- a/lib/src/new_home/ui/new_home.dart +++ b/lib/src/new_home/ui/new_home.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -6,21 +7,69 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wom_pocket/src/my_logger.dart'; import 'package:wom_pocket/src/new_home/application/wom_stats_notifier.dart'; import 'package:wom_pocket/src/new_home/ui/aim_chart.dart'; +import 'package:wom_pocket/src/new_home/ui/nfc_widget.dart'; import 'package:wom_pocket/src/new_home/ui/section_title.dart'; +import 'package:wom_pocket/src/nfc/application/nfc_notifier.dart'; +import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; import 'package:wom_pocket/src/screens/home/widgets/transaction_list.dart'; import 'package:wom_pocket/src/screens/map/map_screen.dart'; import 'package:wom_pocket/src/transaction/ui/transactions_screen.dart'; import 'package:wom_pocket/src/widgets/my_appbar.dart'; -class NewHome extends ConsumerWidget { +class NewHome extends ConsumerStatefulWidget { const NewHome({Key? key}) : super(key: key); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _NewHomeState(); +} + +class _NewHomeState extends ConsumerState with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + // ref.read(nFCNotifierProvider.notifier).resume(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Future didChangeAppLifecycleState(AppLifecycleState state) async { + if (state == AppLifecycleState.resumed) { + ref.read(nFCNotifierProvider.notifier).resume(); + } + } + + @override + Widget build(BuildContext context) { + // ref.listen(nFCNotifierProvider, (previous, next) async { + // if (previous is NFCStateListening && next is NFCStateData) { + // showDialog( + // context: context, + // barrierDismissible: false, + // builder: (_) => PopScope( + // canPop: false, + // child: TotemDialog( + // totemData: next.totemData, + // ), + // ), + // ).then((value){ + // ref.read(nFCNotifierProvider.notifier).resume(); + // }); + // } + // }); final transactionCountAsync = ref.watch(transactionCountNotifierProvider); - print(transactionCountAsync); + return Scaffold( - appBar: PocketAppBar(), + appBar: PocketAppBar( + actions: [ + if (Platform.isAndroid) NfcWidget(), + ], + ), body: SafeArea( child: transactionCountAsync.when( data: (count) { @@ -50,8 +99,7 @@ class NewHome extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ SectionTitle( - title: - 'womMap'.tr(), + title: 'womMap'.tr(), leftPadding: 16, ), AspectRatio( diff --git a/lib/src/new_home/ui/nfc_widget.dart b/lib/src/new_home/ui/nfc_widget.dart new file mode 100644 index 0000000..a0b04d6 --- /dev/null +++ b/lib/src/new_home/ui/nfc_widget.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:wom_pocket/src/nfc/application/nfc_notifier.dart'; + +class NfcWidget extends ConsumerWidget { + const NfcWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(nFCNotifierProvider); + return Padding( + padding: EdgeInsets.all(8), + child: Tooltip( + message: getMessage(state), + child: Icon( + switch (state) { + NFCStateUnavailable() => MdiIcons.nfcVariantOff, + _ => MdiIcons.nfcVariant, + }, + color: getColor(state), + size: 16, + ), + ), + ); + return Container(); + } + + getMessage(NFCState state) { + return switch (state) { + NFCStateUnavailable() => 'NFC non disponibile', + _ => 'NFC attivo', + }; + } + + getColor(NFCState state) { + return switch (state) { + NFCStateData() => Colors.green, + NFCStateError() => Colors.red, + NFCStateUnavailable() => Colors.grey, + _ => Colors.grey, + }; + } +} diff --git a/lib/src/nfc/application/nfc_notifier.dart b/lib/src/nfc/application/nfc_notifier.dart new file mode 100644 index 0000000..a9e30cb --- /dev/null +++ b/lib/src/nfc/application/nfc_notifier.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:nfc_manager/nfc_manager.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; +import 'package:wom_pocket/src/my_logger.dart'; + +part 'nfc_notifier.freezed.dart'; + +part 'nfc_notifier.g.dart'; + +@freezed +class NFCState with _$NFCState { + const factory NFCState.data({ + required TotemData totemData, + }) = NFCStateData; + + const factory NFCState.listening() = NFCStateListening; + + const factory NFCState.unavailable() = NFCStateUnavailable; + + const factory NFCState.loading() = NFCStateLoading; + + const factory NFCState.invalidData() = NFCStateInvalidData; + + const factory NFCState.error(Object error, StackTrace st) = NFCStateError; +} + +@riverpod +class NFCNotifier extends _$NFCNotifier { + @override + NFCState build() { + // ref.onDispose(() { + // stop(); + // }); + _init(); + return NFCState.loading(); + } + + void resume() { + _init(); + } + + Future stop() async { + await NfcManager.instance.stopSession().catchError((_) {}); + } + + Future _init() async { + if (!(await NfcManager.instance.isAvailable())) { + state = NFCState.unavailable(); + } else { + state = NFCState.loading(); + } + // state = NFCState.listening(); + // NfcManager.instance.startSession( + // onDiscovered: (tag) async { + // try { + // final t = _processNFC(tag); + // if (t == null) { + // state = NFCStateInvalidData(); + // } else { + // await NfcManager.instance.stopSession(); + // state = NFCStateData(totemData: t); + // } + // } catch (ex, st) { + // logger.e("", error: ex, stackTrace: st); + // state = NFCStateError(ex, StackTrace.empty); + // // await NfcManager.instance.stopSession().catchError((_) { + // // /* no op */ + // // }); + // } + // }, + // ).catchError((ex, st) { + // logger.e("", error: ex, stackTrace: st); + // state = NFCStateError(ex, StackTrace.empty); + // }); + } + + TotemData? _processNFC(NfcTag tag) { + final tech = Ndef.from(tag); + TotemData? _t; + if (tech is Ndef) { + final cachedMessage = tech.cachedMessage; + if (cachedMessage != null) { + for (int i = 0; i < cachedMessage.records.length; i++) { + final record = cachedMessage.records[i]; + + final _record = Record.fromNdef(record); + if (_record is WellknownUriRecord) { + final link = _record.uri.toString(); + final totemData = validateTotemQrCodeWithRegex(link); + if (totemData != null) { + _t = totemData; + break; + } + } + } + } + } + return _t; + } +} + +abstract class Record { + NdefRecord toNdef(); + + static Record fromNdef(NdefRecord record) { + print("NdefTypeNameFormat: ${record.typeNameFormat}"); + if (record.typeNameFormat == NdefTypeNameFormat.nfcWellknown && + record.type.length == 1 && + record.type.first == 0x55) return WellknownUriRecord.fromNdef(record); + // if (record.typeNameFormat == NdefTypeNameFormat.nfcWellknown && + // record.type.length == 1 && + // record.type.first == 0x54) return WellknownTextRecord.fromNdef(record); + // if (record.typeNameFormat == NdefTypeNameFormat.media) + // return MimeRecord.fromNdef(record); + // if (record.typeNameFormat == NdefTypeNameFormat.absoluteUri) + // return AbsoluteUriRecord.fromNdef(record); + // if (record.typeNameFormat == NdefTypeNameFormat.nfcExternal) + // return ExternalRecord.fromNdef(record); + throw Exception('Unsupported record'); + } +} + +class WellknownUriRecord implements Record { + WellknownUriRecord({this.identifier, required this.uri}); + + final Uint8List? identifier; + + final Uri uri; + + static WellknownUriRecord fromNdef(NdefRecord record) { + final prefix = NdefRecord.URI_PREFIX_LIST[record.payload.first]; + final bodyBytes = record.payload.sublist(1); + return WellknownUriRecord( + identifier: record.identifier, + uri: Uri.parse(prefix + utf8.decode(bodyBytes)), + ); + } + + @override + NdefRecord toNdef() { + var prefixIndex = NdefRecord.URI_PREFIX_LIST + .indexWhere((e) => uri.toString().startsWith(e), 1); + if (prefixIndex < 0) prefixIndex = 0; + final prefix = NdefRecord.URI_PREFIX_LIST[prefixIndex]; + return NdefRecord( + typeNameFormat: NdefTypeNameFormat.nfcWellknown, + type: Uint8List.fromList([0x55]), + identifier: Uint8List(0), + payload: Uint8List.fromList([ + prefixIndex, + ...utf8.encode(uri.toString().substring(prefix.length)), + ]), + ); + } +} + +class UnsupportedRecord implements Record { + UnsupportedRecord(this.record); + + final NdefRecord record; + + static UnsupportedRecord fromNdef(NdefRecord record) { + return UnsupportedRecord(record); + } + + @override + NdefRecord toNdef() => record; +} diff --git a/lib/src/nfc/application/nfc_notifier.freezed.dart b/lib/src/nfc/application/nfc_notifier.freezed.dart new file mode 100644 index 0000000..c9bc55a --- /dev/null +++ b/lib/src/nfc/application/nfc_notifier.freezed.dart @@ -0,0 +1,938 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'nfc_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$NFCState { + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NFCStateCopyWith<$Res> { + factory $NFCStateCopyWith(NFCState value, $Res Function(NFCState) then) = + _$NFCStateCopyWithImpl<$Res, NFCState>; +} + +/// @nodoc +class _$NFCStateCopyWithImpl<$Res, $Val extends NFCState> + implements $NFCStateCopyWith<$Res> { + _$NFCStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$NFCStateDataImplCopyWith<$Res> { + factory _$$NFCStateDataImplCopyWith( + _$NFCStateDataImpl value, $Res Function(_$NFCStateDataImpl) then) = + __$$NFCStateDataImplCopyWithImpl<$Res>; + @useResult + $Res call({TotemData totemData}); + + $TotemDataCopyWith<$Res> get totemData; +} + +/// @nodoc +class __$$NFCStateDataImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateDataImpl> + implements _$$NFCStateDataImplCopyWith<$Res> { + __$$NFCStateDataImplCopyWithImpl( + _$NFCStateDataImpl _value, $Res Function(_$NFCStateDataImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totemData = null, + }) { + return _then(_$NFCStateDataImpl( + totemData: null == totemData + ? _value.totemData + : totemData // ignore: cast_nullable_to_non_nullable + as TotemData, + )); + } + + @override + @pragma('vm:prefer-inline') + $TotemDataCopyWith<$Res> get totemData { + return $TotemDataCopyWith<$Res>(_value.totemData, (value) { + return _then(_value.copyWith(totemData: value)); + }); + } +} + +/// @nodoc + +class _$NFCStateDataImpl implements NFCStateData { + const _$NFCStateDataImpl({required this.totemData}); + + @override + final TotemData totemData; + + @override + String toString() { + return 'NFCState.data(totemData: $totemData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NFCStateDataImpl && + (identical(other.totemData, totemData) || + other.totemData == totemData)); + } + + @override + int get hashCode => Object.hash(runtimeType, totemData); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NFCStateDataImplCopyWith<_$NFCStateDataImpl> get copyWith => + __$$NFCStateDataImplCopyWithImpl<_$NFCStateDataImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return data(totemData); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return data?.call(totemData); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (data != null) { + return data(totemData); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return data(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return data?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (data != null) { + return data(this); + } + return orElse(); + } +} + +abstract class NFCStateData implements NFCState { + const factory NFCStateData({required final TotemData totemData}) = + _$NFCStateDataImpl; + + TotemData get totemData; + @JsonKey(ignore: true) + _$$NFCStateDataImplCopyWith<_$NFCStateDataImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$NFCStateListeningImplCopyWith<$Res> { + factory _$$NFCStateListeningImplCopyWith(_$NFCStateListeningImpl value, + $Res Function(_$NFCStateListeningImpl) then) = + __$$NFCStateListeningImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$NFCStateListeningImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateListeningImpl> + implements _$$NFCStateListeningImplCopyWith<$Res> { + __$$NFCStateListeningImplCopyWithImpl(_$NFCStateListeningImpl _value, + $Res Function(_$NFCStateListeningImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$NFCStateListeningImpl implements NFCStateListening { + const _$NFCStateListeningImpl(); + + @override + String toString() { + return 'NFCState.listening()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$NFCStateListeningImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return listening(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return listening?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (listening != null) { + return listening(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return listening(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return listening?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (listening != null) { + return listening(this); + } + return orElse(); + } +} + +abstract class NFCStateListening implements NFCState { + const factory NFCStateListening() = _$NFCStateListeningImpl; +} + +/// @nodoc +abstract class _$$NFCStateUnavailableImplCopyWith<$Res> { + factory _$$NFCStateUnavailableImplCopyWith(_$NFCStateUnavailableImpl value, + $Res Function(_$NFCStateUnavailableImpl) then) = + __$$NFCStateUnavailableImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$NFCStateUnavailableImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateUnavailableImpl> + implements _$$NFCStateUnavailableImplCopyWith<$Res> { + __$$NFCStateUnavailableImplCopyWithImpl(_$NFCStateUnavailableImpl _value, + $Res Function(_$NFCStateUnavailableImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$NFCStateUnavailableImpl implements NFCStateUnavailable { + const _$NFCStateUnavailableImpl(); + + @override + String toString() { + return 'NFCState.unavailable()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NFCStateUnavailableImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return unavailable(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return unavailable?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (unavailable != null) { + return unavailable(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return unavailable(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return unavailable?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (unavailable != null) { + return unavailable(this); + } + return orElse(); + } +} + +abstract class NFCStateUnavailable implements NFCState { + const factory NFCStateUnavailable() = _$NFCStateUnavailableImpl; +} + +/// @nodoc +abstract class _$$NFCStateLoadingImplCopyWith<$Res> { + factory _$$NFCStateLoadingImplCopyWith(_$NFCStateLoadingImpl value, + $Res Function(_$NFCStateLoadingImpl) then) = + __$$NFCStateLoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$NFCStateLoadingImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateLoadingImpl> + implements _$$NFCStateLoadingImplCopyWith<$Res> { + __$$NFCStateLoadingImplCopyWithImpl( + _$NFCStateLoadingImpl _value, $Res Function(_$NFCStateLoadingImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$NFCStateLoadingImpl implements NFCStateLoading { + const _$NFCStateLoadingImpl(); + + @override + String toString() { + return 'NFCState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$NFCStateLoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class NFCStateLoading implements NFCState { + const factory NFCStateLoading() = _$NFCStateLoadingImpl; +} + +/// @nodoc +abstract class _$$NFCStateInvalidDataImplCopyWith<$Res> { + factory _$$NFCStateInvalidDataImplCopyWith(_$NFCStateInvalidDataImpl value, + $Res Function(_$NFCStateInvalidDataImpl) then) = + __$$NFCStateInvalidDataImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$NFCStateInvalidDataImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateInvalidDataImpl> + implements _$$NFCStateInvalidDataImplCopyWith<$Res> { + __$$NFCStateInvalidDataImplCopyWithImpl(_$NFCStateInvalidDataImpl _value, + $Res Function(_$NFCStateInvalidDataImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$NFCStateInvalidDataImpl implements NFCStateInvalidData { + const _$NFCStateInvalidDataImpl(); + + @override + String toString() { + return 'NFCState.invalidData()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NFCStateInvalidDataImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return invalidData(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return invalidData?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (invalidData != null) { + return invalidData(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return invalidData(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return invalidData?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (invalidData != null) { + return invalidData(this); + } + return orElse(); + } +} + +abstract class NFCStateInvalidData implements NFCState { + const factory NFCStateInvalidData() = _$NFCStateInvalidDataImpl; +} + +/// @nodoc +abstract class _$$NFCStateErrorImplCopyWith<$Res> { + factory _$$NFCStateErrorImplCopyWith( + _$NFCStateErrorImpl value, $Res Function(_$NFCStateErrorImpl) then) = + __$$NFCStateErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({Object error, StackTrace st}); +} + +/// @nodoc +class __$$NFCStateErrorImplCopyWithImpl<$Res> + extends _$NFCStateCopyWithImpl<$Res, _$NFCStateErrorImpl> + implements _$$NFCStateErrorImplCopyWith<$Res> { + __$$NFCStateErrorImplCopyWithImpl( + _$NFCStateErrorImpl _value, $Res Function(_$NFCStateErrorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? st = null, + }) { + return _then(_$NFCStateErrorImpl( + null == error ? _value.error : error, + null == st + ? _value.st + : st // ignore: cast_nullable_to_non_nullable + as StackTrace, + )); + } +} + +/// @nodoc + +class _$NFCStateErrorImpl implements NFCStateError { + const _$NFCStateErrorImpl(this.error, this.st); + + @override + final Object error; + @override + final StackTrace st; + + @override + String toString() { + return 'NFCState.error(error: $error, st: $st)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NFCStateErrorImpl && + const DeepCollectionEquality().equals(other.error, error) && + (identical(other.st, st) || other.st == st)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(error), st); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$NFCStateErrorImplCopyWith<_$NFCStateErrorImpl> get copyWith => + __$$NFCStateErrorImplCopyWithImpl<_$NFCStateErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(TotemData totemData) data, + required TResult Function() listening, + required TResult Function() unavailable, + required TResult Function() loading, + required TResult Function() invalidData, + required TResult Function(Object error, StackTrace st) error, + }) { + return error(this.error, st); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(TotemData totemData)? data, + TResult? Function()? listening, + TResult? Function()? unavailable, + TResult? Function()? loading, + TResult? Function()? invalidData, + TResult? Function(Object error, StackTrace st)? error, + }) { + return error?.call(this.error, st); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(TotemData totemData)? data, + TResult Function()? listening, + TResult Function()? unavailable, + TResult Function()? loading, + TResult Function()? invalidData, + TResult Function(Object error, StackTrace st)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error, st); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(NFCStateData value) data, + required TResult Function(NFCStateListening value) listening, + required TResult Function(NFCStateUnavailable value) unavailable, + required TResult Function(NFCStateLoading value) loading, + required TResult Function(NFCStateInvalidData value) invalidData, + required TResult Function(NFCStateError value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(NFCStateData value)? data, + TResult? Function(NFCStateListening value)? listening, + TResult? Function(NFCStateUnavailable value)? unavailable, + TResult? Function(NFCStateLoading value)? loading, + TResult? Function(NFCStateInvalidData value)? invalidData, + TResult? Function(NFCStateError value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(NFCStateData value)? data, + TResult Function(NFCStateListening value)? listening, + TResult Function(NFCStateUnavailable value)? unavailable, + TResult Function(NFCStateLoading value)? loading, + TResult Function(NFCStateInvalidData value)? invalidData, + TResult Function(NFCStateError value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class NFCStateError implements NFCState { + const factory NFCStateError(final Object error, final StackTrace st) = + _$NFCStateErrorImpl; + + Object get error; + StackTrace get st; + @JsonKey(ignore: true) + _$$NFCStateErrorImplCopyWith<_$NFCStateErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/nfc/application/nfc_notifier.g.dart b/lib/src/nfc/application/nfc_notifier.g.dart new file mode 100644 index 0000000..d177889 --- /dev/null +++ b/lib/src/nfc/application/nfc_notifier.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'nfc_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$nFCNotifierHash() => r'445208d28465a16059144439a36e28e858db76d4'; + +/// See also [NFCNotifier]. +@ProviderFor(NFCNotifier) +final nFCNotifierProvider = + AutoDisposeNotifierProvider.internal( + NFCNotifier.new, + name: r'nFCNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$nFCNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$NFCNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/nfc/ui/nfc_session_dialog.dart b/lib/src/nfc/ui/nfc_session_dialog.dart new file mode 100644 index 0000000..8b6d233 --- /dev/null +++ b/lib/src/nfc/ui/nfc_session_dialog.dart @@ -0,0 +1,58 @@ +/* +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; +import 'package:wom_pocket/src/nfc/application/nfc_notifier.dart'; +import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; + +class NFCSessionDialog extends ConsumerWidget { + const NFCSessionDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(nFCNotifierProvider, (previous, next) { + if (previous is NFCStateListening && next is NFCStateData) { + Navigator.of(context).pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PopScope( + canPop: false, + child: TotemDialog( + totemData: next.totemData, + ), + ), + ); + } + }); + final state = ref.watch(nFCNotifierProvider); + return Dialog( + child: Container( + height: 300, + // constraints: BoxConstraints(maxHeight: 300, minHeight: 100), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (state is NFCStateLoading) + CircularProgressIndicator() + else if (state is NFCStateListening) + Text('Avvicinati al TAG NFC') + else if (state is NFCStateUnavailable) + Text('Lettore NFC non disponibile') + else if (state is NFCStateError) ...[ + Text('Si è verificato un errore'), + ElevatedButton( + onPressed: () { + ref.read(nFCNotifierProvider.notifier).resume(); + }, + child: Text('Riprova'), + ), + ] + ], + ), + ), + ); + } +} +*/ diff --git a/lib/src/nfc/utils.dart b/lib/src/nfc/utils.dart new file mode 100644 index 0000000..fe6acbd --- /dev/null +++ b/lib/src/nfc/utils.dart @@ -0,0 +1,18 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; +import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; + +Future launchTotemDialog( + BuildContext context, TotemData totemData) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PopScope( + canPop: false, + child: TotemDialog( + totemData: totemData, + ), + ), + ); +} diff --git a/lib/src/scanner/application/scanner_state.dart b/lib/src/scanner/application/scanner_state.dart new file mode 100644 index 0000000..078206f --- /dev/null +++ b/lib/src/scanner/application/scanner_state.dart @@ -0,0 +1,78 @@ +import 'package:dart_wom_connector/dart_wom_connector.dart' + show TransactionType; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wom_pocket/src/models/deep_link_model.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; +import 'package:wom_pocket/src/my_logger.dart'; + +part 'scanner_state.freezed.dart'; + +part 'scanner_state.g.dart'; + +@freezed +class ScannerState with _$ScannerState { + const factory ScannerState.single({ + required String url, + required int total, + required TransactionType type, + TotemData? totemData, + }) = ScannerStateSingle; + + const factory ScannerState.multiple({required int total}) = + ScannerStateMultiple; + + const factory ScannerState.processing() = ScannerStateProcessing; + + const factory ScannerState.empty({required int total}) = ScannerStateEmpty; +} + +@riverpod +class ScannerNotifier extends _$ScannerNotifier { + @override + ScannerState build() { + return ScannerStateProcessing(); + } + + bool onProcessing(List barcodes) { + final validQr = <(String, TransactionType)>[]; + for (final qr in barcodes) { + final rawValue = qr.rawValue; + if (rawValue != null) { + final totemData = validateTotemQrCodeWithRegex(rawValue); + if (totemData != null) { + validQr.add((rawValue, TransactionType.VOUCHERS)); + } else { + try { + final deep = DeepLinkModel.fromUri(Uri.parse(rawValue)); + validQr.add((rawValue, deep.type)); + } catch (ex) {} + } + } + } + logger.i('valid qr ${validQr.length}/${barcodes.length}'); + if (validQr.isEmpty) { + // Non ci sono qr code validi + state = ScannerStateEmpty(total: barcodes.length); + return false; + } else if (validQr.length > 1) { + // Troppi qr code validi inquadra meglio + state = ScannerStateMultiple(total: barcodes.length); + return true; + } else { + // Riscatta wom mostra pulsante + state = ScannerStateSingle( + url: validQr.first.$1, + type: validQr.first.$2, + total: barcodes.length, + totemData: validateTotemQrCodeWithRegex(validQr.first.$1), + ); + return true; + } + } + + void reset() { + state = ScannerStateProcessing(); + } +} diff --git a/lib/src/scanner/application/scanner_state.freezed.dart b/lib/src/scanner/application/scanner_state.freezed.dart new file mode 100644 index 0000000..074743b --- /dev/null +++ b/lib/src/scanner/application/scanner_state.freezed.dart @@ -0,0 +1,726 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'scanner_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ScannerState { + @optionalTypeArgs + TResult when({ + required TResult Function( + String url, int total, TransactionType type, TotemData? totemData) + single, + required TResult Function(int total) multiple, + required TResult Function() processing, + required TResult Function(int total) empty, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult? Function(int total)? multiple, + TResult? Function()? processing, + TResult? Function(int total)? empty, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult Function(int total)? multiple, + TResult Function()? processing, + TResult Function(int total)? empty, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(ScannerStateSingle value) single, + required TResult Function(ScannerStateMultiple value) multiple, + required TResult Function(ScannerStateProcessing value) processing, + required TResult Function(ScannerStateEmpty value) empty, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ScannerStateSingle value)? single, + TResult? Function(ScannerStateMultiple value)? multiple, + TResult? Function(ScannerStateProcessing value)? processing, + TResult? Function(ScannerStateEmpty value)? empty, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ScannerStateSingle value)? single, + TResult Function(ScannerStateMultiple value)? multiple, + TResult Function(ScannerStateProcessing value)? processing, + TResult Function(ScannerStateEmpty value)? empty, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ScannerStateCopyWith<$Res> { + factory $ScannerStateCopyWith( + ScannerState value, $Res Function(ScannerState) then) = + _$ScannerStateCopyWithImpl<$Res, ScannerState>; +} + +/// @nodoc +class _$ScannerStateCopyWithImpl<$Res, $Val extends ScannerState> + implements $ScannerStateCopyWith<$Res> { + _$ScannerStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$ScannerStateSingleImplCopyWith<$Res> { + factory _$$ScannerStateSingleImplCopyWith(_$ScannerStateSingleImpl value, + $Res Function(_$ScannerStateSingleImpl) then) = + __$$ScannerStateSingleImplCopyWithImpl<$Res>; + @useResult + $Res call( + {String url, int total, TransactionType type, TotemData? totemData}); + + $TotemDataCopyWith<$Res>? get totemData; +} + +/// @nodoc +class __$$ScannerStateSingleImplCopyWithImpl<$Res> + extends _$ScannerStateCopyWithImpl<$Res, _$ScannerStateSingleImpl> + implements _$$ScannerStateSingleImplCopyWith<$Res> { + __$$ScannerStateSingleImplCopyWithImpl(_$ScannerStateSingleImpl _value, + $Res Function(_$ScannerStateSingleImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? total = null, + Object? type = null, + Object? totemData = freezed, + }) { + return _then(_$ScannerStateSingleImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as TransactionType, + totemData: freezed == totemData + ? _value.totemData + : totemData // ignore: cast_nullable_to_non_nullable + as TotemData?, + )); + } + + @override + @pragma('vm:prefer-inline') + $TotemDataCopyWith<$Res>? get totemData { + if (_value.totemData == null) { + return null; + } + + return $TotemDataCopyWith<$Res>(_value.totemData!, (value) { + return _then(_value.copyWith(totemData: value)); + }); + } +} + +/// @nodoc + +class _$ScannerStateSingleImpl implements ScannerStateSingle { + const _$ScannerStateSingleImpl( + {required this.url, + required this.total, + required this.type, + this.totemData}); + + @override + final String url; + @override + final int total; + @override + final TransactionType type; + @override + final TotemData? totemData; + + @override + String toString() { + return 'ScannerState.single(url: $url, total: $total, type: $type, totemData: $totemData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ScannerStateSingleImpl && + (identical(other.url, url) || other.url == url) && + (identical(other.total, total) || other.total == total) && + (identical(other.type, type) || other.type == type) && + (identical(other.totemData, totemData) || + other.totemData == totemData)); + } + + @override + int get hashCode => Object.hash(runtimeType, url, total, type, totemData); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ScannerStateSingleImplCopyWith<_$ScannerStateSingleImpl> get copyWith => + __$$ScannerStateSingleImplCopyWithImpl<_$ScannerStateSingleImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String url, int total, TransactionType type, TotemData? totemData) + single, + required TResult Function(int total) multiple, + required TResult Function() processing, + required TResult Function(int total) empty, + }) { + return single(url, total, type, totemData); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult? Function(int total)? multiple, + TResult? Function()? processing, + TResult? Function(int total)? empty, + }) { + return single?.call(url, total, type, totemData); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult Function(int total)? multiple, + TResult Function()? processing, + TResult Function(int total)? empty, + required TResult orElse(), + }) { + if (single != null) { + return single(url, total, type, totemData); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ScannerStateSingle value) single, + required TResult Function(ScannerStateMultiple value) multiple, + required TResult Function(ScannerStateProcessing value) processing, + required TResult Function(ScannerStateEmpty value) empty, + }) { + return single(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ScannerStateSingle value)? single, + TResult? Function(ScannerStateMultiple value)? multiple, + TResult? Function(ScannerStateProcessing value)? processing, + TResult? Function(ScannerStateEmpty value)? empty, + }) { + return single?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ScannerStateSingle value)? single, + TResult Function(ScannerStateMultiple value)? multiple, + TResult Function(ScannerStateProcessing value)? processing, + TResult Function(ScannerStateEmpty value)? empty, + required TResult orElse(), + }) { + if (single != null) { + return single(this); + } + return orElse(); + } +} + +abstract class ScannerStateSingle implements ScannerState { + const factory ScannerStateSingle( + {required final String url, + required final int total, + required final TransactionType type, + final TotemData? totemData}) = _$ScannerStateSingleImpl; + + String get url; + int get total; + TransactionType get type; + TotemData? get totemData; + @JsonKey(ignore: true) + _$$ScannerStateSingleImplCopyWith<_$ScannerStateSingleImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ScannerStateMultipleImplCopyWith<$Res> { + factory _$$ScannerStateMultipleImplCopyWith(_$ScannerStateMultipleImpl value, + $Res Function(_$ScannerStateMultipleImpl) then) = + __$$ScannerStateMultipleImplCopyWithImpl<$Res>; + @useResult + $Res call({int total}); +} + +/// @nodoc +class __$$ScannerStateMultipleImplCopyWithImpl<$Res> + extends _$ScannerStateCopyWithImpl<$Res, _$ScannerStateMultipleImpl> + implements _$$ScannerStateMultipleImplCopyWith<$Res> { + __$$ScannerStateMultipleImplCopyWithImpl(_$ScannerStateMultipleImpl _value, + $Res Function(_$ScannerStateMultipleImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? total = null, + }) { + return _then(_$ScannerStateMultipleImpl( + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$ScannerStateMultipleImpl implements ScannerStateMultiple { + const _$ScannerStateMultipleImpl({required this.total}); + + @override + final int total; + + @override + String toString() { + return 'ScannerState.multiple(total: $total)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ScannerStateMultipleImpl && + (identical(other.total, total) || other.total == total)); + } + + @override + int get hashCode => Object.hash(runtimeType, total); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ScannerStateMultipleImplCopyWith<_$ScannerStateMultipleImpl> + get copyWith => + __$$ScannerStateMultipleImplCopyWithImpl<_$ScannerStateMultipleImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String url, int total, TransactionType type, TotemData? totemData) + single, + required TResult Function(int total) multiple, + required TResult Function() processing, + required TResult Function(int total) empty, + }) { + return multiple(total); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult? Function(int total)? multiple, + TResult? Function()? processing, + TResult? Function(int total)? empty, + }) { + return multiple?.call(total); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult Function(int total)? multiple, + TResult Function()? processing, + TResult Function(int total)? empty, + required TResult orElse(), + }) { + if (multiple != null) { + return multiple(total); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ScannerStateSingle value) single, + required TResult Function(ScannerStateMultiple value) multiple, + required TResult Function(ScannerStateProcessing value) processing, + required TResult Function(ScannerStateEmpty value) empty, + }) { + return multiple(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ScannerStateSingle value)? single, + TResult? Function(ScannerStateMultiple value)? multiple, + TResult? Function(ScannerStateProcessing value)? processing, + TResult? Function(ScannerStateEmpty value)? empty, + }) { + return multiple?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ScannerStateSingle value)? single, + TResult Function(ScannerStateMultiple value)? multiple, + TResult Function(ScannerStateProcessing value)? processing, + TResult Function(ScannerStateEmpty value)? empty, + required TResult orElse(), + }) { + if (multiple != null) { + return multiple(this); + } + return orElse(); + } +} + +abstract class ScannerStateMultiple implements ScannerState { + const factory ScannerStateMultiple({required final int total}) = + _$ScannerStateMultipleImpl; + + int get total; + @JsonKey(ignore: true) + _$$ScannerStateMultipleImplCopyWith<_$ScannerStateMultipleImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ScannerStateProcessingImplCopyWith<$Res> { + factory _$$ScannerStateProcessingImplCopyWith( + _$ScannerStateProcessingImpl value, + $Res Function(_$ScannerStateProcessingImpl) then) = + __$$ScannerStateProcessingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$ScannerStateProcessingImplCopyWithImpl<$Res> + extends _$ScannerStateCopyWithImpl<$Res, _$ScannerStateProcessingImpl> + implements _$$ScannerStateProcessingImplCopyWith<$Res> { + __$$ScannerStateProcessingImplCopyWithImpl( + _$ScannerStateProcessingImpl _value, + $Res Function(_$ScannerStateProcessingImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$ScannerStateProcessingImpl implements ScannerStateProcessing { + const _$ScannerStateProcessingImpl(); + + @override + String toString() { + return 'ScannerState.processing()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ScannerStateProcessingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String url, int total, TransactionType type, TotemData? totemData) + single, + required TResult Function(int total) multiple, + required TResult Function() processing, + required TResult Function(int total) empty, + }) { + return processing(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult? Function(int total)? multiple, + TResult? Function()? processing, + TResult? Function(int total)? empty, + }) { + return processing?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult Function(int total)? multiple, + TResult Function()? processing, + TResult Function(int total)? empty, + required TResult orElse(), + }) { + if (processing != null) { + return processing(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ScannerStateSingle value) single, + required TResult Function(ScannerStateMultiple value) multiple, + required TResult Function(ScannerStateProcessing value) processing, + required TResult Function(ScannerStateEmpty value) empty, + }) { + return processing(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ScannerStateSingle value)? single, + TResult? Function(ScannerStateMultiple value)? multiple, + TResult? Function(ScannerStateProcessing value)? processing, + TResult? Function(ScannerStateEmpty value)? empty, + }) { + return processing?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ScannerStateSingle value)? single, + TResult Function(ScannerStateMultiple value)? multiple, + TResult Function(ScannerStateProcessing value)? processing, + TResult Function(ScannerStateEmpty value)? empty, + required TResult orElse(), + }) { + if (processing != null) { + return processing(this); + } + return orElse(); + } +} + +abstract class ScannerStateProcessing implements ScannerState { + const factory ScannerStateProcessing() = _$ScannerStateProcessingImpl; +} + +/// @nodoc +abstract class _$$ScannerStateEmptyImplCopyWith<$Res> { + factory _$$ScannerStateEmptyImplCopyWith(_$ScannerStateEmptyImpl value, + $Res Function(_$ScannerStateEmptyImpl) then) = + __$$ScannerStateEmptyImplCopyWithImpl<$Res>; + @useResult + $Res call({int total}); +} + +/// @nodoc +class __$$ScannerStateEmptyImplCopyWithImpl<$Res> + extends _$ScannerStateCopyWithImpl<$Res, _$ScannerStateEmptyImpl> + implements _$$ScannerStateEmptyImplCopyWith<$Res> { + __$$ScannerStateEmptyImplCopyWithImpl(_$ScannerStateEmptyImpl _value, + $Res Function(_$ScannerStateEmptyImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? total = null, + }) { + return _then(_$ScannerStateEmptyImpl( + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$ScannerStateEmptyImpl implements ScannerStateEmpty { + const _$ScannerStateEmptyImpl({required this.total}); + + @override + final int total; + + @override + String toString() { + return 'ScannerState.empty(total: $total)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ScannerStateEmptyImpl && + (identical(other.total, total) || other.total == total)); + } + + @override + int get hashCode => Object.hash(runtimeType, total); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ScannerStateEmptyImplCopyWith<_$ScannerStateEmptyImpl> get copyWith => + __$$ScannerStateEmptyImplCopyWithImpl<_$ScannerStateEmptyImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function( + String url, int total, TransactionType type, TotemData? totemData) + single, + required TResult Function(int total) multiple, + required TResult Function() processing, + required TResult Function(int total) empty, + }) { + return empty(total); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult? Function(int total)? multiple, + TResult? Function()? processing, + TResult? Function(int total)? empty, + }) { + return empty?.call(total); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function( + String url, int total, TransactionType type, TotemData? totemData)? + single, + TResult Function(int total)? multiple, + TResult Function()? processing, + TResult Function(int total)? empty, + required TResult orElse(), + }) { + if (empty != null) { + return empty(total); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ScannerStateSingle value) single, + required TResult Function(ScannerStateMultiple value) multiple, + required TResult Function(ScannerStateProcessing value) processing, + required TResult Function(ScannerStateEmpty value) empty, + }) { + return empty(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ScannerStateSingle value)? single, + TResult? Function(ScannerStateMultiple value)? multiple, + TResult? Function(ScannerStateProcessing value)? processing, + TResult? Function(ScannerStateEmpty value)? empty, + }) { + return empty?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ScannerStateSingle value)? single, + TResult Function(ScannerStateMultiple value)? multiple, + TResult Function(ScannerStateProcessing value)? processing, + TResult Function(ScannerStateEmpty value)? empty, + required TResult orElse(), + }) { + if (empty != null) { + return empty(this); + } + return orElse(); + } +} + +abstract class ScannerStateEmpty implements ScannerState { + const factory ScannerStateEmpty({required final int total}) = + _$ScannerStateEmptyImpl; + + int get total; + @JsonKey(ignore: true) + _$$ScannerStateEmptyImplCopyWith<_$ScannerStateEmptyImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/scanner/application/scanner_state.g.dart b/lib/src/scanner/application/scanner_state.g.dart new file mode 100644 index 0000000..f34d08a --- /dev/null +++ b/lib/src/scanner/application/scanner_state.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scanner_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$scannerNotifierHash() => r'6985cab5337a0760a8324fcf3a0d5bf71457ab9e'; + +/// See also [ScannerNotifier]. +@ProviderFor(ScannerNotifier) +final scannerNotifierProvider = + AutoDisposeNotifierProvider.internal( + ScannerNotifier.new, + name: r'scannerNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$scannerNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ScannerNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/scanner/ui/scan_screen.dart b/lib/src/scanner/ui/scan_screen.dart new file mode 100644 index 0000000..0e7f39a --- /dev/null +++ b/lib/src/scanner/ui/scan_screen.dart @@ -0,0 +1,286 @@ +import 'package:dart_wom_connector/dart_wom_connector.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:wom_pocket/src/models/deep_link_model.dart'; +import 'package:wom_pocket/src/models/totem_data.dart'; +import 'package:wom_pocket/src/scanner/application/scanner_state.dart'; + +import 'package:wom_pocket/src/widgets/scanner_overlay_shape.dart'; +import '../../my_logger.dart'; +import '../../utils/colors.dart'; + +class ScanScreen extends ConsumerStatefulWidget { + const ScanScreen({Key? key}) : super(key: key); + + @override + _ScanScreenState createState() => _ScanScreenState(); +} + +class _ScanScreenState extends ConsumerState { + MobileScannerController cameraController = MobileScannerController(); + +/* // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + }*/ + + bool cameraOnPause = false; + double _zoomFactor = 0.0; + final double _scaleSensitivity = 0.05; + + @override + void dispose() { + logger.i('ScanScreen disposed'); + cameraController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Builder( + builder: (context) { + return Stack( + children: [ + MobileScanner( + onScannerStarted: (args) { + logger.i('Scanner onScannerStarted'); + }, + controller: cameraController, + onDetect: onDetect, + ), + // Container( + // decoration: ShapeDecoration( + // shape: QrScannerOverlayShape( + // borderColor: lightBlue, + // borderRadius: 3, + // borderWidth: 10, + // cutOutSize: MediaQuery.of(context).size.width * (3 / 4), + // ), + // ), + // ), + Positioned( + top: MediaQuery.of(context).padding.top + 8, + right: 16, + child: IconButton( + icon: CircleAvatar( + child: Icon(Icons.clear), + backgroundColor: Colors.white, + ), + color: Colors.black, + onPressed: () { + Navigator.of(context).pop(); + }), + ), + GestureDetector( + onScaleUpdate: (details) { + _zoomFactor += _scaleSensitivity * (details.scale - 1); + _zoomFactor = _zoomFactor.clamp(0.0, 1.0); + setState(() {}); + cameraController.setZoomScale(_zoomFactor); + }, + ), + ScanInfo( + onUpdate: (_cameraOnPause) { + setState(() { + cameraOnPause = _cameraOnPause; + }); + }, + ), + ], + ); + }, + ), + ); + } + + void onDetect(BarcodeCapture barcode) { + logger.i('Scanner onDetect new'); + + if (!cameraOnPause) { + if (barcode.barcodes.isNotEmpty) { + logger.i('scanner detects ${barcode.barcodes.length} QR'); + setState(() { + cameraOnPause = true; + }); + final res = ref + .read(scannerNotifierProvider.notifier) + .onProcessing(barcode.barcodes); + if (!res) { + setState(() { + cameraOnPause = false; + }); + } + } + } + } + + // old + onDetectOld(barcode) { + logger.i('Scanner onDetect'); + // if (scanned) return; + // scanned = true; + final qr = barcode.barcodes.first.rawValue; + cameraController.stop(); + Navigator.of(context).pop(qr); + } +} + +class TopMessage extends StatelessWidget { + final String text; + + const TopMessage({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + text, + textAlign: TextAlign.center, + ), + ); + } +} + +class ScanInfo extends ConsumerWidget { + final Function(bool) onUpdate; + + const ScanInfo({Key? key, required this.onUpdate}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24.0, 64, 24, 24), + child: Consumer( + builder: (context, ref, child) { + final state = ref.watch(scannerNotifierProvider); + return switch (state) { + ScannerStateEmpty() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: TopMessage( + text: 'scanStateEmpty'.tr(), + ), + ), + Spacer(), + Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Chip( + label: Text( + 'scanning'.tr(), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ScannerStateSingle() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: TopMessage( + text: 'scanStateSingle'.tr(), + ), + ), + Spacer(), + Center( + child: FloatingActionButton.extended( + backgroundColor: primaryColor, + label: Text( + getText(state.type), + style: TextStyle( + color: Colors.white, + ), + ), + icon: const Icon( + Icons.camera_enhance, + color: Colors.white, + ), + onPressed: () { + Navigator.of(context).pop(state.url); + }, + ), + ), + ], + ), + ScannerStateMultiple() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: TopMessage( + text: 'scanStateMultiple'.tr(), + ), + ), + Spacer(), + Center( + child: FloatingActionButton.extended( + backgroundColor: primaryColor, + label: Text( + 'scanContinueToScan'.tr(), + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + icon: const Icon( + Icons.camera_enhance, + color: Colors.white, + ), + onPressed: () { + ref.read(scannerNotifierProvider.notifier).reset(); + Future.delayed(Duration(milliseconds: 250), () { + onUpdate(false); + }); + }, + ), + ), + ], + ), + _ => Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Chip( + label: Text( + 'scanning'.tr(), + textAlign: TextAlign.center, + ), + ), + ), + ), + }; + }, + ), + ), + ); + } + + getText(TransactionType type) { + return switch (type) { + TransactionType.VOUCHERS => 'scanGetWom'.tr(), + TransactionType.PAYMENT => 'scanPay'.tr(), + TransactionType.MIGRATION_IMPORT => 'scanImportMigration'.tr(), + TransactionType.EXCHANGE_IMPORT => 'scanExchangeImport'.tr(), + _ => 'scanGetWom'.tr() + }; + } +} diff --git a/lib/src/screens/home/home_screen.dart b/lib/src/screens/home/home_screen.dart index b218f12..ac0337b 100644 --- a/lib/src/screens/home/home_screen.dart +++ b/lib/src/screens/home/home_screen.dart @@ -17,8 +17,9 @@ import 'package:wom_pocket/src/migration/application/import_notifier.dart'; import 'package:wom_pocket/src/migration/ui/import_screen.dart'; import 'package:wom_pocket/src/models/totem_data.dart'; import 'package:wom_pocket/src/new_home/ui/new_home.dart'; +import 'package:wom_pocket/src/nfc/utils.dart'; import 'package:wom_pocket/src/offers/ui/offers_screen.dart'; -import 'package:wom_pocket/src/screens/home/scan_screen.dart'; +import 'package:wom_pocket/src/scanner/ui/scan_screen.dart'; import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; import 'package:wom_pocket/src/services/app_repository.dart'; import 'package:rflutter_alert/rflutter_alert.dart'; @@ -302,18 +303,7 @@ class _HomeScreen2State extends ConsumerState { if (link == null) return; final totemData = validateTotemQrCodeWithRegex(link); if (totemData != null) { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => PopScope( - canPop: false, - child: Dialog( - child: TotemDialog( - totemData: totemData, - ), - ), - ), - ); + launchTotemDialog(context, totemData); } else { final deepLinkModel = DeepLinkModel.fromUri(Uri.parse(link)); logger.i('wom_scan_done $link'); diff --git a/lib/src/screens/home/scan_screen.dart b/lib/src/screens/home/scan_screen.dart deleted file mode 100644 index 0576c6d..0000000 --- a/lib/src/screens/home/scan_screen.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:mobile_scanner/mobile_scanner.dart'; - -import 'package:wom_pocket/src/widgets/scanner_overlay_shape.dart'; -import '../../my_logger.dart'; -import '../../utils/colors.dart'; - -class ScanScreen extends StatefulWidget { - const ScanScreen({Key? key}) : super(key: key); - - @override - _ScanScreenState createState() => _ScanScreenState(); -} - -class _ScanScreenState extends State { - Barcode? result; - MobileScannerController cameraController = MobileScannerController(); - -/* // In order to get hot reload to work we need to pause the camera if the platform - // is android, or resume the camera if the platform is iOS. - @override - void reassemble() { - super.reassemble(); - if (Platform.isAndroid) { - controller!.pauseCamera(); - } else if (Platform.isIOS) { - controller!.resumeCamera(); - } - }*/ - - bool scanned = false; - double _zoomFactor = 0.0; - final double _scaleSensitivity = 0.05; - - @override - void dispose() { - logger.i('ScanScreen disposed'); - cameraController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold(body: Builder(builder: (context) { - return Stack( - children: [ - MobileScanner( - onScannerStarted: (args) { - logger.i('Scanner onScannerStarted'); - }, - controller: cameraController, - onDetect: (barcode) { - logger.i('Scanner onDetect'); - // if (scanned) return; - // scanned = true; - final qr = barcode.barcodes.first.rawValue; - cameraController.stop(); - Navigator.of(context).pop(qr); - }, - ), - Container( - decoration: ShapeDecoration( - shape: QrScannerOverlayShape( - borderColor: lightBlue, - borderRadius: 3, - borderWidth: 10, - cutOutSize: MediaQuery.of(context).size.width * (3 / 4), - ), - ), - ), - Positioned( - top: MediaQuery.of(context).padding.top + 8, - right: 16, - child: IconButton( - icon: CircleAvatar( - child: Icon(Icons.clear), - backgroundColor: Colors.white, - ), - color: Colors.black, - onPressed: () { - Navigator.of(context).pop(); - }), - ), - /* Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Slider( - max: 1, - divisions: 100, - value: _zoomFactor, - label: "${(_zoomFactor * 100).toStringAsFixed(0)} %", - onChanged: (value) { - print(value); - setState(() { - _zoomFactor = value; - cameraController.setZoomScale(value); - }); - }, - ), - ], - ),*/ - GestureDetector( - onScaleUpdate: (details) { - _zoomFactor += _scaleSensitivity * (details.scale - 1); - _zoomFactor = _zoomFactor.clamp(0.0, 1.0); - setState(() {}); - cameraController.setZoomScale(_zoomFactor); - }, - ) - ], - ); - })); - } - -/* void _onQRViewCreated(QRViewController controller) { - this.controller = controller; - controller.scannedDataStream.listen((scanData) { - if (scanned) return; - - scanned = true; - print(scanData.code); - Navigator.of(context).pop(scanData.code); - }); - }*/ -} diff --git a/lib/src/screens/home/widgets/totem_dialog.dart b/lib/src/screens/home/widgets/totem_dialog.dart index ff7eb26..15e3c5b 100644 --- a/lib/src/screens/home/widgets/totem_dialog.dart +++ b/lib/src/screens/home/widgets/totem_dialog.dart @@ -257,97 +257,99 @@ class TotemDialog extends ConsumerWidget { askGender: askGender, )); final size = MediaQuery.sizeOf(context); - return Container( - padding: EdgeInsets.all(8), - constraints: BoxConstraints( - maxWidth: size.width * 0.8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state is TotemDialogGenderRequest) ...[ - GenderSelectorWidget( - onAction: () { - ref - .read( - totemNotifierProvider( - totemData, - askGender: askGender, - ).notifier, - ) - .action(); - }, - ), - ] else if (state is TotemDialogStateError) ...[ - Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 8), - Text(state.totemError.description(context)), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (state.totemError.hasCancel) - TextButton( + return Dialog( + child: Container( + padding: EdgeInsets.all(8), + constraints: BoxConstraints( + maxWidth: size.width * 0.8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (state is TotemDialogGenderRequest) ...[ + GenderSelectorWidget( + onAction: () { + ref + .read( + totemNotifierProvider( + totemData, + askGender: askGender, + ).notifier, + ) + .action(); + }, + ), + ] else if (state is TotemDialogStateError) ...[ + Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 8), + Text(state.totemError.description(context)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (state.totemError.hasCancel) + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('cancel'.tr()), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), onPressed: () { - Navigator.of(context).pop(); + switch (state.totemError) { + case TotemError.sessionNotStarted: + case TotemError.wrongRequestId: + case TotemError.outOfPolygon: + case TotemError.sessionAlreadyScanned: + case TotemError.sessionExpired: + case TotemError.eventIsClosed: + case TotemError.totemDisabled: + case TotemError.noWomForThisEvent: + case TotemError.totemSessionInactive: + case TotemError.mockedLocation: + Navigator.of(context).pop(); + break; + case TotemError.gpsServiceDisabled: + Geolocator.openLocationSettings(); + break; + case TotemError.gpsPermission: + case TotemError.gpsTimeout: + case TotemError.unknown: + ref + .read(totemNotifierProvider( + totemData, + askGender: askGender, + ).notifier) + .action(); + } }, - child: Text('cancel'.tr()), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - onPressed: () { - switch (state.totemError) { - case TotemError.sessionNotStarted: - case TotemError.wrongRequestId: - case TotemError.outOfPolygon: - case TotemError.sessionAlreadyScanned: - case TotemError.sessionExpired: - case TotemError.eventIsClosed: - case TotemError.totemDisabled: - case TotemError.noWomForThisEvent: - case TotemError.totemSessionInactive: - case TotemError.mockedLocation: - Navigator.of(context).pop(); - break; - case TotemError.gpsServiceDisabled: - Geolocator.openLocationSettings(); - break; - case TotemError.gpsPermission: - case TotemError.gpsTimeout: - case TotemError.unknown: - ref - .read(totemNotifierProvider( - totemData, - askGender: askGender, - ).notifier) - .action(); - } - }, - child: Text(state.totemError.errorActionText(context)), - ) - ], - ), - ] else ...[ - CircularProgressIndicator(), - const SizedBox(height: 8), - switch (state) { - TotemDialogRetrievingGPS() => Text('acquiringYourPosition'.tr()), - TotemDialogCommunicationWithServer() => - Text('communicatingWithServer'.tr()), - TotemDialogComplete() => Text('completed'.tr()), - _ => SizedBox.shrink(), - }, + child: Text(state.totemError.errorActionText(context)), + ) + ], + ), + ] else ...[ + CircularProgressIndicator(), + const SizedBox(height: 8), + switch (state) { + TotemDialogRetrievingGPS() => Text('acquiringYourPosition'.tr()), + TotemDialogCommunicationWithServer() => + Text('communicatingWithServer'.tr()), + TotemDialogComplete() => Text('completed'.tr()), + _ => SizedBox.shrink(), + }, + ], ], - ], + ), ), ); } diff --git a/lib/src/screens/intro/intro.dart b/lib/src/screens/intro/intro.dart index 8dd74f2..b79c348 100644 --- a/lib/src/screens/intro/intro.dart +++ b/lib/src/screens/intro/intro.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:auto_size_text/auto_size_text.dart'; import 'package:dots_indicator/dots_indicator.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -10,7 +12,8 @@ import 'package:wom_pocket/constants.dart'; import 'package:wom_pocket/src/application/app_notifier.dart'; import 'package:wom_pocket/src/models/totem_data.dart'; -import 'package:wom_pocket/src/screens/home/widgets/totem_dialog.dart'; +import 'package:wom_pocket/src/new_home/application/wom_stats_notifier.dart'; +import 'package:wom_pocket/src/nfc/utils.dart'; import 'package:wom_pocket/src/utils/colors.dart'; class IntroScreen extends HookConsumerWidget { @@ -22,11 +25,12 @@ class IntroScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final transactionCount = + ref.watch(transactionCountNotifierProvider).valueOrNull ?? 0; final selectedPage = useState(0); final pages = [ IntroPage( backGroundColor: lightBackground, - //TODO mettere in bold W O M di Worth One Minute message: 'introDesc1'.tr(), title: 'introTitle1'.tr(), child: Padding( @@ -100,42 +104,33 @@ class IntroScreen extends HookConsumerWidget { size: 200, ), ), - IntroPage( - textColor: Colors.white, - backGroundColor: darkBackground, - message: 'introDesc7'.tr(), - title: 'introTitle7'.tr(), - bottomButton: ElevatedButton( - onPressed: fromSettings - ? null - : () { - final totemData = validateTotemQrCodeWithRegex( - 'https://link.wom.social/cmi/e3441c34-b02c-4bd9-8de5-9e312468ca69/d67c6e3a-053a-4cb7-b4ce-d1d0427c6cad'); - if (totemData != null) { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => PopScope( - canPop: true, - child: Dialog( - child: TotemDialog( - totemData: totemData, - askGender: false, - ), - ), - ), - ); - } - }, - child: Text('Riscatta subito'), - ), - child: Image.asset( - 'assets/images/wom.png', - height: 285.0, - width: 285.0, - alignment: Alignment.topCenter, + if (transactionCount == 0) + IntroPage( + textColor: Colors.white, + backGroundColor: darkBackground, + message: 'introDesc7'.tr(), + title: 'introTitle7'.tr(), + bottomButton: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: accentColor), + onPressed: () { + final totemData = validateTotemQrCodeWithRegex( + 'https://link.wom.social/cmi/e3441c34-b02c-4bd9-8de5-9e312468ca69/d67c6e3a-053a-4cb7-b4ce-d1d0427c6cad'); + if (totemData != null) { + launchTotemDialog(context, totemData); + } + }, + child: Text( + 'introAction7'.tr(), + style: TextStyle(color: primaryColor), + ), + ), + child: Image.asset( + 'assets/images/wom.png', + height: 285.0, + width: 285.0, + alignment: Alignment.topCenter, + ), ), - ), ]; return Stack( @@ -158,7 +153,7 @@ class IntroScreen extends HookConsumerWidget { builder: (context, ref, child) { return DotsIndicator( decorator: DotsDecorator(color: lightBlue), - position: selectedPage.value.toInt(), + position: min(selectedPage.value.toInt(), pages.length - 1), dotsCount: pages.length, ); }, @@ -170,7 +165,7 @@ class IntroScreen extends HookConsumerWidget { right: 0, child: SizedBox( height: 80, - child: selectedPage.value == pages.length - 1 + child: min(selectedPage.value, pages.length - 1) == pages.length - 1 ? TextButton( onPressed: () { if (fromSettings) { @@ -182,9 +177,11 @@ class IntroScreen extends HookConsumerWidget { } }, child: Text( - 'Ok', + 'done'.tr(), style: TextStyle( - color: Colors.white, + color: transactionCount > 0 + ? Theme.of(context).primaryColor + : Colors.white, fontSize: 20, ), ), diff --git a/lib/src/screens/settings/settings.dart b/lib/src/screens/settings/settings.dart index 8a157b6..af2c31c 100644 --- a/lib/src/screens/settings/settings.dart +++ b/lib/src/screens/settings/settings.dart @@ -8,6 +8,7 @@ import 'package:package_info/package_info.dart'; import 'package:rflutter_alert/rflutter_alert.dart'; import 'package:wom_pocket/src/application/aim_notifier.dart'; import 'package:wom_pocket/src/log_output.dart'; +import 'package:wom_pocket/src/nfc/ui/nfc_session_dialog.dart'; import 'package:wom_pocket/src/screens/home/widgets/wom_stats_widget.dart'; import 'package:wom_pocket/src/screens/intro/intro.dart'; import 'package:wom_pocket/src/screens/table_page/db_page.dart'; @@ -186,14 +187,6 @@ class _SettingsScreenState extends ConsumerState { ); }, ), - // SettingsItem( - // title: 'enableHomeTutorialTitle'.tr(), - // subtitle: 'enableHomeTutorialDesc'.tr(), - // icon: Icons.cast_for_education, - // onTap: () { - // _clearTutorial(context); - // }, - // ), SettingsItem( title: 'settings_info_title'.tr(), subtitle: 'settings_info_desc'.tr(), diff --git a/pubspec.yaml b/pubspec.yaml index e46f6db..5f81238 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: wom_pocket description: A new Flutter application. publish_to: none -version: 1.5.0+77 +version: 1.6.1+82 environment: sdk: '>=3.2.0 <4.0.0' @@ -76,6 +76,9 @@ dependencies: freezed_annotation: dio: easy_localization: ^3.0.3 + nfc_manager: + nfc_background: + path: /Users/gianmarcodifrancesco/Work/Digit/nfc_background # UI material_design_icons_flutter: ^7.0.7296