├── .envrc ├── metadata ├── build_number.txt ├── title.txt ├── release_notes.txt ├── short_description.txt └── full_description.txt ├── android ├── Gemfile ├── fastlane │ ├── metadata │ │ └── android │ │ │ └── en-US │ │ │ ├── changelogs │ │ │ ├── 92.txt │ │ │ ├── 93.txt │ │ │ ├── 94.txt │ │ │ ├── 95.txt │ │ │ ├── 96.txt │ │ │ ├── 31.txt │ │ │ ├── 33.txt │ │ │ ├── 34.txt │ │ │ ├── 35.txt │ │ │ ├── 90.txt │ │ │ ├── 91.txt │ │ │ ├── 36.txt │ │ │ ├── 37.txt │ │ │ ├── 39.txt │ │ │ ├── 40.txt │ │ │ ├── 41.txt │ │ │ ├── 42.txt │ │ │ ├── 43.txt │ │ │ ├── 44.txt │ │ │ ├── 45.txt │ │ │ ├── 46.txt │ │ │ ├── 47.txt │ │ │ ├── 48.txt │ │ │ ├── 49.txt │ │ │ ├── 50.txt │ │ │ ├── 51.txt │ │ │ ├── 52.txt │ │ │ ├── 53.txt │ │ │ ├── 54.txt │ │ │ ├── 55.txt │ │ │ ├── 56.txt │ │ │ ├── 57.txt │ │ │ ├── 58.txt │ │ │ ├── 59.txt │ │ │ ├── 60.txt │ │ │ ├── 61.txt │ │ │ ├── 62.txt │ │ │ ├── 63.txt │ │ │ ├── 64.txt │ │ │ ├── 65.txt │ │ │ ├── 66.txt │ │ │ ├── 67.txt │ │ │ ├── 68.txt │ │ │ ├── 69.txt │ │ │ ├── 70.txt │ │ │ ├── 71.txt │ │ │ ├── 72.txt │ │ │ ├── 73.txt │ │ │ ├── 74.txt │ │ │ ├── 75.txt │ │ │ ├── 76.txt │ │ │ ├── 77.txt │ │ │ ├── 78.txt │ │ │ ├── 79.txt │ │ │ ├── 80.txt │ │ │ ├── 81.txt │ │ │ ├── 87.txt │ │ │ ├── 88.txt │ │ │ └── 89.txt │ │ │ ├── title.txt │ │ │ ├── full_description.txt │ │ │ ├── short_description.txt │ │ │ └── images │ │ │ ├── icon.jpg │ │ │ └── phoneScreenshots │ │ │ ├── Pixel_6_01.png │ │ │ ├── Pixel_6_02.png │ │ │ ├── Pixel_6_03.png │ │ │ └── Pixel_6_04.png │ ├── Appfile │ ├── README.md │ └── Fastfile ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── co │ │ │ │ │ └── timsmart │ │ │ │ │ └── vouchervault │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore └── Podfile ├── lib ├── voucher_form │ ├── barcode_scanner │ │ ├── providers │ │ │ ├── index.dart │ │ │ └── camera.dart │ │ ├── lib │ │ │ ├── index.dart │ │ │ └── camera_utils.dart │ │ ├── models │ │ │ ├── index.dart │ │ │ ├── barcode_result.dart │ │ │ ├── ml_context.dart │ │ │ └── ml_error.dart │ │ ├── widgets │ │ │ ├── index.dart │ │ │ ├── barcode_button.g.dart │ │ │ ├── barcode_scanner_field.g.dart │ │ │ ├── scanner_dialog.g.dart │ │ │ ├── barcode_button.dart │ │ │ ├── barcode_scanner_field.dart │ │ │ └── scanner_dialog.dart │ │ ├── index.dart │ │ └── service.dart │ ├── widgets │ │ ├── index.dart │ │ ├── dialog.g.dart │ │ ├── form.g.dart │ │ └── dialog.dart │ └── index.dart ├── vouchers │ ├── models │ │ ├── index.dart │ │ ├── state.dart │ │ ├── voucher.g.dart │ │ └── voucher.dart │ ├── menu │ │ ├── index.dart │ │ ├── vouchers_menu_container.g.dart │ │ ├── vouchers_menu.g.dart │ │ ├── vouchers_menu_container.dart │ │ └── vouchers_menu.dart │ ├── list │ │ ├── index.dart │ │ ├── vouchers_list_container.g.dart │ │ ├── voucher_list.g.dart │ │ ├── voucher_item.g.dart │ │ ├── voucher_list.dart │ │ ├── vouchers_list_container.dart │ │ └── voucher_item.dart │ ├── dialog │ │ ├── index.dart │ │ ├── voucher_spend_dialog.g.dart │ │ ├── voucher_dialog_container.g.dart │ │ ├── voucher_spend_dialog.dart │ │ ├── voucher_dialog.g.dart │ │ └── voucher_dialog_container.dart │ ├── index.dart │ ├── vouchers_screen.g.dart │ ├── vouchers_screen.dart │ └── service.dart ├── auth │ ├── index.dart │ ├── auth_screen.g.dart │ ├── auth_screen.dart │ ├── model.g.dart │ ├── model.dart │ └── service.dart ├── shared │ ├── scaffold │ │ ├── scaffold.dart │ │ ├── app_scaffold_simple.dart │ │ ├── app_scaffold_simple.g.dart │ │ ├── app_scaffold.g.dart │ │ └── app_scaffold.dart │ └── voucher_details │ │ ├── voucher_details.g.dart │ │ └── voucher_details.dart ├── hooks │ ├── index.dart │ ├── use_system_overlay_style.dart │ ├── use_full_brightness.dart │ └── use_route_observer.dart ├── app │ ├── colors.dart │ ├── index.dart │ ├── settings.dart │ ├── atoms.dart │ ├── settings.g.dart │ ├── voucher_vault_app.g.dart │ ├── theme.dart │ └── voucher_vault_app.dart ├── lib │ ├── lib.dart │ ├── datetime.dart │ ├── intersperse.dart │ ├── option.dart │ ├── milliunits.dart │ ├── barcode.dart │ └── files.dart ├── l10n │ ├── app_en.arb │ └── app_fr.arb └── main.dart ├── assets ├── icon │ ├── icon.png │ ├── icon_bg.png │ └── icon_fg.png └── fonts │ ├── AlegreyaSans-Black.ttf │ ├── AlegreyaSans-Bold.ttf │ ├── AlegreyaSans-Italic.ttf │ ├── AlegreyaSans-Regular.ttf │ ├── AlegreyaSans-BoldItalic.ttf │ ├── AlegreyaSans-ExtraBold.ttf │ ├── AlegreyaSans-BlackItalic.ttf │ └── AlegreyaSans-ExtraBoldItalic.ttf ├── l10n.yaml ├── .vscode ├── settings.json └── launch.json ├── .sops.yaml ├── README.md ├── analysis_options.yaml ├── test └── widget_test.dart ├── tools └── screenshots.dart ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── .gitignore ├── .metadata ├── Makefile ├── flake.nix ├── nix └── flutter.nix ├── flake.lock ├── test_driver ├── main.dart └── main_test.dart ├── pubspec.yaml ├── PRIVACY.md └── secrets └── fastlane-google-key /.envrc: -------------------------------------------------------------------------------- 1 | use flake; -------------------------------------------------------------------------------- /metadata/build_number.txt: -------------------------------------------------------------------------------- 1 | 96 2 | -------------------------------------------------------------------------------- /metadata/title.txt: -------------------------------------------------------------------------------- 1 | Voucher Vault 2 | -------------------------------------------------------------------------------- /metadata/release_notes.txt: -------------------------------------------------------------------------------- 1 | Refreshed look and feel! -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/92.txt: -------------------------------------------------------------------------------- 1 | Update look and feel. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/93.txt: -------------------------------------------------------------------------------- 1 | Update look and feel. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/94.txt: -------------------------------------------------------------------------------- 1 | Update look and feel. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/95.txt: -------------------------------------------------------------------------------- 1 | Update look and feel. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/96.txt: -------------------------------------------------------------------------------- 1 | Update look and feel. -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/31.txt: -------------------------------------------------------------------------------- 1 | Released on Google Play! -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/33.txt: -------------------------------------------------------------------------------- 1 | Released on Google Play! -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/34.txt: -------------------------------------------------------------------------------- 1 | Released on Google Play! -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/35.txt: -------------------------------------------------------------------------------- 1 | Released on Google Play! -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | ../../../../../metadata/title.txt -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/providers/index.dart: -------------------------------------------------------------------------------- 1 | export 'camera.dart'; 2 | -------------------------------------------------------------------------------- /lib/vouchers/models/index.dart: -------------------------------------------------------------------------------- 1 | export 'state.dart'; 2 | export 'voucher.dart'; 3 | -------------------------------------------------------------------------------- /metadata/short_description.txt: -------------------------------------------------------------------------------- 1 | A vault for all your vouchers and gift cards. 2 | -------------------------------------------------------------------------------- /lib/voucher_form/widgets/index.dart: -------------------------------------------------------------------------------- 1 | export 'dialog.dart'; 2 | export 'form.dart'; 3 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/90.txt: -------------------------------------------------------------------------------- 1 | Bug fixes and performance improvements. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/91.txt: -------------------------------------------------------------------------------- 1 | Bug fixes and performance improvements. -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/icon/icon.png -------------------------------------------------------------------------------- /lib/auth/index.dart: -------------------------------------------------------------------------------- 1 | export 'auth_screen.dart'; 2 | export 'model.dart'; 3 | export 'service.dart'; 4 | -------------------------------------------------------------------------------- /lib/voucher_form/index.dart: -------------------------------------------------------------------------------- 1 | export 'barcode_scanner/index.dart'; 2 | export 'widgets/index.dart'; 3 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | ../../../../../metadata/full_description.txt -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | ../../../../../metadata/short_description.txt -------------------------------------------------------------------------------- /assets/icon/icon_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/icon/icon_bg.png -------------------------------------------------------------------------------- /assets/icon/icon_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/icon/icon_fg.png -------------------------------------------------------------------------------- /lib/shared/scaffold/scaffold.dart: -------------------------------------------------------------------------------- 1 | export 'app_scaffold.dart'; 2 | export 'app_scaffold_simple.dart'; 3 | -------------------------------------------------------------------------------- /lib/vouchers/menu/index.dart: -------------------------------------------------------------------------------- 1 | export 'vouchers_menu.dart'; 2 | export 'vouchers_menu_container.dart'; 3 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/lib/index.dart: -------------------------------------------------------------------------------- 1 | export 'camera_utils.dart'; 2 | export 'extraction.dart'; 3 | -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-ExtraBold.ttf -------------------------------------------------------------------------------- /lib/vouchers/list/index.dart: -------------------------------------------------------------------------------- 1 | export 'voucher_item.dart'; 2 | export 'voucher_list.dart'; 3 | export 'vouchers_list_container.dart'; 4 | -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-BlackItalic.ttf -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/hooks/index.dart: -------------------------------------------------------------------------------- 1 | export 'use_full_brightness.dart'; 2 | export 'use_route_observer.dart'; 3 | export 'use_system_overlay_style.dart'; 4 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/models/index.dart: -------------------------------------------------------------------------------- 1 | export 'barcode_result.dart'; 2 | export 'ml_context.dart'; 3 | export 'ml_error.dart'; 4 | -------------------------------------------------------------------------------- /assets/fonts/AlegreyaSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/assets/fonts/AlegreyaSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/app/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const lightGrey = Color(0xFFE5E5E5); 5 | } 6 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/index.dart: -------------------------------------------------------------------------------- 1 | export 'voucher_dialog.dart'; 2 | export 'voucher_dialog_container.dart'; 3 | export 'voucher_spend_dialog.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/app/index.dart: -------------------------------------------------------------------------------- 1 | export 'atoms.dart'; 2 | export 'colors.dart'; 3 | export 'settings.dart'; 4 | export 'theme.dart'; 5 | export 'voucher_vault_app.dart'; 6 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/index.dart: -------------------------------------------------------------------------------- 1 | export 'barcode_button.dart'; 2 | export 'barcode_scanner_field.dart'; 3 | export 'scanner_dialog.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/fastlane/metadata/android/en-US/images/icon.jpg -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/36.txt: -------------------------------------------------------------------------------- 1 | - Fixed bug with voucher form 2 | - Open scanner immediately 3 | - Added smart scanning features (english only) 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/37.txt: -------------------------------------------------------------------------------- 1 | - Fixed bug with voucher form 2 | - Open scanner immediately 3 | - Added smart scanning features (english only) 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/39.txt: -------------------------------------------------------------------------------- 1 | - Fixed bug with voucher form 2 | - Open scanner immediately 3 | - Added smart scanning features (english only) 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.freezed.dart": true, 4 | "**/*.g.dart": true 5 | }, 6 | "dart.flutterSdkPath": ".flutter/flutter" 7 | } 8 | -------------------------------------------------------------------------------- /lib/lib/lib.dart: -------------------------------------------------------------------------------- 1 | export 'barcode.dart'; 2 | export 'datetime.dart'; 3 | export 'files.dart'; 4 | export 'intersperse.dart'; 5 | export 'milliunits.dart'; 6 | export 'option.dart'; 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/40.txt: -------------------------------------------------------------------------------- 1 | - Immediately open scanner when adding a new voucher 2 | - Added smart scanning features (english only) 3 | - Fixed some bugs 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/41.txt: -------------------------------------------------------------------------------- 1 | - Immediately open scanner when adding a new voucher 2 | - Added smart scanning features (english only) 3 | - Fixed some bugs 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/42.txt: -------------------------------------------------------------------------------- 1 | - Immediately open scanner when adding a new voucher 2 | - Added smart scanning features (english only) 3 | - Fixed some bugs 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/43.txt: -------------------------------------------------------------------------------- 1 | - Immediately open scanner when adding a new voucher 2 | - Added smart scanning features (english only) 3 | - Fixed some bugs 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/44.txt: -------------------------------------------------------------------------------- 1 | - Immediately open scanner when adding a new voucher 2 | - Added smart scanning features (english only) 3 | - Fixed some bugs 4 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/index.dart: -------------------------------------------------------------------------------- 1 | export 'lib/index.dart'; 2 | export 'models/index.dart'; 3 | export 'providers/index.dart'; 4 | export 'service.dart'; 5 | export 'widgets/index.dart'; 6 | -------------------------------------------------------------------------------- /lib/lib/datetime.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_date/dart_date.dart'; 2 | 3 | String formatExpires(DateTime dt) { 4 | dt = dt.endOfDay; 5 | return dt.isPast ? 'Expired' : dt.timeago(allowFromNow: true); 6 | } 7 | -------------------------------------------------------------------------------- /lib/vouchers/index.dart: -------------------------------------------------------------------------------- 1 | export 'dialog/index.dart'; 2 | export 'list/index.dart'; 3 | export 'menu/index.dart'; 4 | export 'models/index.dart'; 5 | export 'service.dart'; 6 | export 'vouchers_screen.dart'; 7 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_01.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_02.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_03.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-smart/vouchervault/HEAD/android/fastlane/metadata/android/en-US/images/phoneScreenshots/Pixel_6_04.png -------------------------------------------------------------------------------- /.sops.yaml: -------------------------------------------------------------------------------- 1 | keys: 2 | - &admin_tim age1cqrnyaj8tcu6svafggn5f45juq5lnvghqlqsazvp5pvzt5wa2els9ak7pv 3 | creation_rules: 4 | - path_regex: secrets/[^/]+$ 5 | key_groups: 6 | - age: 7 | - *admin_tim 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/co/timsmart/vouchervault/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package co.timsmart.vouchervault 2 | 3 | import io.flutter.embedding.android.FlutterFragmentActivity 4 | 5 | class MainActivity: FlutterFragmentActivity() 6 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("./keys/google.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("co.timsmart.vouchervault") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vouchervault 2 | 3 | Get it on Google Play 4 | 5 | A vault for all your vouchers and gift cards. 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /metadata/full_description.txt: -------------------------------------------------------------------------------- 1 | Voucher Vault helps you keep track of all your vouchers, gift cards and 2 | loyalty cards. 3 | 4 | Features include: 5 | 6 | * Supports several types of barcodes, including plain text 7 | * Keep track of expiry dates 8 | * Keep track of gift card balances 9 | * Assign each voucher a different color 10 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | errors: 5 | no_wildcard_variable_uses: ignore 6 | exclude: 7 | - lib/**/*.freezed.dart 8 | - lib/**/*.g.dart 9 | 10 | linter: 11 | rules: 12 | library_prefixes: false 13 | prefer_function_declarations_over_variables: false 14 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | keys/ 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /lib/lib/intersperse.dart: -------------------------------------------------------------------------------- 1 | Iterable Function(Iterable) intersperse(T element) => 2 | (iterable) sync* { 3 | final iterator = iterable.iterator; 4 | if (iterator.moveNext()) { 5 | yield iterator.current; 6 | while (iterator.moveNext()) { 7 | yield element; 8 | yield iterator.current; 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/45.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/46.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/47.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/48.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/49.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/50.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/51.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/52.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/53.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/54.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/55.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/56.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/57.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/58.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/59.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/60.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/61.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/62.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/63.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/64.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/65.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/66.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/67.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/68.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/69.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/72.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/73.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/74.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/75.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/76.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/77.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/78.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/79.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/80.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/81.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/87.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/88.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/89.txt: -------------------------------------------------------------------------------- 1 | Smart scanning! 2 | 3 | In addition to scanning barcodes, the scanner also tries to extract the voucher's description, expiry date and balance. 4 | At this stage it only supports the English language. 5 | 6 | You can toggle this setting in the menu. 7 | 8 | Other changes: 9 | - Immediately open scanner when adding a new voucher 10 | - Fixed some bugs 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'settings.freezed.dart'; 4 | part 'settings.g.dart'; 5 | 6 | @freezed 7 | class VoucherVaultSettings with _$VoucherVaultSettings { 8 | const factory VoucherVaultSettings({ 9 | @Default(true) bool smartScan, 10 | }) = _VoucherVaultSettings; 11 | 12 | factory VoucherVaultSettings.fromJson(dynamic json) => 13 | _$VoucherVaultSettingsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | // import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() {} 11 | -------------------------------------------------------------------------------- /lib/auth/auth_screen.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_screen.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class AuthScreen extends HookWidget { 10 | const AuthScreen({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext _context) => authScreen(_context); 14 | } 15 | -------------------------------------------------------------------------------- /tools/screenshots.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:emulators/emulators.dart'; 4 | 5 | Future main() async { 6 | final emu = await Emulators.build(); 7 | 8 | // Shutdown all the running emulators 9 | await emu.shutdownAll(); 10 | 11 | await emu.forEach(['Pixel_6'])((device) async { 12 | final p = await emu.drive( 13 | device, 14 | 'test_driver/main.dart', 15 | ); 16 | stderr.addStream(p.stderr); 17 | await stdout.addStream(p.stdout); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/app/atoms.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:vouchervault/app/index.dart'; 3 | 4 | final nucleusStorage = atom( 5 | (get) => get(runtimeAtom).runSyncOrThrow(storageLayer.access), 6 | ); 7 | 8 | final appSettings = stateAtomWithStorage( 9 | const VoucherVaultSettings(), 10 | key: 'rp_persist_settingsProvider', 11 | storage: nucleusStorage, 12 | fromJson: VoucherVaultSettings.fromJson, 13 | toJson: (s) => s.toJson(), 14 | ); 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/vouchers/vouchers_screen.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'vouchers_screen.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VouchersScreen extends StatelessWidget { 10 | const VouchersScreen({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext _context) => _vouchersScreen(_context); 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/voucher_spend_dialog.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_spend_dialog.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherSpendDialog extends HookWidget { 10 | const VoucherSpendDialog({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext _context) => _voucherSpendDialog(_context); 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "vouchervault", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "vouchervault profile", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /lib/vouchers/menu/vouchers_menu_container.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'vouchers_menu_container.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VouchersMenuContainer extends StatelessWidget { 10 | const VouchersMenuContainer({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext _context) => vouchersMenuContainer(); 14 | } 15 | -------------------------------------------------------------------------------- /lib/vouchers/list/vouchers_list_container.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'vouchers_list_container.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VouchersListContainer extends StatelessWidget { 10 | const VouchersListContainer({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext _context) => _vouchersListContainer(_context); 14 | } 15 | -------------------------------------------------------------------------------- /lib/shared/scaffold/app_scaffold_simple.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:vouchervault/hooks/index.dart'; 5 | 6 | part 'app_scaffold_simple.g.dart'; 7 | 8 | @hwidget 9 | Widget appScaffoldSimple( 10 | BuildContext context, { 11 | required Widget body, 12 | }) { 13 | final style = useSystemOverlayStyle(); 14 | return AnnotatedRegion( 15 | value: style, 16 | child: Scaffold(body: body), 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/lib/option.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | 3 | Option optionOfString(String? s) => 4 | Option.fromNullable(s).map((s) => s.trim()).filter((s) => s.isNotEmpty); 5 | 6 | final maybeParseInt = optionOfString 7 | .c((_) => _.flatMap((_) => Option.fromNullable(int.tryParse(_)))); 8 | 9 | final maybeParseDouble = optionOfString 10 | .c((_) => _.flatMap((_) => Option.fromNullable(double.tryParse(_)))); 11 | 12 | extension IfSomeListExt on Option { 13 | Iterable ifSomeList(Iterable Function(A a) f) => match(() => [], f); 14 | } 15 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/models/barcode_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; 4 | 5 | part 'barcode_result.freezed.dart'; 6 | 7 | @freezed 8 | class BarcodeResult with _$BarcodeResult { 9 | const factory BarcodeResult({ 10 | required Barcode barcode, 11 | @Default(Option.none()) Option merchant, 12 | @Default(Option.none()) Option balance, 13 | @Default(Option.none()) Option expires, 14 | }) = _BarcodeResult; 15 | } 16 | -------------------------------------------------------------------------------- /lib/shared/scaffold/app_scaffold_simple.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_scaffold_simple.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class AppScaffoldSimple extends HookWidget { 10 | const AppScaffoldSimple({ 11 | Key? key, 12 | required this.body, 13 | }) : super(key: key); 14 | 15 | final Widget body; 16 | 17 | @override 18 | Widget build(BuildContext _context) => appScaffoldSimple( 19 | _context, 20 | body: body, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/models/ml_context.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; 3 | import 'package:google_mlkit_entity_extraction/google_mlkit_entity_extraction.dart'; 4 | import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; 5 | 6 | part 'ml_context.freezed.dart'; 7 | 8 | @freezed 9 | class MlContext with _$MlContext { 10 | const factory MlContext({ 11 | required TextRecognizer textRecognizer, 12 | required BarcodeScanner barcodeScanner, 13 | required EntityExtractor entityExtractor, 14 | }) = _MlContext; 15 | } 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/dart 3 | { 4 | "name": "Flutter", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | 9 | // Set *default* container specific settings.json values on container create. 10 | "settings": {}, 11 | 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": ["dart-code.dart-code", "dart-code.flutter"] 14 | 15 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 16 | // "forwardPorts": [], 17 | } 18 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | 34 | **/dgph -------------------------------------------------------------------------------- /lib/voucher_form/widgets/dialog.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'dialog.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherFormDialog extends HookWidget { 10 | const VoucherFormDialog({ 11 | Key? key, 12 | this.initialValue = const Option.none(), 13 | }) : super(key: key); 14 | 15 | final Option initialValue; 16 | 17 | @override 18 | Widget build(BuildContext _context) => _voucherFormDialog( 19 | _context, 20 | initialValue: initialValue, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/app/settings.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$VoucherVaultSettingsImpl _$$VoucherVaultSettingsImplFromJson( 10 | Map json) => 11 | _$VoucherVaultSettingsImpl( 12 | smartScan: json['smartScan'] as bool? ?? true, 13 | ); 14 | 15 | Map _$$VoucherVaultSettingsImplToJson( 16 | _$VoucherVaultSettingsImpl instance) => 17 | { 18 | 'smartScan': instance.smartScan, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/voucher_dialog_container.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_dialog_container.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherDialogContainer extends HookWidget { 10 | const VoucherDialogContainer({ 11 | Key? key, 12 | required this.voucher, 13 | }) : super(key: key); 14 | 15 | final Voucher voucher; 16 | 17 | @override 18 | Widget build(BuildContext _context) => _voucherDialogContainer( 19 | _context, 20 | voucher: voucher, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/lib/milliunits.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:vouchervault/lib/lib.dart'; 3 | 4 | int millisFromDouble(double i) => (i * 1000).round(); 5 | double millisToDouble(int units) => units / 1000.0; 6 | String millisToString(int i) => millisToDouble(i).toStringAsFixed(2); 7 | 8 | Option millisFromNullableDouble(double? i) => 9 | Option.fromNullable(i).map(millisFromDouble); 10 | 11 | Option millisFromString(String? s) => 12 | maybeParseDouble(s).map(millisFromDouble); 13 | 14 | Option maybeMillisToDouble(int? i) => 15 | Option.fromNullable(i).map(millisToDouble); 16 | 17 | Option maybeMillisToString(int? i) => 18 | Option.fromNullable(i).map(millisToString); 19 | -------------------------------------------------------------------------------- /lib/vouchers/list/voucher_list.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_list.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherList extends StatelessWidget { 10 | const VoucherList({ 11 | Key? key, 12 | required this.vouchers, 13 | required this.onPressed, 14 | }) : super(key: key); 15 | 16 | final IList vouchers; 17 | 18 | final void Function(Voucher) onPressed; 19 | 20 | @override 21 | Widget build(BuildContext _context) => _voucherList( 22 | vouchers: vouchers, 23 | onPressed: onPressed, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/models/ml_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ml_error.freezed.dart'; 4 | 5 | @freezed 6 | class MlError with _$MlError { 7 | const MlError._(); 8 | 9 | const factory MlError.barcodeNotFound() = _MlErrorBarcodeNotFound; 10 | const factory MlError.pickerError(String message) = _MlErrorPicker; 11 | const factory MlError.mlkitError({ 12 | required String op, 13 | required dynamic err, 14 | }) = _MlErrorMlKit; 15 | 16 | String get friendlyMessage => when( 17 | barcodeNotFound: () => 'Could not find a barcode', 18 | pickerError: (msg) => 'Could not load image: $msg', 19 | mlkitError: (op, err) => 'Could not process image in method: $op', 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/voucher_form/widgets/form.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'form.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherForm extends StatelessWidget { 10 | const VoucherForm({ 11 | Key? key, 12 | required this.formKey, 13 | required this.initialValue, 14 | }) : super(key: key); 15 | 16 | final GlobalKey formKey; 17 | 18 | final Voucher initialValue; 19 | 20 | @override 21 | Widget build(BuildContext _context) => voucherForm( 22 | _context, 23 | formKey: formKey, 24 | initialValue: initialValue, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.3.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /lib/vouchers/list/voucher_item.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_item.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherItem extends StatelessWidget { 10 | const VoucherItem({ 11 | Key? key, 12 | required this.voucher, 13 | required this.onPressed, 14 | this.bottomPadding = 0, 15 | }) : super(key: key); 16 | 17 | final Voucher voucher; 18 | 19 | final void Function() onPressed; 20 | 21 | final double bottomPadding; 22 | 23 | @override 24 | Widget build(BuildContext _context) => voucherItem( 25 | _context, 26 | voucher: voucher, 27 | onPressed: onPressed, 28 | bottomPadding: bottomPadding, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/voucher_vault_app.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_vault_app.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherVaultApp extends StatelessWidget { 10 | const VoucherVaultApp({ 11 | Key? key, 12 | this.initialValues = const [], 13 | }) : super(key: key); 14 | 15 | final List> initialValues; 16 | 17 | @override 18 | Widget build(BuildContext _context) => _voucherVaultApp( 19 | _context, 20 | initialValues: initialValues, 21 | ); 22 | } 23 | 24 | class _App extends StatelessWidget { 25 | const _App({Key? key}) : super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext _context) => __app(); 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | android/fastlane/report.xml 43 | 44 | # macos platform 45 | /macos 46 | .direnv/ 47 | /.flutter/ -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/vouchers/menu/vouchers_menu.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'vouchers_menu.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VouchersMenu extends StatelessWidget { 10 | const VouchersMenu({ 11 | Key? key, 12 | required this.onSelected, 13 | required this.values, 14 | required this.disabled, 15 | }) : super(key: key); 16 | 17 | final void Function(VouchersMenuAction) onSelected; 18 | 19 | final Map values; 20 | 21 | final Set disabled; 22 | 23 | @override 24 | Widget build(BuildContext _context) => vouchersMenu( 25 | onSelected: onSelected, 26 | values: values, 27 | disabled: disabled, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/barcode_button.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'barcode_button.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class BarcodeButton extends StatelessWidget { 10 | const BarcodeButton({ 11 | Key? key, 12 | required this.barcodeType, 13 | required this.data, 14 | required this.onPressed, 15 | }) : super(key: key); 16 | 17 | final Option barcodeType; 18 | 19 | final String data; 20 | 21 | final void Function() onPressed; 22 | 23 | @override 24 | Widget build(BuildContext _context) => _barcodeButton( 25 | _context, 26 | barcodeType: barcodeType, 27 | data: data, 28 | onPressed: onPressed, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "vouchers": "Vouchers", 3 | "edit": "Edit", 4 | "close": "Close", 5 | "copiedToClipboard": "Copied to clipboard", 6 | "areYouSure": "Are you sure?", 7 | "confirmRemoveVoucher": "That you want to remove this voucher?", 8 | "cancel": "Cancel", 9 | "remove": "Remove", 10 | "howMuchSpend": "How much did you spend?", 11 | "amount": "Amount", 12 | "ok": "OK", 13 | "addVoucher": "Add voucher", 14 | "editVoucher": "Edit voucher", 15 | "create": "Create", 16 | "update": "Update", 17 | "scanBarcode": "Scan barcode", 18 | "description": "Description", 19 | "code": "Code", 20 | "expires": "Expires", 21 | "removeOnceExpired": "Remove once expired", 22 | "balance": "Balance", 23 | "notes": "Notes", 24 | "import": "Import", 25 | "export": "Export", 26 | "appLock": "App lock", 27 | "smartScan": "Smart scan" 28 | } 29 | -------------------------------------------------------------------------------- /lib/vouchers/list/voucher_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:vouchervault/vouchers/index.dart'; 5 | 6 | part 'voucher_list.g.dart'; 7 | 8 | @swidget 9 | Widget _voucherList({ 10 | required IList vouchers, 11 | required void Function(Voucher) onPressed, 12 | }) { 13 | final vouchersLength = vouchers.length; 14 | 15 | return SliverList( 16 | delegate: SliverChildBuilderDelegate( 17 | (context, index) { 18 | final v = vouchers[index]; 19 | return VoucherItem( 20 | voucher: v, 21 | onPressed: () => onPressed(v), 22 | bottomPadding: 23 | index == vouchersLength - 1 ? 0 : kVoucherItemBorderRadius, 24 | ); 25 | }, 26 | childCount: vouchersLength, 27 | ), 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /lib/hooks/use_system_overlay_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | SystemUiOverlayStyle useSystemOverlayStyle() { 6 | final context = useContext(); 7 | final theme = Theme.of(context); 8 | 9 | final style = useMemoized( 10 | () => theme.brightness == Brightness.light 11 | ? SystemUiOverlayStyle.dark.copyWith( 12 | statusBarColor: Colors.transparent, 13 | systemNavigationBarColor: theme.colorScheme.surface, 14 | systemNavigationBarIconBrightness: Brightness.dark, 15 | ) 16 | : SystemUiOverlayStyle.light.copyWith( 17 | statusBarColor: Colors.transparent, 18 | systemNavigationBarColor: theme.colorScheme.surface, 19 | systemNavigationBarIconBrightness: Brightness.light, 20 | ), 21 | [theme], 22 | ); 23 | 24 | return style; 25 | } 26 | -------------------------------------------------------------------------------- /lib/l10n/app_fr.arb: -------------------------------------------------------------------------------- 1 | { 2 | "vouchers": "Mes bons", 3 | "edit": "Modifier", 4 | "close": "Fermer", 5 | "copiedToClipboard": "Copié dans le presse-papier", 6 | "areYouSure": "Etes-vous sûr?", 7 | "confirmRemoveVoucher": "De vouloir supprimer ce bon?", 8 | "cancel": "Annuler", 9 | "remove": "Supprimer", 10 | "howMuchSpend": "Quel montant avez-vous depensé?", 11 | "amount": "Montant", 12 | "ok": "Valider", 13 | "addVoucher": "Ajouter un bon", 14 | "editVoucher": "Modifier ce bon", 15 | "create": "Créer", 16 | "update": "Modifier", 17 | "scanBarcode": "Scanner un code barre", 18 | "description": "Description", 19 | "code": "Code", 20 | "expires": "Date d'expiration", 21 | "removeOnceExpired": "Supprimer une fois expiré", 22 | "balance": "Solde", 23 | "notes": "Notes", 24 | "import": "Importer", 25 | "export": "Exporter", 26 | "appLock": "Verrouiller l'appli", 27 | "smartScan": "Scan intelligent" 28 | } 29 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart' hide Logger; 3 | import 'package:logging/logging.dart'; 4 | import 'package:vouchervault/app/index.dart'; 5 | import 'package:vouchervault/auth/index.dart'; 6 | import 'package:vouchervault/vouchers/index.dart'; 7 | 8 | void main({IList? vouchers}) async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | 11 | Logger.root.level = Level.ALL; 12 | Logger.root.onRecord.listen((r) => 13 | // ignore: avoid_print 14 | print('${r.loggerName}: ${r.level.name}: ${r.time}: ${r.message}')); 15 | 16 | final runtime = await runtimeInitialValue([ 17 | sharedPrefsLayer, 18 | authLayer, 19 | if (vouchers != null) 20 | vouchersLayer.replace(IO.succeed(VouchersService( 21 | ref: Ref.unsafeMake(VouchersState(vouchers)), 22 | ))) 23 | else 24 | vouchersLayer, 25 | ]); 26 | 27 | runApp(VoucherVaultApp(initialValues: [runtime])); 28 | } 29 | -------------------------------------------------------------------------------- /lib/shared/voucher_details/voucher_details.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_details.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class _VoucherDetailRow extends StatelessWidget { 10 | const _VoucherDetailRow( 11 | this.icon, 12 | this.text, { 13 | Key? key, 14 | this.alignment = CrossAxisAlignment.center, 15 | this.iconPadding = false, 16 | this.selectable = false, 17 | }) : super(key: key); 18 | 19 | final IconData icon; 20 | 21 | final String text; 22 | 23 | final CrossAxisAlignment alignment; 24 | 25 | final bool iconPadding; 26 | 27 | final bool selectable; 28 | 29 | @override 30 | Widget build(BuildContext _context) => __voucherDetailRow( 31 | _context, 32 | icon, 33 | text, 34 | alignment: alignment, 35 | iconPadding: iconPadding, 36 | selectable: selectable, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "7c6b7e9ca485f7eaaed913c6bb50f4be6da47e30" 8 | channel: "beta" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 7c6b7e9ca485f7eaaed913c6bb50f4be6da47e30 17 | base_revision: 7c6b7e9ca485f7eaaed913c6bb50f4be6da47e30 18 | - platform: android 19 | create_revision: 7c6b7e9ca485f7eaaed913c6bb50f4be6da47e30 20 | base_revision: 7c6b7e9ca485f7eaaed913c6bb50f4be6da47e30 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /lib/vouchers/models/state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:vouchervault/vouchers/index.dart'; 4 | 5 | part 'state.freezed.dart'; 6 | 7 | @freezed 8 | class VouchersState with _$VouchersState { 9 | VouchersState._(); 10 | factory VouchersState(IList vouchers) = _VouchersState; 11 | 12 | late final IList sortedVouchers = vouchers.sort(_compareVoucher); 13 | 14 | dynamic toJson() => vouchers.toJson((v) => v.toJson()); 15 | static VouchersState fromJson(dynamic json) => VouchersState( 16 | IList.fromJson(json, Voucher.fromJson), 17 | ); 18 | } 19 | 20 | int _unix(Option dt) => 21 | dt.map((d) => d.millisecondsSinceEpoch).getOrElse(() => 0); 22 | 23 | int _compareVoucher(Voucher a, Voucher b) { 24 | final compare = a.description.compareTo(b.description); 25 | final expiresCompare = 26 | _unix(a.normalizedExpires).compareTo(_unix(b.normalizedExpires)); 27 | 28 | return compare != 0 ? compare : expiresCompare; 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/shared/scaffold/app_scaffold.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_scaffold.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class AppScaffold extends HookWidget { 10 | const AppScaffold({ 11 | Key? key, 12 | required this.title, 13 | required this.slivers, 14 | this.actions = const [], 15 | this.floatingActionButton = const Option.none(), 16 | this.leading = false, 17 | }) : super(key: key); 18 | 19 | final String title; 20 | 21 | final List slivers; 22 | 23 | final List actions; 24 | 25 | final Option floatingActionButton; 26 | 27 | final bool leading; 28 | 29 | @override 30 | Widget build(BuildContext _context) => appScaffold( 31 | _context, 32 | title: title, 33 | slivers: slivers, 34 | actions: actions, 35 | floatingActionButton: floatingActionButton, 36 | leading: leading, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /lib/app/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppTheme { 4 | static const baseFontSize = 18; 5 | static double rem(double rem) => baseFontSize * rem; 6 | static double px(double px) => rem(px / 18); 7 | 8 | static double get space1 => rem(0.1); 9 | static double get space2 => rem(0.3); 10 | static double get space3 => rem(0.6); 11 | static double get space4 => rem(1); 12 | static double get space5 => rem(1.5); 13 | static double get space6 => rem(2.5); 14 | static double get space7 => rem(4); 15 | 16 | static ThemeData build( 17 | ColorScheme scheme, { 18 | TextTheme? textTheme, 19 | }) { 20 | final theme = ThemeData.from( 21 | useMaterial3: true, 22 | colorScheme: scheme.copyWith( 23 | outline: scheme.outline.withAlpha(100), 24 | ), 25 | textTheme: textTheme, 26 | ); 27 | return theme.copyWith( 28 | pageTransitionsTheme: const PageTransitionsTheme(builders: { 29 | TargetPlatform.android: CupertinoPageTransitionsBuilder(), 30 | TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), 31 | }), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/shared/scaffold/app_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 5 | import 'package:vouchervault/hooks/index.dart'; 6 | 7 | part 'app_scaffold.g.dart'; 8 | 9 | @hwidget 10 | Widget appScaffold( 11 | BuildContext context, { 12 | required String title, 13 | required List slivers, 14 | List actions = const [], 15 | Option floatingActionButton = const Option.none(), 16 | bool leading = false, 17 | }) { 18 | final style = useSystemOverlayStyle(); 19 | 20 | return Scaffold( 21 | body: AnnotatedRegion( 22 | value: style, 23 | child: NestedScrollView( 24 | headerSliverBuilder: (context, innerBoxIsScrolled) => [ 25 | SliverAppBar.large( 26 | systemOverlayStyle: style, 27 | actions: actions, 28 | title: Text(title), 29 | ), 30 | ], 31 | body: CustomScrollView(slivers: slivers), 32 | ), 33 | ), 34 | floatingActionButton: floatingActionButton.toNullable(), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch-slim 2 | 3 | # Hack to get openjdk to install in a container 4 | RUN mkdir -p /usr/share/man/man1 \ 5 | && mkdir -p /usr/share/man/man7 6 | 7 | # Apt 8 | RUN apt update && apt install -y curl wget git xz-utils lib32stdc++6 unzip openjdk-8-jdk-headless 9 | 10 | # Android SDK 11 | RUN wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip 12 | RUN mkdir android-sdk && unzip /sdk-tools-linux-4333796.zip -d android-sdk 13 | RUN rm /sdk-tools-linux-4333796.zip 14 | ENV ANDROID_HOME="/android-sdk" 15 | ENV PATH="/android-sdk/tools/bin:/android-sdk/build-tools:/android-sdk/platform-tools:${PATH}" 16 | 17 | # SDK manager 18 | RUN yes | sdkmanager --licenses 19 | RUN sdkmanager "platforms;android-28" "platform-tools" "build-tools;28.0.3" 20 | 21 | # Flutter 22 | ENV FLUTTER_VERSION "3.0.1-stable" 23 | RUN wget "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}.tar.xz" 24 | RUN tar xf "flutter_linux_${FLUTTER_VERSION}.tar.xz" 25 | RUN rm "flutter_linux_${FLUTTER_VERSION}.tar.xz" 26 | ENV PATH="/flutter/bin:${PATH}" 27 | 28 | RUN flutter config --no-analytics 29 | 30 | # Set a useful default shell 31 | ENV SHELL /bin/bash 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_BUILD_NUMBER = $(shell cat metadata/build_number.txt | head) 2 | NEXT_BUILD_NUMBER = $(shell expr $(CURRENT_BUILD_NUMBER) + 1) 3 | 4 | .PHONY: default 5 | default: alpha 6 | 7 | .PHONY: clean 8 | clean: 9 | flutter clean 10 | 11 | .PHONY: build_runner 12 | build_runner: 13 | pm2 start --no-daemon "flutter pub run build_runner watch --delete-conflicting-outputs" 14 | 15 | .PHONY: icons 16 | icons: 17 | flutter pub run flutter_launcher_icons:main 18 | 19 | .PHONY: screenshots 20 | screenshots: 21 | dart tools/screenshots.dart 22 | 23 | git reset 24 | git add android/fastlane/metadata/android/en-US/images 25 | git add ios/fastlane/screenshots 26 | git commit -m "Update screenshots" 27 | 28 | .PHONY: increment-build-number 29 | increment-build-number: 30 | @echo $(NEXT_BUILD_NUMBER) > metadata/build_number.txt 31 | 32 | .PHONY: alpha 33 | alpha: clean increment-build-number 34 | cd android && bundle update fastlane && bundle exec fastlane alpha 35 | 36 | .PHONY: beta 37 | beta: clean increment-build-number 38 | cd android && bundle update fastlane && bundle exec fastlane beta 39 | 40 | .PHONY: release 41 | release: clean increment-build-number 42 | cd android && bundle update fastlane && bundle exec fastlane release -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | outputs = { 7 | self, 8 | nixpkgs, 9 | flake-utils, 10 | }: 11 | flake-utils.lib.eachDefaultSystem (system: let 12 | pkgs = import nixpkgs { 13 | inherit system; 14 | config = { 15 | android_sdk.accept_license = true; 16 | allowUnfree = true; 17 | }; 18 | }; 19 | flutter_path = "$PWD/.flutter"; 20 | flutter = import ./nix/flutter.nix { 21 | inherit pkgs system; 22 | path = flutter_path; 23 | }; 24 | in { 25 | devShell = with pkgs; 26 | mkShellNoCC { 27 | buildInputs = [ 28 | cocoapods 29 | jdk17 30 | ruby 31 | sops 32 | ]; 33 | shellHook = '' 34 | ${flutter.unpack}/bin/unpack 35 | export PATH="${flutter_path}/flutter/bin:$PATH" 36 | export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk 37 | mkdir -p android/keys 38 | sops -d secrets/fastlane-google-key > android/keys/google.json 39 | ''; 40 | }; 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android build 19 | 20 | ```sh 21 | [bundle exec] fastlane android build 22 | ``` 23 | 24 | 25 | 26 | ### android alpha 27 | 28 | ```sh 29 | [bundle exec] fastlane android alpha 30 | ``` 31 | 32 | Submit a new build to Google Play internal track 33 | 34 | ### android beta 35 | 36 | ```sh 37 | [bundle exec] fastlane android beta 38 | ``` 39 | 40 | Submit a new build to Google Play beta track 41 | 42 | ### android release 43 | 44 | ```sh 45 | [bundle exec] fastlane android release 46 | ``` 47 | 48 | Submit a new build to Google Play production track 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 53 | 54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 55 | 56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 57 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/voucher_spend_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | part 'voucher_spend_dialog.g.dart'; 7 | 8 | @hwidget 9 | Widget _voucherSpendDialog(BuildContext context) { 10 | final amount = useValueNotifier(''); 11 | void submit() => Navigator.pop(context, amount.value); 12 | 13 | return AlertDialog( 14 | title: Text(AppLocalizations.of(context)!.howMuchSpend), 15 | content: TextField( 16 | autofocus: true, 17 | decoration: InputDecoration( 18 | border: const OutlineInputBorder(), 19 | labelText: AppLocalizations.of(context)!.amount, 20 | ), 21 | keyboardType: const TextInputType.numberWithOptions(signed: true), 22 | onChanged: (s) => amount.value = s, 23 | onSubmitted: (s) => submit(), 24 | ), 25 | actions: [ 26 | TextButton( 27 | onPressed: () => Navigator.pop(context, null), 28 | child: Text(AppLocalizations.of(context)!.cancel), 29 | ), 30 | TextButton( 31 | onPressed: submit, 32 | child: Text(AppLocalizations.of(context)!.ok), 33 | ), 34 | ], 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /nix/flutter.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import {}, 3 | path, 4 | system, 5 | }: rec { 6 | source = 7 | if system == "aarch64-darwin" 8 | then 9 | pkgs.fetchurl { 10 | url = "https://storage.googleapis.com/flutter_infra_release/releases/betc/macos/flutter_macos_arm64_3.24.0-0.2.pre-beta.zip"; 11 | hash = ""; 12 | } 13 | else if system == "x86_64-darwin" 14 | then 15 | pkgs.fetchurl { 16 | url = "https://storage.googleapis.com/flutter_infra_release/releases/beta/macos/flutter_macos_3.24.0-0.2.pre-beta.zip"; 17 | hash = "sha256-AbeSWHYRORM74qM9W4v0pyQ9Wc/DO8FJejR2QwReEgY="; 18 | } 19 | else abort "unsupported system"; 20 | 21 | unpack = pkgs.writeShellApplication { 22 | name = "unpack"; 23 | runtimeInputs = with pkgs; [unzip]; 24 | text = '' 25 | flutter_local_dir=${path} 26 | flutter_bin_dir="$flutter_local_dir"/flutter/bin 27 | flutter_bin_file="$flutter_bin_dir"/flutter 28 | 29 | echo "flutter needs local installation? ..." 30 | 31 | if [ -f "$flutter_bin_file" ]; then 32 | echo "flutter is already installed locally in '$flutter_local_dir'" 33 | else 34 | echo "... installing flutter locally in '$flutter_local_dir'" 35 | unzip ${source} -d "$flutter_local_dir" 36 | fi 37 | ''; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /lib/auth/auth_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 5 | import 'package:vouchervault/app/index.dart'; 6 | import 'package:vouchervault/auth/index.dart'; 7 | import 'package:vouchervault/shared/scaffold/app_scaffold_simple.dart'; 8 | 9 | part 'auth_screen.g.dart'; 10 | 11 | @hwidget 12 | Widget authScreen(BuildContext context) { 13 | final authenticate = useZIO(authLayer.accessWithZIO((_) => _.authenticate)); 14 | final theme = Theme.of(context); 15 | 16 | useEffect(() { 17 | authenticate(); 18 | return null; 19 | }, []); 20 | 21 | return AppScaffoldSimple( 22 | body: Center( 23 | child: ElevatedButton( 24 | style: ElevatedButton.styleFrom( 25 | backgroundColor: theme.colorScheme.primary, 26 | foregroundColor: theme.colorScheme.onPrimary, 27 | ), 28 | onPressed: authenticate, 29 | child: Row( 30 | mainAxisSize: MainAxisSize.min, 31 | children: [ 32 | const Icon(Icons.lock_open), 33 | SizedBox(width: AppTheme.space2), 34 | const Text('Unlock'), 35 | ], 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /lib/auth/model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$UnauthenticatedImpl _$$UnauthenticatedImplFromJson( 10 | Map json) => 11 | _$UnauthenticatedImpl( 12 | $type: json['runtimeType'] as String?, 13 | ); 14 | 15 | Map _$$UnauthenticatedImplToJson( 16 | _$UnauthenticatedImpl instance) => 17 | { 18 | 'runtimeType': instance.$type, 19 | }; 20 | 21 | _$AuthenticatedImpl _$$AuthenticatedImplFromJson(Map json) => 22 | _$AuthenticatedImpl( 23 | $enumDecode(_$AuthenticatedReasonEnumMap, json['reason']), 24 | $type: json['runtimeType'] as String?, 25 | ); 26 | 27 | Map _$$AuthenticatedImplToJson(_$AuthenticatedImpl instance) => 28 | { 29 | 'reason': _$AuthenticatedReasonEnumMap[instance.reason]!, 30 | 'runtimeType': instance.$type, 31 | }; 32 | 33 | const _$AuthenticatedReasonEnumMap = { 34 | AuthenticatedReason.NOT_AVAILABLE: 'NOT_AVAILABLE', 35 | AuthenticatedReason.NOT_REQUIRED: 'NOT_REQUIRED', 36 | AuthenticatedReason.SUCCESS: 'SUCCESS', 37 | }; 38 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:android) 2 | 3 | build_number = File.open("../../metadata/build_number.txt", "r") { |f| f.read.strip.to_i } 4 | 5 | platform :android do 6 | lane :build do 7 | # Update changelogs 8 | FileUtils.cp( 9 | "../../metadata/release_notes.txt", 10 | "metadata/android/en-US/changelogs/#{build_number}.txt", 11 | ) 12 | 13 | Dir.chdir("../..") do 14 | sh( 15 | "flutter", 16 | "build", 17 | "appbundle", 18 | "--release", 19 | "--build-number=#{build_number}", 20 | ) 21 | end 22 | end 23 | 24 | desc "Submit a new build to Google Play internal track" 25 | lane :alpha do 26 | build() 27 | upload_to_play_store( 28 | aab: "../build/app/outputs/bundle/release/app-release.aab", 29 | track: "internal", 30 | ) 31 | end 32 | 33 | desc "Submit a new build to Google Play beta track" 34 | lane :beta do 35 | alpha() 36 | upload_to_play_store( 37 | track: "internal", 38 | track_promote_to: "beta", 39 | version_code: build_number.to_s, 40 | ) 41 | end 42 | 43 | desc "Submit a new build to Google Play production track" 44 | lane :release do 45 | beta() 46 | upload_to_play_store( 47 | track: "beta", 48 | track_promote_to: "production", 49 | version_code: build_number.to_s, 50 | ) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/lib/barcode.dart: -------------------------------------------------------------------------------- 1 | import 'package:barcode_widget/barcode_widget.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart' 4 | show BarcodeFormat; 5 | import 'package:vouchervault/vouchers/index.dart'; 6 | 7 | final Map _codeTypeMap = { 8 | VoucherCodeType.AZTEC: Barcode.aztec(minECCPercent: 5), 9 | VoucherCodeType.CODE128: Barcode.code128(), 10 | VoucherCodeType.CODE39: Barcode.code39(), 11 | VoucherCodeType.EAN13: Barcode.ean13(), 12 | VoucherCodeType.PDF417: Barcode.pdf417(), 13 | VoucherCodeType.QR: Barcode.qrCode(), 14 | }; 15 | final barcodeFromCodeType = _codeTypeMap.lookup; 16 | final barcodeFromCodeTypeJson = codeTypeFromJson.c(barcodeFromCodeType); 17 | 18 | final Map _barcodeFormatMap = { 19 | BarcodeFormat.aztec: VoucherCodeType.AZTEC, 20 | BarcodeFormat.code128: VoucherCodeType.CODE128, 21 | BarcodeFormat.code39: VoucherCodeType.CODE39, 22 | BarcodeFormat.ean13: VoucherCodeType.EAN13, 23 | BarcodeFormat.pdf417: VoucherCodeType.PDF417, 24 | BarcodeFormat.qrCode: VoucherCodeType.QR, 25 | }; 26 | VoucherCodeType codeTypeFromFormat(BarcodeFormat format) => 27 | _barcodeFormatMap.lookup(format).getOrElse(() => VoucherCodeType.CODE128); 28 | 29 | final codeTypeValueFromFormat = codeTypeFromFormat.c(codeTypeToJson); 30 | -------------------------------------------------------------------------------- /lib/auth/model.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | part 'model.freezed.dart'; 6 | part 'model.g.dart'; 7 | 8 | enum AuthenticatedReason { 9 | NOT_AVAILABLE, 10 | NOT_REQUIRED, 11 | SUCCESS, 12 | } 13 | 14 | @freezed 15 | class AuthState with _$AuthState { 16 | const AuthState._(); 17 | 18 | const factory AuthState.unauthenticated() = Unauthenticated; 19 | const factory AuthState.authenticated(AuthenticatedReason reason) = 20 | Authenticated; 21 | 22 | static const notAvailable = 23 | AuthState.authenticated(AuthenticatedReason.NOT_AVAILABLE); 24 | static const notRequired = 25 | AuthState.authenticated(AuthenticatedReason.NOT_REQUIRED); 26 | static const success = AuthState.authenticated(AuthenticatedReason.SUCCESS); 27 | 28 | factory AuthState.fromJson(Map json) => 29 | _$AuthStateFromJson(json); 30 | 31 | bool get available => this != notAvailable; 32 | 33 | bool get enabled => when( 34 | unauthenticated: () => true, 35 | authenticated: (reason) => reason == AuthenticatedReason.SUCCESS, 36 | ); 37 | 38 | AuthState enable() => 39 | available ? const AuthState.unauthenticated() : notAvailable; 40 | 41 | AuthState disable() => available ? notRequired : notAvailable; 42 | 43 | AuthState init() => enabled ? const AuthState.unauthenticated() : this; 44 | } 45 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/barcode_scanner_field.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'barcode_scanner_field.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class BarcodeScannerField extends HookWidget { 10 | const BarcodeScannerField({ 11 | Key? key, 12 | required this.onChange, 13 | required this.initialValue, 14 | required this.barcodeType, 15 | required this.labelText, 16 | this.errorText = const Option.none(), 17 | required this.onScan, 18 | this.launchScannerImmediately = false, 19 | }) : super(key: key); 20 | 21 | final void Function(String) onChange; 22 | 23 | final String initialValue; 24 | 25 | final Option barcodeType; 26 | 27 | final String labelText; 28 | 29 | final Option errorText; 30 | 31 | final void Function(BarcodeResult) onScan; 32 | 33 | final bool launchScannerImmediately; 34 | 35 | @override 36 | Widget build(BuildContext _context) => _barcodeScannerField( 37 | _context, 38 | onChange: onChange, 39 | initialValue: initialValue, 40 | barcodeType: barcodeType, 41 | labelText: labelText, 42 | errorText: errorText, 43 | onScan: onScan, 44 | launchScannerImmediately: launchScannerImmediately, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/hooks/use_full_brightness.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide Action; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | import 'package:screen_brightness/screen_brightness.dart'; 5 | import 'package:vouchervault/hooks/index.dart'; 6 | 7 | void useFullBrightness( 8 | RouteObserver routeObserver, { 9 | bool enabled = true, 10 | }) { 11 | final brightness = useMemoized(() => ScreenBrightness()); 12 | final goBright = useZIO( 13 | EIO 14 | .tryCatch( 15 | () => brightness.setScreenBrightness(1), 16 | (error, stackTrace) => 'Could not set brightness: $error', 17 | ) 18 | .ignoreLogged, 19 | [brightness], 20 | ); 21 | final goDark = useZIO( 22 | EIO 23 | .tryCatch( 24 | () => brightness.resetScreenBrightness(), 25 | (error, stackTrace) => 'Could not set brightness: $error', 26 | ) 27 | .ignoreLogged, 28 | [brightness], 29 | ); 30 | 31 | useEffect(() { 32 | if (enabled) { 33 | goBright(); 34 | } 35 | 36 | return () { 37 | goDark(); 38 | }; 39 | }, [enabled]); 40 | 41 | // If something gets pushed on top of the route, then go dark again. 42 | useRouteObserver( 43 | routeObserver, 44 | didPushNext: Option.of(() { 45 | goDark(); 46 | }), 47 | didPopNext: Option.of(() { 48 | if (!enabled) return; 49 | goBright(); 50 | }), 51 | keys: [enabled], 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/vouchers/list/vouchers_list_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:dismissible_page/dismissible_page.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_elemental/flutter_elemental.dart'; 4 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 5 | import 'package:vouchervault/vouchers/index.dart'; 6 | 7 | part 'vouchers_list_container.g.dart'; 8 | 9 | class _DialogRoute extends PageRoute with MaterialRouteTransitionMixin { 10 | _DialogRoute(this.builder) : super(fullscreenDialog: true); 11 | 12 | final WidgetBuilder builder; 13 | 14 | @override 15 | Widget buildContent(BuildContext context) => builder(context); 16 | 17 | @override 18 | bool get opaque => false; 19 | 20 | @override 21 | bool get maintainState => false; 22 | 23 | @override 24 | Color get barrierColor => Colors.black54; 25 | } 26 | 27 | @swidget 28 | Widget _vouchersListContainer(BuildContext context) { 29 | return AtomBuilder( 30 | (context, watch, child) => VoucherList( 31 | vouchers: watch(vouchersAtom), 32 | onPressed: (v) => Navigator.push( 33 | context, 34 | _DialogRoute( 35 | (context) => DismissiblePage( 36 | backgroundColor: Colors.transparent, 37 | direction: DismissiblePageDismissDirection.multi, 38 | minScale: 0.3, 39 | onDismissed: () => Navigator.pop(context), 40 | child: Center(child: VoucherDialogContainer(voucher: v)), 41 | ), 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/vouchers/vouchers_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:vouchervault/app/index.dart'; 5 | import 'package:vouchervault/shared/scaffold/scaffold.dart'; 6 | import 'package:vouchervault/voucher_form/index.dart'; 7 | import 'package:vouchervault/vouchers/index.dart'; 8 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 9 | 10 | part 'vouchers_screen.g.dart'; 11 | 12 | @swidget 13 | Widget _vouchersScreen(BuildContext context) { 14 | return AppScaffold( 15 | title: AppLocalizations.of(context)!.vouchers, 16 | actions: const [VouchersMenuContainer()], 17 | slivers: [ 18 | SliverPadding( 19 | padding: EdgeInsets.only( 20 | top: AppTheme.rem(1.5), 21 | bottom: AppTheme.space6, 22 | ), 23 | sliver: const VouchersListContainer(), 24 | ), 25 | ], 26 | floatingActionButton: Option.of(FloatingActionButton( 27 | onPressed: () => Navigator.of(context) 28 | .pushIO(MaterialPageRoute( 29 | builder: (context) => const VoucherFormDialog(), 30 | fullscreenDialog: true, 31 | )) 32 | .tap(_createVoucher) 33 | .provide(context) 34 | .runContext(context), 35 | child: const Icon(Icons.add), 36 | )), 37 | ); 38 | } 39 | 40 | BuildContextIO _createVoucher(Voucher v) => 41 | vouchersLayer.accessWithZIO((_) => _.create(v).lift()); 42 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def keystoreProperties = new Properties() 8 | def keystorePropertiesFile = rootProject.file('key.properties') 9 | if (keystorePropertiesFile.exists()) { 10 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 11 | } 12 | 13 | android { 14 | namespace = "co.timsmart.vouchervault" 15 | compileSdk = 34 16 | ndkVersion = flutter.ndkVersion 17 | 18 | compileOptions { 19 | sourceCompatibility = JavaVersion.VERSION_1_8 20 | targetCompatibility = JavaVersion.VERSION_1_8 21 | } 22 | 23 | kotlinOptions { 24 | jvmTarget = JavaVersion.VERSION_1_8 25 | } 26 | 27 | defaultConfig { 28 | applicationId = "co.timsmart.vouchervault" 29 | minSdk = flutter.minSdkVersion 30 | targetSdk = 34 31 | versionCode = flutter.versionCode 32 | versionName = flutter.versionName 33 | } 34 | 35 | signingConfigs { 36 | release { 37 | keyAlias keystoreProperties['keyAlias'] 38 | keyPassword keystoreProperties['keyPassword'] 39 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 40 | storePassword keystoreProperties['storePassword'] 41 | } 42 | } 43 | 44 | buildTypes { 45 | release { 46 | signingConfig signingConfigs.release 47 | 48 | minifyEnabled true 49 | shrinkResources true 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source = "../.." 56 | } 57 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | 41 | target.build_configurations.each do |config| 42 | config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = "13.0" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/vouchers/menu/vouchers_menu_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:vouchervault/app/index.dart'; 5 | import 'package:vouchervault/auth/index.dart'; 6 | import 'package:vouchervault/vouchers/index.dart'; 7 | 8 | part 'vouchers_menu_container.g.dart'; 9 | 10 | extension _VoucherMenuActions on VouchersMenuAction { 11 | void execute(BuildContext x) { 12 | switch (this) { 13 | case VouchersMenuAction.import: 14 | vouchersLayer.accessWithZIO((_) => _.import).runContext(x); 15 | return; 16 | case VouchersMenuAction.export: 17 | vouchersLayer.accessWithZIO((_) => _.export).runContext(x); 18 | return; 19 | case VouchersMenuAction.authentication: 20 | authLayer.accessWithZIO((_) => _.toggle).runContext(x); 21 | return; 22 | case VouchersMenuAction.smartScan: 23 | x.updateAtom(appSettings)((s) => s.copyWith(smartScan: !s.smartScan)); 24 | return; 25 | } 26 | } 27 | } 28 | 29 | @swidget 30 | Widget vouchersMenuContainer() { 31 | return AtomBuilder((context, watch, child) { 32 | final authAvailable = watch(authAvailableAtom); 33 | 34 | return VouchersMenu( 35 | onSelected: (action) => action.execute(context), 36 | values: { 37 | VouchersMenuAction.authentication: watch(authEnabledAtom), 38 | VouchersMenuAction.smartScan: watch(appSettings).smartScan, 39 | }, 40 | disabled: { 41 | if (!authAvailable) VouchersMenuAction.authentication, 42 | }, 43 | ); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1722640603, 24 | "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /test_driver/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_driver/driver_extension.dart'; 3 | import 'package:flutter_elemental/flutter_elemental.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | import 'package:vouchervault/main.dart' as app; 6 | import 'package:vouchervault/vouchers/index.dart'; 7 | 8 | void main() async { 9 | WidgetsApp.debugAllowBannerOverride = false; 10 | enableFlutterDriverExtension(); 11 | 12 | app.main( 13 | vouchers: IList([ 14 | Voucher( 15 | uuid: Option.of(const Uuid().v4()), 16 | description: "Walmart", 17 | code: const Option.of('12345'), 18 | codeType: VoucherCodeType.QR, 19 | color: VoucherColor.BLUE, 20 | balanceMilliunits: const Option.of(77 * 1000), 21 | expires: DateTime.now().add(const Duration(days: 120)).chain(Option.of), 22 | ), 23 | Voucher( 24 | uuid: const Uuid().v4().chain(Option.of), 25 | description: "Starbucks", 26 | code: "12345".chain(Option.of), 27 | codeType: VoucherCodeType.CODE128, 28 | color: VoucherColor.GREEN, 29 | balanceMilliunits: const Option.of(50 * 1000), 30 | ), 31 | Voucher( 32 | uuid: const Uuid().v4().chain(Option.of), 33 | description: "New World Clubcard", 34 | code: "12345".chain(Option.of), 35 | codeType: VoucherCodeType.QR, 36 | color: VoucherColor.RED, 37 | ), 38 | Voucher( 39 | uuid: const Uuid().v4().chain(Option.of), 40 | description: "Barkers", 41 | code: "12345".chain(Option.of), 42 | codeType: VoucherCodeType.QR, 43 | color: VoucherColor.GREY, 44 | balanceMilliunits: const Option.of(100 * 1000), 45 | ), 46 | ]), 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: vouchervault 2 | description: An app for storing vouchers and loyalty cards 3 | publish_to: "none" 4 | version: 1.2.0 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | auto_size_text: ^3.0.0 13 | barcode_widget: ^2.0.0 14 | camera: ^0.11.0+1 15 | dart_date: ^1.1.0 16 | file_picker: ^8.0.7 17 | flutter_form_builder: ^9.3.0 18 | flutter_hooks: ^0.20.5 19 | flutter_localizations: 20 | sdk: flutter 21 | flutter_elemental: ^0.3.1 22 | fluttertoast: ^8.2.6 23 | form_builder_validators: ^11.0.0 24 | freezed_annotation: ^2.0.3 25 | functional_widget_annotation: ^0.10.0 26 | google_mlkit_barcode_scanning: ^0.12.0 27 | google_mlkit_entity_extraction: ^0.13.0 28 | google_mlkit_text_recognition: ^0.13.0 29 | intl: ^0.19.0 30 | json_annotation: ^4.9.0 31 | local_auth: ^2.1.0 32 | logging: ^1.0.2 33 | path_provider: ^2.0.1 34 | recase: ^4.0.0 35 | rxdart: ^0.28.0 36 | share: ^2.0.0 37 | screen_brightness: ^1.0.1 38 | uuid: ^4.4.2 39 | shared_preferences: ^2.0.13 40 | image_picker: ^1.0.2 41 | dynamic_color: ^1.7.0 42 | dismissible_page: ^1.0.2 43 | 44 | dev_dependencies: 45 | build_runner: ^2.0.1 46 | emulators: ^0.6.0 47 | flutter_driver: 48 | sdk: flutter 49 | flutter_launcher_icons: ^0.13.1 50 | flutter_lints: ^4.0.0 51 | flutter_test: 52 | sdk: flutter 53 | freezed: ^2.0.3 54 | functional_widget: ^0.10.0 55 | json_serializable: ^6.0.1 56 | test: any 57 | 58 | flutter: 59 | uses-material-design: true 60 | generate: true 61 | 62 | flutter_icons: 63 | android: true 64 | ios: true 65 | image_path: assets/icon/icon.png 66 | adaptive_icon_background: assets/icon/icon_bg.png 67 | adaptive_icon_foreground: assets/icon/icon_fg.png 68 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Voucher Vault 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | vouchervault 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSCameraUsageDescription 28 | Camera permission is required for barcode scanning. 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIStatusBarStyle 34 | UIStatusBarStyleLightContent 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | 43 | UIViewControllerBasedStatusBarAppearance 44 | 45 | NSFaceIDUsageDescription 46 | Face ID is being used to protect your vouchers. 47 | CADisableMinimumFrameDurationOnPhone 48 | 49 | UIApplicationSupportsIndirectInputEvents 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/scanner_dialog.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'scanner_dialog.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class ScannerDialog extends HookWidget { 10 | const ScannerDialog({ 11 | Key? key, 12 | required this.onScan, 13 | }) : super(key: key); 14 | 15 | final void Function(BarcodeResult) onScan; 16 | 17 | @override 18 | Widget build(BuildContext _context) => _scannerDialog( 19 | _context, 20 | onScan: onScan, 21 | ); 22 | } 23 | 24 | class _PreviewDialog extends StatelessWidget { 25 | const _PreviewDialog({ 26 | Key? key, 27 | required this.controller, 28 | required this.onPressedPicker, 29 | required this.onPressedFlash, 30 | }) : super(key: key); 31 | 32 | final Option controller; 33 | 34 | final void Function() onPressedPicker; 35 | 36 | final void Function() onPressedFlash; 37 | 38 | @override 39 | Widget build(BuildContext _context) => __previewDialog( 40 | _context, 41 | controller: controller, 42 | onPressedPicker: onPressedPicker, 43 | onPressedFlash: onPressedFlash, 44 | ); 45 | } 46 | 47 | class _CameraPreview extends StatelessWidget { 48 | const _CameraPreview({ 49 | Key? key, 50 | required this.controller, 51 | }) : super(key: key); 52 | 53 | final CameraController controller; 54 | 55 | @override 56 | Widget build(BuildContext _context) => 57 | __cameraPreview(controller: controller); 58 | } 59 | 60 | class _FlashIcon extends HookWidget { 61 | const _FlashIcon({ 62 | Key? key, 63 | required this.controller, 64 | }) : super(key: key); 65 | 66 | final CameraController controller; 67 | 68 | @override 69 | Widget build(BuildContext _context) => __flashIcon(controller: controller); 70 | } 71 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/voucher_dialog.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher_dialog.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalWidgetGenerator 7 | // ************************************************************************** 8 | 9 | class VoucherDialog extends StatelessWidget { 10 | const VoucherDialog({ 11 | Key? key, 12 | required this.voucher, 13 | required this.onTapBarcode, 14 | required this.onEdit, 15 | required this.onClose, 16 | required this.onRemove, 17 | required this.onSpend, 18 | }) : super(key: key); 19 | 20 | final Voucher voucher; 21 | 22 | final void Function() onTapBarcode; 23 | 24 | final void Function() onEdit; 25 | 26 | final void Function() onClose; 27 | 28 | final void Function() onRemove; 29 | 30 | final void Function() onSpend; 31 | 32 | @override 33 | Widget build(BuildContext _context) => _voucherDialog( 34 | _context, 35 | voucher: voucher, 36 | onTapBarcode: onTapBarcode, 37 | onEdit: onEdit, 38 | onClose: onClose, 39 | onRemove: onRemove, 40 | onSpend: onSpend, 41 | ); 42 | } 43 | 44 | class _DialogWrap extends StatelessWidget { 45 | const _DialogWrap({ 46 | Key? key, 47 | required this.theme, 48 | required this.child, 49 | }) : super(key: key); 50 | 51 | final ThemeData theme; 52 | 53 | final Widget child; 54 | 55 | @override 56 | Widget build(BuildContext _context) => __dialogWrap( 57 | _context, 58 | theme: theme, 59 | child: child, 60 | ); 61 | } 62 | 63 | class _Barcode extends StatelessWidget { 64 | const _Barcode({ 65 | Key? key, 66 | required this.type, 67 | required this.data, 68 | required this.onTap, 69 | }) : super(key: key); 70 | 71 | final VoucherCodeType type; 72 | 73 | final String data; 74 | 75 | final void Function() onTap; 76 | 77 | @override 78 | Widget build(BuildContext _context) => __barcode( 79 | _context, 80 | type: type, 81 | data: data, 82 | onTap: onTap, 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/barcode_button.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unnecessary_cast 2 | import 'package:auto_size_text/auto_size_text.dart'; 3 | import 'package:barcode_widget/barcode_widget.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_elemental/flutter_elemental.dart'; 6 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 7 | import 'package:vouchervault/app/index.dart'; 8 | import 'package:vouchervault/lib/lib.dart'; 9 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 10 | 11 | part 'barcode_button.g.dart'; 12 | 13 | Option _barcodeWidget(Option type, Option code) => 14 | type.map2( 15 | code, 16 | (type, String code) => Padding( 17 | padding: EdgeInsets.symmetric(horizontal: AppTheme.rem(0.5)), 18 | child: SizedBox( 19 | height: AppTheme.rem(5), 20 | child: BarcodeWidget( 21 | data: code, 22 | barcode: type, 23 | errorBuilder: (context, err) => const Text('Code not valid'), 24 | ), 25 | ), 26 | ), 27 | ); 28 | 29 | Option _autoSizeText(String text) => 30 | optionOfString(text).map((text) => AutoSizeText( 31 | text, 32 | style: const TextStyle(fontSize: 40), 33 | maxLines: 1, 34 | ) as Widget); 35 | 36 | @swidget 37 | Widget _barcodeButton( 38 | BuildContext context, { 39 | required Option barcodeType, 40 | required String data, 41 | required void Function() onPressed, 42 | }) => 43 | TextButton( 44 | style: TextButton.styleFrom( 45 | backgroundColor: AppColors.lightGrey, 46 | foregroundColor: Colors.black, 47 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), 48 | minimumSize: Size.fromHeight(AppTheme.rem(7)), 49 | ), 50 | onPressed: onPressed, 51 | child: Center( 52 | child: _barcodeWidget(barcodeType, optionOfString(data)) 53 | .alt(() => _autoSizeText(data)) 54 | .getOrElse( 55 | () => Text(AppLocalizations.of(context)!.scanBarcode), 56 | ), 57 | ), 58 | ); 59 | -------------------------------------------------------------------------------- /lib/vouchers/menu/vouchers_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | part 'vouchers_menu.g.dart'; 6 | 7 | enum VouchersMenuAction { 8 | import, 9 | export, 10 | authentication(hasCheckbox: true), 11 | smartScan(hasCheckbox: true); 12 | 13 | const VouchersMenuAction({ 14 | this.hasCheckbox = false, 15 | }); 16 | 17 | String label(AppLocalizations locales) { 18 | switch (this) { 19 | case VouchersMenuAction.import: 20 | return locales.import; 21 | case VouchersMenuAction.export: 22 | return locales.export; 23 | case VouchersMenuAction.authentication: 24 | return locales.appLock; 25 | case VouchersMenuAction.smartScan: 26 | return locales.smartScan; 27 | } 28 | } 29 | 30 | final bool hasCheckbox; 31 | } 32 | 33 | @swidget 34 | Widget vouchersMenu({ 35 | required void Function(VouchersMenuAction) onSelected, 36 | required Map values, 37 | required Set disabled, 38 | }) => 39 | PopupMenuButton( 40 | onSelected: onSelected, 41 | itemBuilder: (context) => VouchersMenuAction.values 42 | .map>((a) => PopupMenuItem( 43 | value: a, 44 | enabled: !disabled.contains(a), 45 | child: Row( 46 | children: [ 47 | Text(a.label(AppLocalizations.of(context)!)), 48 | if (a.hasCheckbox) ...[ 49 | const Spacer(), 50 | Checkbox( 51 | value: values[a] ?? false, 52 | onChanged: disabled.contains(a) 53 | ? null 54 | : (_) { 55 | onSelected(a); 56 | Navigator.of(context).pop(); 57 | }, 58 | ), 59 | ] 60 | ], 61 | ), 62 | )) 63 | .toList(), 64 | ); 65 | -------------------------------------------------------------------------------- /test_driver/main_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:emulators/emulators.dart'; 2 | import 'package:flutter_driver/flutter_driver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | Future main() async { 6 | final driver = await FlutterDriver.connect(); 7 | final emu = await Emulators.build(); 8 | final screenshot = emu.screenshotHelper( 9 | androidPath: 10 | 'android/fastlane/metadata/android/en-US/images/phoneScreenshots', 11 | iosPath: 'ios/fastlane/screenshots/en-AU', 12 | ); 13 | 14 | setUpAll(() async { 15 | await driver.waitUntilFirstFrameRasterized(); 16 | await screenshot.cleanStatusBar(); 17 | await emu.toolchain.adb([ 18 | "-s", 19 | screenshot.device!.state.id, 20 | "shell", 21 | "pm", 22 | "grant", 23 | "co.timsmart.vouchervault", 24 | "android.permission.CAMERA", 25 | ]).string(); 26 | }); 27 | 28 | // Close the connection to the driver after the tests have completed. 29 | tearDownAll(() async { 30 | await driver.close(); 31 | }); 32 | 33 | group('Screenshots', () { 34 | test('home screen', () async { 35 | await driver.waitFor(find.text('Vouchers')); 36 | await screenshot.capture('01'); 37 | }); 38 | 39 | test('walmart', () async { 40 | await driver.tap(find.ancestor( 41 | of: find.text('Walmart'), 42 | matching: find.byType('VoucherItem'), 43 | )); 44 | await driver.waitUntilNoTransientCallbacks(); 45 | await screenshot.capture('02'); 46 | }); 47 | 48 | test('walmart spend', () async { 49 | await driver.tap(find.byValueKey('SpendIconButton')); 50 | await driver.waitUntilNoTransientCallbacks(); 51 | await screenshot.capture('03'); 52 | 53 | await driver.tap(find.text('Cancel')); 54 | await driver.waitUntilNoTransientCallbacks(); 55 | 56 | await driver.tap(find.text('Close')); 57 | await driver.waitUntilNoTransientCallbacks(); 58 | }); 59 | 60 | final buttonFinder = find.byType('FloatingActionButton'); 61 | test('form', () async { 62 | await driver.tap(buttonFinder); 63 | await driver.waitUntilNoTransientCallbacks(); 64 | await driver.tap(find.text('Cancel')); 65 | await driver.waitUntilNoTransientCallbacks(); 66 | await screenshot.capture('04'); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /lib/app/voucher_vault_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:dynamic_color/dynamic_color.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_elemental/flutter_elemental.dart'; 4 | import 'package:form_builder_validators/form_builder_validators.dart'; 5 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 6 | import 'package:vouchervault/app/index.dart'; 7 | import 'package:vouchervault/auth/index.dart'; 8 | import 'package:vouchervault/vouchers/index.dart'; 9 | import 'package:flutter_localizations/flutter_localizations.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | 12 | part 'voucher_vault_app.g.dart'; 13 | 14 | final routeObserver = RouteObserver(); 15 | 16 | @swidget 17 | Widget _voucherVaultApp( 18 | BuildContext context, { 19 | List initialValues = const [], 20 | }) => 21 | AtomScope( 22 | initialValues: initialValues, 23 | child: const _App(), 24 | ); 25 | 26 | @swidget 27 | Widget __app() { 28 | return DynamicColorBuilder(builder: (lightThemeSystem, darkThemeSystem) { 29 | final lightTheme = lightThemeSystem ?? 30 | ColorScheme.fromSeed( 31 | brightness: Brightness.light, 32 | seedColor: Colors.red, 33 | ); 34 | final darkTheme = darkThemeSystem ?? 35 | ColorScheme.fromSeed( 36 | brightness: Brightness.dark, 37 | seedColor: Colors.red, 38 | ); 39 | 40 | return AtomBuilder((context, watch, child) { 41 | final auth = watch(authState); 42 | 43 | return MaterialApp( 44 | debugShowCheckedModeBanner: false, 45 | theme: AppTheme.build(lightTheme), 46 | darkTheme: AppTheme.build(darkTheme), 47 | home: auth.when( 48 | unauthenticated: () => const AuthScreen(), 49 | authenticated: (_) => const VouchersScreen(), 50 | ), 51 | navigatorObservers: [routeObserver], 52 | localizationsDelegates: const [ 53 | AppLocalizations.delegate, 54 | GlobalMaterialLocalizations.delegate, 55 | GlobalWidgetsLocalizations.delegate, 56 | GlobalCupertinoLocalizations.delegate, 57 | FormBuilderLocalizations.delegate, 58 | ], 59 | supportedLocales: AppLocalizations.supportedLocales, 60 | ); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/lib/camera_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:camera/camera.dart'; 5 | import 'package:flutter_elemental/flutter_elemental.dart'; 6 | import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; 7 | import 'package:google_mlkit_entity_extraction/google_mlkit_entity_extraction.dart'; 8 | import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; 9 | import 'package:rxdart/rxdart.dart'; 10 | import 'package:vouchervault/lib/lib.dart'; 11 | 12 | typedef CameraControllerWithImage = (CameraController, CameraImage); 13 | 14 | Stream cameraImageStream( 15 | CameraController c, { 16 | Duration throttleTime = const Duration(milliseconds: 250), 17 | int skipFrames = 3, 18 | }) { 19 | late StreamController sc; 20 | 21 | Future onStart() => c.startImageStream((image) => sc.add((c, image))); 22 | 23 | sc = StreamController( 24 | onListen: onStart, 25 | onCancel: c.stopImageStream, 26 | sync: true, 27 | ); 28 | 29 | return sc.stream 30 | .throttleTime(throttleTime) 31 | .skip(skipFrames) 32 | .asBroadcastStream(); 33 | } 34 | 35 | Option inputImage( 36 | CameraImage image, { 37 | required CameraDescription camera, 38 | }) => 39 | Option.Do(($) { 40 | final rotation = $(Option.fromNullable( 41 | InputImageRotationValue.fromRawValue(camera.sensorOrientation), 42 | )); 43 | final format = $(Option.fromNullable( 44 | InputImageFormatValue.fromRawValue(image.format.raw), 45 | )); 46 | final plane = $(image.planes.head); 47 | return InputImage.fromBytes( 48 | bytes: plane.bytes, 49 | metadata: InputImageMetadata( 50 | format: format, 51 | rotation: rotation, 52 | size: Size(image.width.toDouble(), image.height.toDouble()), 53 | bytesPerRow: plane.bytesPerRow, 54 | ), 55 | ); 56 | }); 57 | 58 | final _neverController = StreamController.broadcast(sync: true); 59 | Stream neverStream() => _neverController.stream.cast(); 60 | 61 | final pickInputImage = pickImage.flatMap((i) => ZIO 62 | .fromNullable(i.path) 63 | .mapError((_) => 'pickInputImage: pickImage returned empty path') 64 | .map(InputImage.fromFilePath)); 65 | -------------------------------------------------------------------------------- /lib/hooks/use_route_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:flutter_hooks/flutter_hooks.dart'; 4 | 5 | class _RouteObserverHook extends Hook { 6 | const _RouteObserverHook( 7 | this.routeObserver, { 8 | this.didPopNext = const Option.none(), 9 | this.didPush = const Option.none(), 10 | this.didPop = const Option.none(), 11 | this.didPushNext = const Option.none(), 12 | List keys = const [], 13 | }) : super(keys: keys); 14 | 15 | final RouteObserver routeObserver; 16 | final Option didPopNext; 17 | final Option didPush; 18 | final Option didPop; 19 | final Option didPushNext; 20 | 21 | @override 22 | _RouteObserverHookState createState() => _RouteObserverHookState(); 23 | } 24 | 25 | class _RouteObserverHookState extends HookState 26 | implements RouteAware { 27 | ModalRoute? _route; 28 | 29 | @override 30 | void build(BuildContext context) { 31 | if (_route != null) return; 32 | 33 | _route = ModalRoute.of(context); 34 | if (_route != null) { 35 | hook.routeObserver.subscribe(this, _route!); 36 | } 37 | } 38 | 39 | @override 40 | void dispose() { 41 | if (_route != null) { 42 | hook.routeObserver.unsubscribe(this); 43 | } 44 | } 45 | 46 | @override 47 | void didPopNext() { 48 | hook.didPopNext.map((f) => f()); 49 | } 50 | 51 | @override 52 | void didPush() { 53 | hook.didPush.map((f) => f()); 54 | } 55 | 56 | @override 57 | void didPop() { 58 | hook.didPop.map((f) => f()); 59 | } 60 | 61 | @override 62 | void didPushNext() { 63 | hook.didPushNext.map((f) => f()); 64 | } 65 | } 66 | 67 | void useRouteObserver( 68 | RouteObserver routeObserver, { 69 | Option didPopNext = const Option.none(), 70 | Option didPush = const Option.none(), 71 | Option didPop = const Option.none(), 72 | Option didPushNext = const Option.none(), 73 | List keys = const [], 74 | }) { 75 | use(_RouteObserverHook( 76 | routeObserver, 77 | didPop: didPop, 78 | didPopNext: didPopNext, 79 | didPush: didPush, 80 | didPushNext: didPushNext, 81 | keys: keys, 82 | )); 83 | } 84 | -------------------------------------------------------------------------------- /lib/lib/files.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_elemental/flutter_elemental.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | EIO createFile(String filename) => EIO 8 | .tryCatch(getTemporaryDirectory, (err, s) => 'Failed to get tmp dir: $err') 9 | .map((d) => File('${d.path}/$filename')); 10 | 11 | EIO writeFile(String filename, List bytes) => 12 | createFile(filename).flatMapThrowable( 13 | (f) => f.writeAsBytes(bytes), 14 | (err, stackTrace) => 'Failed to write bytes: $err', 15 | ); 16 | 17 | EIO writeStringToFile( 18 | String filename, 19 | String content, 20 | ) => 21 | createFile(filename).flatMapThrowable( 22 | (f) => f.writeAsString(content), 23 | (err, stackTrace) => 'Failed to write string: $err', 24 | ); 25 | 26 | EIO)> _readPlatformFileStream( 27 | PlatformFile f, 28 | ) => 29 | EIO 30 | .tryCatch( 31 | () => f.readStream!.reduce((bytes, chunk) => bytes + chunk), 32 | (err, s) => 'Could not read file: $err', 33 | ) 34 | .map((bytes) => (f, bytes)); 35 | 36 | EIO)> pickFile({ 37 | FileType type = FileType.any, 38 | List? extensions, 39 | }) => 40 | EIO 41 | .tryCatch( 42 | () async { 43 | await FilePicker.platform.clearTemporaryFiles(); 44 | return FilePicker.platform.pickFiles( 45 | type: type, 46 | allowedExtensions: extensions, 47 | withReadStream: true, 48 | ); 49 | }, 50 | (err, s) => 'pickFiles failed: $err', 51 | ) 52 | .flatMapNullableOrFail(identity, (_) => 'pickFiles gave no result') 53 | .flatMap((r) => r.files.head.asZIO 54 | .mapError((_) => 'pickFiles had an empty response')) 55 | .flatMap(_readPlatformFileStream); 56 | 57 | final pickImage = EIO 58 | .tryCatch( 59 | () => FilePicker.platform.pickFiles(type: FileType.image), 60 | (err, s) => 'pickFiles failed: $err', 61 | ) 62 | .flatMapNullableOrFail(identity, (_) => 'file picker cancelled') 63 | .flatMap( 64 | (r) => 65 | r.files.head.asZIO.mapError((_) => 'pickFiles had an empty response'), 66 | ); 67 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/auth/service.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: depend_on_referenced_packages 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:local_auth/local_auth.dart'; 4 | import 'package:local_auth_android/local_auth_android.dart'; 5 | import 'package:local_auth_darwin/local_auth_darwin.dart'; 6 | import 'package:vouchervault/auth/index.dart'; 7 | 8 | class AuthService { 9 | AuthService({ 10 | required this.ref, 11 | required this.localAuth, 12 | }); 13 | 14 | final Ref ref; 15 | final LocalAuthentication localAuth; 16 | 17 | IO get toggle => 18 | ref.update((_) => _.enabled ? _.disable() : _.enable()); 19 | 20 | EIO get _cancel => EIO 21 | .tryCatch( 22 | () => localAuth.stopAuthentication(), 23 | (err, stackTrace) => 'Could not cancel previous auth requests', 24 | ) 25 | .asUnit; 26 | 27 | IO get authenticate => _cancel 28 | .zipRight(EIO.tryCatch( 29 | () => localAuth.authenticate( 30 | localizedReason: ' ', 31 | authMessages: const [ 32 | AndroidAuthMessages( 33 | signInTitle: 'Unlock your vouchers', 34 | biometricHint: '', 35 | ), 36 | IOSAuthMessages(), 37 | ], 38 | ), 39 | (err, _) => 'Error trying to authenticate: $err', 40 | )) 41 | .filterOrFail(identity, (_) => 'Authentication failed') 42 | .zipRight(ref.set(AuthState.success)) 43 | .ignoreLogged; 44 | } 45 | 46 | // === layer 47 | 48 | final authLayer = Layer.scoped(ZIO.Do(($, env) { 49 | final ref = $.sync(StorageRef.make( 50 | AuthState.notAvailable, 51 | key: 'pbs_AuthBloc', 52 | fromJson: (_) => AuthState.fromJson(_), 53 | toJson: (_) => _.toJson(), 54 | ).tap((_) => _.update((s) => s.init())).orDie); 55 | 56 | final localAuth = LocalAuthentication(); 57 | 58 | if (ref.unsafeGet().enabled) { 59 | return AuthService( 60 | ref: ref, 61 | localAuth: localAuth, 62 | ); 63 | } 64 | 65 | return $(ZIO, String, bool>.tryCatch( 66 | () => localAuth.isDeviceSupported(), 67 | (error, stackTrace) => 'Could not check if auth is available', 68 | ) 69 | .logOrElse((_) => false) 70 | .flatMap( 71 | (_) => ref.set(_ ? AuthState.notRequired : AuthState.notAvailable), 72 | ) 73 | .as(AuthService(ref: ref, localAuth: localAuth))); 74 | })); 75 | 76 | // ==== atoms 77 | 78 | final authState = zioRefAtomSync(authLayer.accessWith((_) => _.ref)); 79 | final authEnabledAtom = authState.select((s) => s.enabled); 80 | final authAvailableAtom = authState.select((s) => s.available); 81 | -------------------------------------------------------------------------------- /lib/shared/voucher_details/voucher_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 4 | import 'package:vouchervault/app/index.dart'; 5 | import 'package:vouchervault/lib/lib.dart'; 6 | import 'package:vouchervault/vouchers/models/voucher.dart'; 7 | import 'package:intl/intl.dart'; 8 | 9 | part 'voucher_details.g.dart'; 10 | 11 | final formatCurrency = NumberFormat.simpleCurrency(); 12 | 13 | List buildVoucherDetails( 14 | BuildContext context, 15 | Voucher voucher, { 16 | Option space = const Option.none(), 17 | bool includeNotes = false, 18 | }) => 19 | intersperse(SizedBox( 20 | height: space.getOrElse(() => AppTheme.space1), 21 | ))([ 22 | ...voucher.normalizedExpires.ifSomeList((dt) => [ 23 | _VoucherDetailRow( 24 | Icons.history, 25 | formatExpires(dt), 26 | ) 27 | ]), 28 | ...voucher.balanceDoubleOption.ifSomeList((b) => [ 29 | _VoucherDetailRow( 30 | Icons.account_balance, 31 | formatCurrency.format(b), 32 | ), 33 | ]), 34 | ...voucher.notesOption.filter((_) => includeNotes).ifSomeList((notes) => [ 35 | _VoucherDetailRow( 36 | Icons.article, 37 | notes, 38 | alignment: CrossAxisAlignment.start, 39 | iconPadding: true, 40 | selectable: true, 41 | ), 42 | ]), 43 | ]).toList(); 44 | 45 | @swidget 46 | Widget __voucherDetailRow( 47 | BuildContext context, 48 | IconData icon, 49 | String text, { 50 | CrossAxisAlignment alignment = CrossAxisAlignment.center, 51 | bool iconPadding = false, 52 | bool selectable = false, 53 | }) { 54 | final theme = Theme.of(context); 55 | 56 | return Row(crossAxisAlignment: alignment, children: [ 57 | Padding( 58 | padding: iconPadding 59 | ? EdgeInsets.only(top: AppTheme.rem(0.1)) 60 | : EdgeInsets.zero, 61 | child: Icon( 62 | icon, 63 | size: AppTheme.rem(1), 64 | color: theme.colorScheme.onPrimary, 65 | ), 66 | ), 67 | SizedBox(width: AppTheme.space2), 68 | selectable 69 | ? SelectableText( 70 | text, 71 | style: theme.textTheme.bodyMedium!.copyWith( 72 | color: theme.colorScheme.onPrimary, 73 | ), 74 | ) 75 | : Text( 76 | text, 77 | style: theme.textTheme.bodyMedium!.copyWith( 78 | color: theme.colorScheme.onPrimary, 79 | ), 80 | ), 81 | ]); 82 | } 83 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/barcode_scanner_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:barcode_widget/barcode_widget.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutter_elemental/flutter_elemental.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 7 | import 'package:vouchervault/app/index.dart'; 8 | import 'package:vouchervault/voucher_form/index.dart'; 9 | 10 | part 'barcode_scanner_field.g.dart'; 11 | 12 | @hwidget 13 | Widget _barcodeScannerField( 14 | BuildContext context, { 15 | required void Function(String) onChange, 16 | required String initialValue, 17 | required Option barcodeType, 18 | required String labelText, 19 | Option errorText = const Option.none(), 20 | required void Function(BarcodeResult r) onScan, 21 | bool launchScannerImmediately = false, 22 | }) { 23 | final theme = Theme.of(context); 24 | final controller = useTextEditingController(text: initialValue); 25 | final setText = useCallback( 26 | (String data) => controller.value = TextEditingValue( 27 | text: data, 28 | selection: TextSelection.fromPosition(TextPosition(offset: data.length)), 29 | ), 30 | [controller], 31 | ); 32 | 33 | final showDialog = useCallback(() { 34 | Navigator.of(context).push(MaterialPageRoute( 35 | builder: (context) => ScannerDialog( 36 | onScan: (r) { 37 | setText(r.barcode.rawValue!); 38 | onChange(r.barcode.rawValue!); 39 | onScan(r); 40 | Navigator.of(context).pop(); 41 | }, 42 | ), 43 | fullscreenDialog: true, 44 | )); 45 | }, [context, setText, onChange, onScan]); 46 | 47 | useEffect(() { 48 | if (launchScannerImmediately && initialValue.isEmpty) { 49 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 50 | showDialog(); 51 | }); 52 | } 53 | return null; 54 | }, []); 55 | 56 | return Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | children: [ 59 | BarcodeButton( 60 | barcodeType: barcodeType, 61 | data: initialValue, 62 | onPressed: showDialog, 63 | ), 64 | SizedBox(height: AppTheme.space3), 65 | TextField( 66 | controller: controller, 67 | decoration: InputDecoration( 68 | border: const OutlineInputBorder(), 69 | labelText: labelText, 70 | ), 71 | onChanged: onChange, 72 | ), 73 | ...errorText.match( 74 | () => [], 75 | (error) => [ 76 | SizedBox(height: AppTheme.space2), 77 | Text( 78 | error, 79 | style: TextStyle(color: theme.colorScheme.error), 80 | ), 81 | ], 82 | ), 83 | ], 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/vouchers/models/voucher.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voucher.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$VoucherImpl _$$VoucherImplFromJson(Map json) => 10 | _$VoucherImpl( 11 | uuid: json['uuid'] == null 12 | ? const Option.none() 13 | : Option.fromJson(json['uuid'], (value) => value as String), 14 | description: json['description'] as String? ?? '', 15 | code: json['code'] == null 16 | ? const Option.none() 17 | : Option.fromJson(json['code'], (value) => value as String), 18 | codeType: 19 | $enumDecodeNullable(_$VoucherCodeTypeEnumMap, json['codeType']) ?? 20 | VoucherCodeType.CODE128, 21 | expires: json['expires'] == null 22 | ? const Option.none() 23 | : Option.fromJson( 24 | json['expires'], (value) => DateTime.parse(value as String)), 25 | removeOnceExpired: json['removeOnceExpired'] as bool? ?? true, 26 | balance: json['balance'] == null 27 | ? const Option.none() 28 | : Option.fromJson( 29 | json['balance'], (value) => (value as num).toDouble()), 30 | balanceMilliunits: json['balanceMilliunits'] == null 31 | ? const Option.none() 32 | : Option.fromJson( 33 | json['balanceMilliunits'], (value) => (value as num).toInt()), 34 | notes: json['notes'] as String? ?? '', 35 | color: $enumDecodeNullable(_$VoucherColorEnumMap, json['color']) ?? 36 | VoucherColor.GREY, 37 | ); 38 | 39 | Map _$$VoucherImplToJson(_$VoucherImpl instance) => 40 | { 41 | 'uuid': instance.uuid.toJson( 42 | (value) => value, 43 | ), 44 | 'description': instance.description, 45 | 'code': instance.code.toJson( 46 | (value) => value, 47 | ), 48 | 'codeType': _$VoucherCodeTypeEnumMap[instance.codeType]!, 49 | 'expires': instance.expires.toJson( 50 | (value) => value.toIso8601String(), 51 | ), 52 | 'removeOnceExpired': instance.removeOnceExpired, 53 | 'balance': instance.balance.toJson( 54 | (value) => value, 55 | ), 56 | 'balanceMilliunits': instance.balanceMilliunits.toJson( 57 | (value) => value, 58 | ), 59 | 'notes': instance.notes, 60 | 'color': _$VoucherColorEnumMap[instance.color]!, 61 | }; 62 | 63 | const _$VoucherCodeTypeEnumMap = { 64 | VoucherCodeType.AZTEC: 'AZTEC', 65 | VoucherCodeType.CODE128: 'CODE128', 66 | VoucherCodeType.CODE39: 'CODE39', 67 | VoucherCodeType.EAN13: 'EAN13', 68 | VoucherCodeType.PDF417: 'PDF417', 69 | VoucherCodeType.QR: 'QR', 70 | VoucherCodeType.TEXT: 'TEXT', 71 | }; 72 | 73 | const _$VoucherColorEnumMap = { 74 | VoucherColor.GREY: 'GREY', 75 | VoucherColor.BLUE: 'BLUE', 76 | VoucherColor.GREEN: 'GREEN', 77 | VoucherColor.ORANGE: 'ORANGE', 78 | VoucherColor.PURPLE: 'PURPLE', 79 | VoucherColor.RED: 'RED', 80 | VoucherColor.YELLOW: 'YELLOW', 81 | }; 82 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/providers/camera.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:camera/camera.dart'; 5 | import 'package:flutter_elemental/flutter_elemental.dart' hide Logger; 6 | import 'package:logging/logging.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | import 'package:vouchervault/app/atoms.dart'; 9 | import 'package:vouchervault/voucher_form/index.dart'; 10 | 11 | final _log = Logger('barcode_scanner_field/providers/providers.dart'); 12 | final cameras = futureAtom((get) => availableCameras()); 13 | 14 | bool _isRearCamera(CameraDescription d) => 15 | d.lensDirection == CameraLensDirection.back; 16 | 17 | final cameraProvider = atom((get) => get(cameras).whenOrElse( 18 | data: (cameras) => cameras.where(_isRearCamera).head, 19 | orElse: () => const Option.none(), 20 | )).keepAlive(); 21 | 22 | final cameraPaused = stateAtom(false); 23 | 24 | final _cameraControllerProvider = atom((get) { 25 | final paused = get(cameraPaused); 26 | final camera = get(cameraProvider); 27 | 28 | return camera.filter((_) => !paused).map((camera) { 29 | final controller = CameraController( 30 | camera, 31 | ResolutionPreset.high, 32 | enableAudio: false, 33 | imageFormatGroup: Platform.isAndroid 34 | ? ImageFormatGroup.nv21 35 | : ImageFormatGroup.bgra8888, 36 | ); 37 | 38 | get.onDispose(() async { 39 | await controller.setFlashMode(FlashMode.off); 40 | 41 | if (controller.value.isStreamingImages) { 42 | try { 43 | await controller.stopImageStream(); 44 | } catch (_) {} 45 | } 46 | await controller.dispose(); 47 | }); 48 | 49 | return controller; 50 | }); 51 | }); 52 | 53 | final initializedCameraController = 54 | futureAtom((get) => get(_cameraControllerProvider).match( 55 | () => Future.any([]), 56 | (c) => c.initialize().then((_) => c), 57 | )); 58 | 59 | final imageProvider = atom((get) => get(initializedCameraController).whenOrElse( 60 | data: (_) => cameraImageStream(_), 61 | orElse: () => neverStream(), 62 | )); 63 | 64 | final barcodeResultProvider = atom((get) { 65 | final scanner = get(barcodeScannerAtom); 66 | final smartScan = get(appSettings.select((a) => a.smartScan)); 67 | 68 | return get(imageProvider) 69 | .exhaustMap( 70 | (t) => inputImage( 71 | t.$2, 72 | camera: t.$1.description, 73 | ).match>>( 74 | () => const Stream.empty(), 75 | (image) => Stream.fromFuture( 76 | scanner 77 | .extractAll(image, embellish: smartScan) 78 | .either 79 | .runFutureOrThrowRegistry(get.registry), 80 | ), 81 | ), 82 | ) 83 | .expand((_) => _.match( 84 | (left) { 85 | left.when( 86 | barcodeNotFound: () {}, 87 | pickerError: _log.info, 88 | mlkitError: _log.info, 89 | ); 90 | 91 | return const []; 92 | }, 93 | (r) => [r], 94 | )) 95 | .asBroadcastStream(); 96 | }); 97 | -------------------------------------------------------------------------------- /lib/voucher_form/widgets/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_elemental/flutter_elemental.dart'; 3 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 4 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 5 | import 'package:vouchervault/app/index.dart'; 6 | import 'package:vouchervault/shared/scaffold/app_scaffold.dart'; 7 | import 'package:vouchervault/voucher_form/index.dart'; 8 | import 'package:vouchervault/vouchers/index.dart'; 9 | import 'package:flutter_hooks/flutter_hooks.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | 12 | part 'dialog.g.dart'; 13 | 14 | @hwidget 15 | Widget _voucherFormDialog( 16 | BuildContext context, { 17 | Option initialValue = const Option.none(), 18 | }) { 19 | final theme = Theme.of(context); 20 | final formKey = useMemoized(() => GlobalKey()); 21 | final title = initialValue.match( 22 | () => AppLocalizations.of(context)!.addVoucher, 23 | (_) => AppLocalizations.of(context)!.editVoucher, 24 | ); 25 | final action = initialValue.match( 26 | () => AppLocalizations.of(context)!.create, 27 | (_) => AppLocalizations.of(context)!.update, 28 | ); 29 | 30 | return AppScaffold( 31 | leading: true, 32 | title: title, 33 | slivers: [ 34 | SliverPadding( 35 | padding: EdgeInsets.symmetric( 36 | horizontal: AppTheme.space4, 37 | ), 38 | sliver: SliverToBoxAdapter( 39 | child: VoucherForm( 40 | formKey: formKey, 41 | initialValue: initialValue.getOrElse(() => Voucher()), 42 | ), 43 | ), 44 | ), 45 | SliverSafeArea( 46 | bottom: true, 47 | top: false, 48 | sliver: SliverPadding( 49 | padding: EdgeInsets.only( 50 | bottom: AppTheme.space5, 51 | left: AppTheme.space4, 52 | right: AppTheme.space4, 53 | top: AppTheme.space3, 54 | ), 55 | sliver: SliverToBoxAdapter( 56 | child: ElevatedButton( 57 | style: ElevatedButton.styleFrom( 58 | foregroundColor: theme.colorScheme.onPrimary, 59 | backgroundColor: theme.colorScheme.primary, 60 | padding: EdgeInsets.symmetric( 61 | horizontal: AppTheme.space3, 62 | vertical: AppTheme.space3, 63 | ), 64 | textStyle: theme.textTheme.bodyLarge!.copyWith( 65 | fontWeight: FontWeight.w600, 66 | ), 67 | ), 68 | child: Text(action), 69 | onPressed: () { 70 | if (formKey.currentState!.saveAndValidate()) { 71 | final voucher = Voucher.fromFormValue( 72 | formKey.currentState!.value, 73 | ); 74 | 75 | Navigator.pop( 76 | context, 77 | initialValue.match( 78 | () => voucher, 79 | (iv) => voucher.copyWith(uuid: iv.uuid), 80 | ), 81 | ); 82 | } 83 | }, 84 | ), 85 | ), 86 | ), 87 | ), 88 | ], 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /lib/vouchers/list/voucher_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 3 | import 'package:vouchervault/app/index.dart'; 4 | import 'package:vouchervault/vouchers/index.dart' as V; 5 | import 'package:vouchervault/vouchers/index.dart' show Voucher; 6 | import 'package:vouchervault/shared/voucher_details/voucher_details.dart'; 7 | 8 | part 'voucher_item.g.dart'; 9 | 10 | const kVoucherItemBorderRadius = 15.0; 11 | 12 | @swidget 13 | Widget voucherItem( 14 | BuildContext context, { 15 | required V.Voucher voucher, 16 | required VoidCallback onPressed, 17 | double bottomPadding = 0, 18 | }) { 19 | final theme = voucher.color.theme(Theme.of(context)); 20 | 21 | return Theme( 22 | data: theme, 23 | child: Stack( 24 | clipBehavior: Clip.none, 25 | children: [ 26 | Positioned( 27 | bottom: -bottomPadding, 28 | left: 0, 29 | right: 0, 30 | top: 0, 31 | child: Container( 32 | decoration: BoxDecoration( 33 | borderRadius: BorderRadius.circular(kVoucherItemBorderRadius), 34 | boxShadow: [ 35 | BoxShadow( 36 | color: Colors.black.withAlpha(150), 37 | offset: const Offset(0, 7), 38 | blurRadius: 20, 39 | ), 40 | ], 41 | ), 42 | child: ElevatedButton( 43 | style: ElevatedButton.styleFrom( 44 | padding: EdgeInsets.zero, 45 | shape: RoundedRectangleBorder( 46 | borderRadius: bottomPadding == 0 47 | ? BorderRadius.circular(kVoucherItemBorderRadius) 48 | : const BorderRadius.vertical( 49 | top: Radius.circular(kVoucherItemBorderRadius), 50 | ), 51 | ), 52 | backgroundColor: theme.colorScheme.primary, 53 | foregroundColor: theme.colorScheme.onPrimary, 54 | ).copyWith( 55 | elevation: WidgetStateProperty.all(0), 56 | ), 57 | onPressed: onPressed, 58 | child: Container(), 59 | ), 60 | ), 61 | ), 62 | IgnorePointer( 63 | child: Padding( 64 | padding: EdgeInsets.only( 65 | left: AppTheme.space4, 66 | right: AppTheme.space4, 67 | top: AppTheme.space4, 68 | bottom: AppTheme.space4, 69 | ), 70 | child: Column( 71 | crossAxisAlignment: CrossAxisAlignment.start, 72 | children: [ 73 | Text( 74 | voucher.description, 75 | style: theme.textTheme.titleMedium!.copyWith( 76 | color: theme.colorScheme.onPrimary, 77 | fontWeight: FontWeight.w600, 78 | ), 79 | ), 80 | if (voucher.hasDetails) ...[ 81 | SizedBox(height: AppTheme.space2), 82 | ...buildVoucherDetails( 83 | context, 84 | voucher, 85 | ), 86 | ] 87 | ], 88 | ), 89 | ), 90 | ), 91 | ], 92 | ), 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /lib/vouchers/dialog/voucher_dialog_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_elemental/flutter_elemental.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:fluttertoast/fluttertoast.dart'; 7 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 8 | import 'package:vouchervault/app/index.dart'; 9 | import 'package:vouchervault/hooks/index.dart'; 10 | import 'package:vouchervault/voucher_form/index.dart'; 11 | import 'package:vouchervault/vouchers/index.dart'; 12 | 13 | part 'voucher_dialog_container.g.dart'; 14 | 15 | @hwidget 16 | Widget _voucherDialogContainer( 17 | BuildContext context, { 18 | required Voucher voucher, 19 | }) { 20 | // Full brightness unless text barcode 21 | useFullBrightness( 22 | routeObserver, 23 | enabled: voucher.codeType != VoucherCodeType.TEXT, 24 | ); 25 | 26 | // state 27 | final v = useAtom(voucherAtom(voucher.uuid)).getOrElse(() => voucher); 28 | 29 | final onTapBarcode = useCallback( 30 | () => v.code.map((code) { 31 | Clipboard.setData(ClipboardData(text: code)); 32 | Fluttertoast.showToast( 33 | msg: AppLocalizations.of(context)!.copiedToClipboard); 34 | }), 35 | [v.code], 36 | ); 37 | 38 | final onSpend = useZIO( 39 | _showSpendDialog(context).flatMap( 40 | (amountInput) => vouchersLayer 41 | .accessWithZIO((_) => _.maybeUpdateBalance(v, amountInput)), 42 | ), 43 | [v], 44 | ); 45 | 46 | final onEdit = useZIO( 47 | Navigator.of(context) 48 | .pushIONoContext(MaterialPageRoute( 49 | fullscreenDialog: true, 50 | builder: (context) => VoucherFormDialog(initialValue: Option.of(v)), 51 | )) 52 | .flatMap((v) => vouchersLayer.accessWithZIO((_) => _.update(v))), 53 | [v], 54 | ); 55 | 56 | final onRemove = useZIO( 57 | _showRemoveDialog(context, onPressed: (context) { 58 | vouchersLayer.accessWithZIO((_) => _.remove(v)).runContext(context); 59 | Navigator.pop(context, true); 60 | }).filter(identity).zipLeft(ZIO(Navigator.of(context).pop)), 61 | [v], 62 | ); 63 | 64 | return VoucherDialog( 65 | voucher: v, 66 | onTapBarcode: onTapBarcode, 67 | onEdit: onEdit, 68 | onClose: () => Navigator.pop(context), 69 | onRemove: onRemove, 70 | onSpend: onSpend, 71 | ); 72 | } 73 | 74 | IOOption _showSpendDialog(BuildContext context) => ZIO.tryCatchNullable( 75 | () => showDialog( 76 | context: context, 77 | builder: (context) => const VoucherSpendDialog(), 78 | ), 79 | ); 80 | 81 | IOOption _showRemoveDialog( 82 | BuildContext context, { 83 | required void Function(BuildContext) onPressed, 84 | }) => 85 | ZIO.tryCatchNullable( 86 | () => showDialog( 87 | context: context, 88 | builder: (context) => AlertDialog( 89 | title: Text(AppLocalizations.of(context)!.areYouSure), 90 | content: Text(AppLocalizations.of(context)!.confirmRemoveVoucher), 91 | actions: [ 92 | TextButton( 93 | onPressed: () => Navigator.pop(context, false), 94 | child: Text(AppLocalizations.of(context)!.cancel), 95 | ), 96 | TextButton( 97 | onPressed: () => onPressed(context), 98 | child: Text(AppLocalizations.of(context)!.remove), 99 | ), 100 | ], 101 | ), 102 | ), 103 | ); 104 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/vouchers/service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_date/dart_date.dart'; 5 | import 'package:flutter_elemental/flutter_elemental.dart'; 6 | import 'package:share/share.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | import 'package:vouchervault/lib/lib.dart'; 9 | import 'package:vouchervault/vouchers/index.dart'; 10 | 11 | final vouchersLayer = Layer.scoped( 12 | StorageRef.make( 13 | VouchersState(IList()), 14 | key: 'pbs_VouchersBloc', 15 | fromJson: VouchersState.fromJson, 16 | toJson: (a) => a.toJson(), 17 | ) 18 | .map((_) => VouchersService(ref: _)) 19 | .tap((_) => _.removeExpired.lift()) 20 | .orDie, 21 | ); 22 | 23 | final vouchersState = zioRefAtomSync(vouchersLayer.accessWith((_) => _.ref)); 24 | 25 | final vouchersAtom = vouchersState.select((_) => _.sortedVouchers); 26 | 27 | final voucherAtom = atomFamily( 28 | (Option uuid) => 29 | vouchersState.select((s) => s.vouchers.where((v) => v.uuid == uuid).head), 30 | ); 31 | 32 | class VouchersService { 33 | const VouchersService({required this.ref}); 34 | 35 | final Ref ref; 36 | final uuid = const Uuid(); 37 | 38 | IO get removeExpired => 39 | ref.update((_) => _.copyWith(vouchers: _removeExpired(_.vouchers))); 40 | 41 | IO create(Voucher voucher) => ref.update((_) => _.copyWith( 42 | vouchers: _.vouchers.add(voucher.copyWith( 43 | uuid: Option.of(uuid.v4()), 44 | )), 45 | )); 46 | 47 | IO update(Voucher voucher) => ref.update((_) => _.copyWith( 48 | vouchers: _.vouchers.updateById([voucher], (v) => v.uuid), 49 | )); 50 | 51 | IO remove(Voucher voucher) => ref.update((_) => _.copyWith( 52 | vouchers: _.vouchers.removeWhere((v) => v.uuid == voucher.uuid), 53 | )); 54 | 55 | IO maybeUpdateBalance(Voucher voucher, String input) => 56 | _newBalance(voucher, input).match( 57 | () => ZIO.unit(), 58 | (balance) => update(voucher.copyWith( 59 | balanceMilliunits: Option.of(balance), 60 | )), 61 | ); 62 | 63 | IO get import => _importFromFiles.tap(ref.set).ignoreLogged; 64 | 65 | IO get export => ref 66 | .get() 67 | .flatMap((_) => _writeAndShareState('vouchervault.json', _)) 68 | .ignoreLogged; 69 | } 70 | 71 | // === Helpers === 72 | 73 | IList _removeExpired(IList vouchers) => vouchers.removeWhere( 74 | (v) => v.removeAt.filter((expires) => expires.isPast).isSome(), 75 | ); 76 | 77 | Option _newBalance(Voucher v, String s) => optionOfString(s) 78 | .flatMap(millisFromString) 79 | .map2(v.balanceOption, (amount, int balance) => balance - amount); 80 | 81 | // == Import and replace vouchers 82 | final _importFromFiles = pickFile() 83 | .map((r) => String.fromCharCodes(r.$2)) 84 | .flatMapThrowable( 85 | jsonDecode, 86 | (err, s) => 'Could not parse import JSON: $err', 87 | ) 88 | .flatMapNullableOrFail(identity, (_) => 'Import was null') 89 | .flatMapThrowable( 90 | VouchersState.fromJson, 91 | (err, stack) => 'Could not convert json to VouchersState: $err', 92 | ); 93 | 94 | // == Export vouchers to JSON file 95 | final _writeStateToFile = (String fileName, VouchersState value) => EIO 96 | .tryCatch( 97 | () => jsonEncode(value.toJson()), 98 | (err, stack) => 'encode json error: $err', 99 | ) 100 | .flatMap((_) => writeStringToFile(fileName, _)); 101 | 102 | EIO _shareFile(File file) => EIO 103 | .tryCatch( 104 | () => Share.shareFiles( 105 | [file.path], 106 | subject: 'VoucherVault export', 107 | ), 108 | (err, stackTrace) => 'share files error: $err', 109 | ) 110 | .asUnit; 111 | 112 | EIO _writeAndShareState(String fileName, VouchersState state) => 113 | _writeStateToFile(fileName, state).flatMap(_shareFile); 114 | -------------------------------------------------------------------------------- /lib/vouchers/models/voucher.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'package:dart_date/dart_date.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_elemental/flutter_elemental.dart'; 6 | import 'package:freezed_annotation/freezed_annotation.dart'; 7 | import 'package:vouchervault/lib/lib.dart'; 8 | 9 | part 'voucher.freezed.dart'; 10 | part 'voucher.g.dart'; 11 | 12 | enum VoucherCodeType { 13 | AZTEC(label: 'Aztec', square: true), 14 | CODE128(label: 'Code128'), 15 | CODE39(label: 'Code39'), 16 | EAN13(label: 'EAN-13'), 17 | PDF417(label: 'PDF417'), 18 | QR(label: 'QR Code', square: true), 19 | TEXT(label: 'Text'); 20 | 21 | final String label; 22 | final bool square; 23 | 24 | const VoucherCodeType({ 25 | required this.label, 26 | this.square = false, 27 | }); 28 | } 29 | 30 | enum VoucherColor { 31 | GREY(color: Color(0xFF616161)), 32 | BLUE(color: Colors.blue), 33 | GREEN(color: Color(0xFF43A047)), 34 | ORANGE(color: Color(0xFFEF6C00)), 35 | PURPLE(color: Colors.purple), 36 | RED(color: Color(0xFFD32F2F)), 37 | YELLOW(color: Color(0xFFFBC02D)); 38 | 39 | final Color color; 40 | 41 | const VoucherColor({ 42 | required this.color, 43 | }); 44 | 45 | ThemeData theme(ThemeData theme) { 46 | return theme.copyWith( 47 | colorScheme: ColorScheme.fromSeed( 48 | brightness: theme.brightness, 49 | seedColor: color, 50 | primary: color, 51 | onPrimary: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | String colorToJson(VoucherColor c) => _$VoucherColorEnumMap[c]!; 58 | 59 | // Voucher code type functions 60 | String codeTypeToJson(VoucherCodeType c) => _$VoucherCodeTypeEnumMap[c]!; 61 | 62 | final codeTypeFromJson = (String? json) => Option.fromNullable(json) 63 | .flatMap( 64 | (t) => Option.tryCatch(() => $enumDecode(_$VoucherCodeTypeEnumMap, t)), 65 | ) 66 | .getOrElse(() => VoucherCodeType.CODE128); 67 | 68 | @freezed 69 | class Voucher with _$Voucher { 70 | Voucher._(); 71 | 72 | factory Voucher({ 73 | @Default(Option.none()) Option uuid, 74 | @Default('') String description, 75 | @Default(Option.none()) Option code, 76 | @Default(VoucherCodeType.CODE128) VoucherCodeType codeType, 77 | @Default(Option.none()) Option expires, 78 | @Default(true) bool removeOnceExpired, 79 | @Default(Option.none()) Option balance, 80 | @Default(Option.none()) Option balanceMilliunits, 81 | @Default('') String notes, 82 | @Default(VoucherColor.GREY) VoucherColor color, 83 | }) = _Voucher; 84 | 85 | factory Voucher.fromJson(dynamic json) => 86 | _$VoucherFromJson(Map.from(json)); 87 | 88 | late final Option normalizedExpires = 89 | expires.map((d) => d.endOfDay); 90 | 91 | late final Option removeAt = 92 | normalizedExpires.filter((_) => removeOnceExpired); 93 | 94 | late final Option balanceOption = 95 | balanceMilliunits.alt(() => balance.map(millisFromDouble)); 96 | 97 | late final Option balanceDoubleOption = 98 | balanceOption.map(millisToDouble); 99 | 100 | late final Option notesOption = optionOfString(notes); 101 | 102 | late final bool hasDetails = 103 | normalizedExpires.isSome() || balanceOption.isSome(); 104 | late final bool hasDetailsOrNotes = hasDetails || notesOption.isSome(); 105 | 106 | static Voucher fromFormValue(dynamic json) => 107 | Voucher.fromJson({ 108 | ...json, 109 | 'balanceMilliunits': 110 | millisFromString(json['balanceMilliunits']).toNullable(), 111 | }); 112 | 113 | dynamic toFormValue() => { 114 | ...toJson(), 115 | 'balanceMilliunits': balanceOption.map(millisToString).toNullable(), 116 | 'codeType': _$VoucherCodeTypeEnumMap[codeType], 117 | 'expires': expires.toNullable(), 118 | 'color': _$VoucherColorEnumMap[color], 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | Tim Smart built the Voucher Vault app as an Open Source app. This SERVICE is provided by Tim Smart at no cost and is intended for use as is. 4 | 5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. 6 | 7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. 8 | 9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which are accessible at Voucher Vault unless otherwise defined in this Privacy Policy. 10 | 11 | **Information Collection and Use** 12 | 13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way. 14 | 15 | The app does use third-party services that may collect information used to identify you. 16 | 17 | Link to the privacy policy of third-party service providers used by the app 18 | 19 | - [Google Play Services](https://www.google.com/policies/privacy/) 20 | 21 | **Log Data** 22 | 23 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. 24 | 25 | **Cookies** 26 | 27 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. 28 | 29 | This Service does not use these “cookies” explicitly. However, the app may use third-party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. 30 | 31 | **Service Providers** 32 | 33 | I may employ third-party companies and individuals due to the following reasons: 34 | 35 | - To facilitate our Service; 36 | - To provide the Service on our behalf; 37 | - To perform Service-related services; or 38 | - To assist us in analyzing how our Service is used. 39 | 40 | I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. 41 | 42 | **Security** 43 | 44 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. 45 | 46 | **Children’s Privacy** 47 | 48 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions. 49 | 50 | **Changes to This Privacy Policy** 51 | 52 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. 53 | 54 | This policy is effective as of 2022-05-25 55 | 56 | **Contact Us** 57 | 58 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at hello@timsmart.co. 59 | -------------------------------------------------------------------------------- /secrets/fastlane-google-key: -------------------------------------------------------------------------------- 1 | { 2 | "data": "ENC[AES256_GCM,data:BcwzHqg71FOoCmuxPNpXA2r7ai8+Qm7soheo9vs8PWaOtF8W6jc68amCUkPZ1hW6zsCW7v9/LzfDZheO90+CTtMPCy3mkPQr4YdnH+oYLePY/U60fDcNUbC6ogtrG0HGW202+YQoYmr25JrKOT0HyGP4ZZ9zXvQ3R+3McywrVvgjRwoq4QufCC8L1ZdcvOAqcZBZvIYA557XYgXpdtMw0wPWrmojRDKiMDK1yd6qrGxNocWmniV3caVbiVRLLRYYFJmRtWJ3tOYHvvkKK25w6PPjIh/ZVwg5yVSTPDb+M4rKv2aAlNb+ZQ43fYspCKmNVLvpv1ejKLoA7AT7ShBPl4Bcm+cXZWrcL9Msww9LUmB659KomrMVQ6HhPUqJecA5QCCe3YnL5u8Xcean7cBw50DZgl9FSrP69GbdzG2mdMa40V3/j2uxJRWgNjZ973C6VAHJhytm6g8zOXHUhiruZg5lzIkjnwUsUpVjImGP63PA+wJHmCHxV8NsIp8gpvLoC7LOW2Q7+Num+vemZGuOX14aBGPilwWYia2bT+Cnl5P/6laENo3xzlfyLJQNHNO2WOaYxssShKmf9rG4IcvSkPfxJdKWYURnwSGJP+QhmEZmTUb/ZFEinYoUuMZxaAO4dGSi/LNNsWh0Eaj42elprABR1Yh0M6cdJMvgM296GyMXgCqXnz4L4wAEj1vcyi/hrvilVDPtt+TEC28u6VBgXAoyRoV7rV7qfP06m5Y7/zouW/LQjlWEPdxSty/uvuam4yQ0hTxTFkQrHeSprb5ROMXpiCFlW9ZaYKMpa7FQb603ff+8BcdTApADmYAVtjlHbvrLFXgZA820zb8rufaxGsLIWst0QgOKGxOpriFURCgT3ee5apQm4DEx0KKRMAPVaXnTL00mOzK3E4M1T+QXCY/A05XM1RveG1P5OwqsK0t+qP2+q29iJqvnIju60zk0Twdm0uz9h8FXYx2NV0EjwSH/PB5jrhZFZlCYLamIV1nkfsqnP/K8NGGEgmz+RP6OMUSLvSd3KAKo22EkT91YCvUtJxIn6/NGttb2bhIdzqpPEXLXNcf/qqZoOshy2Iq3lrtGhK5VpoIveH7lDAMkiAzbSoWIHV/a1i3TU5qI/NblD1zBbEanV0UCY/kND7f/F4yvXxLBe/edCxlOCOFavYUzkBI/DNGtYE0m4tix3W9IAiAOgSszLr0Dp6ESJ2/j/9yshAtsj6eMR2jDlfHeFVQgRM4Avx4MhPw9LfjrBHMc+CE4CPPxmWdXO68H8MAtOw41OAiAgolrZohqonUb3Lx1Tn6sQ9IzEoYvA7oY8cNj+aUWYaSaQMtHN0h40WLp6yUd+7QI5+PXv9XDIO/0HJCKBfIInH3Ffbwv5cLTJVGURCfWyHWilgDsgYb/CDsELHVcy9Rol4ud4yislkDy03dwjHvWeYcecZeZAj7kldCsFLtkZMRu1Q6INBsoXt2dWkRfK4tEMPN/qj84QoO1VmajHE53AHqatjqhij37MSkoGVGD8Emt7LIJ8f5oW6esRpfhRY8UZUOIIJIJHGfqknhH6DxDWB7+Iak0zkOr6xo9KZ/PSEgH6Ce1xgXdpTdWu/arDFPVZNlIU7udVmgi7vSA+YistiylIIui5aEaIgMfsg6MC7JmCwuk3GTmuVxoyNDi7+wgx71omB1ZSdMGwmWnUBbAiS7me4q+jpPAALlfw9osZ0KaCOCS3uRc40i6EYYsMS78VroxLrU63eRTpyclZVdN/DdJA/gAgLggfg3HTv5CsaZPM15MCT5fPglsLSSpuiiXi1yXggJjZhHw88lEGIhGsYVbaRJG6V4MQlGjTmmFOzQ1uVFs9ZBQF2DJirc13MKrxu6UiHZ5Uos3l3YNoHBNa0rjv2tM46Qp42/kPxSoxVdpwxUqq4TqpvQR4qhr0TGcRS2W+QJtOY3v1zPNntYWyAP8E+by51l3epPWlQs5abkvc1DMGZK6VI9bvCIe/HhsMtA56GYzsDDmaHLe9KztD3+eNkglWKf6nTD0SCPSx26XkMhC+mO2Yj7KNL15SW33Sn8zYqh6Eec0QYyn23Yii/3xkZaWN4N4XT6IuGof8sUIfmfbOan3iAeBpd9HQneJnosvwUSh6JM1z7Ny7mM5z3cUdF9rmsFbDsnmIRQv+drfsL/8bI8oZritNYEleBmQ6cOniBfsweskBK7qo8Y78dblKI79RnTHEtHaTnuPG2FIGmaZCEJPMN5dzceWFCrcFpbZOnelaYkSmIm/94eUY4vuRLUKmA7EJBGmdN7X3ZlNHxT/qFK8cjRAAtUzeUVKV4mWufIyZtG1zGnD3Q1hgrr/JpQKc9rhscBBChFVJXwoc14e8sK2eNEWclEtwKZ3GwxipQLto/M2LmWIz7kkHGdDbCueSyLQ1YzUkHXtwMZVdvT2sdk5wXEAf4J6FEFL5enf4sPFer9dOad/ZbwCpp8NLHChWt42X7Qe1bdr99ixrsocGzW5CG8vWzpe56F1kVtcAE7LlJNV6bkvToyMoKvvu/fK1q+P922W3VzGHtGjGHHOCpEGRxGg9YjaNTrqhWp0FBSHhgfLimkAVB8JVPsvQR9fJHR4vF+KWZBqjfH0TpNIZf2BWBPxmN0YZCmMLTHNEfBGnTzISts4XVsa/5kK/fyen5tlhVIxEfiOn93YRveDbVZyCuNRhenMf1JmCqbSBnQdHnGrXtVv3gX3/Jjm2W6O2rkg3egF3vwNXruITDiXACMx3D3ot06zJpDPn3qsVi6CSuvNKgntsPysGU/kjbiryUHYK7HyCTgeM7EpustSpD2Oivl9r2rOQZTa3bJ7nLt4h/S3p127Knw1DEXQgI/IcAAVEM78leTvayRScMaszAN2AS1jiydcOyFfJ64KDmvjFF2GLhbV2qinjqTvBfFdaP3n0T5yJgqXrvax/5XENQ2ozUGJ7hkASyu+MZOBV9hBsYj9l5saYkSvetcizk7TNWixYn/QLKw8TEh9x8KbR5CPUFEfvBd5fuhk5BkdxK/dQC0cWzknuQFqa1Y94jjMxZ5CAPr+rYl43gnt4oyX/uaQnuqkfgOyfPm2I2kNjTJFdB//4+o11ycYEglVcqhoLbABIglyIjSE0+to2A6CkIf0EKKm4UUdOA9cA8DltpvYeW65zD8GO0E+/4PTePkjwxtrntb9idLkCtjmr6vrCAnR16w+,iv:OL7b7xxQxoDL3rYQ6cBajulEy4tgswrXGfLRCGDh2lU=,tag:XZsdbL6fSHhQJuKAWKEttA==,type:str]", 3 | "sops": { 4 | "kms": null, 5 | "gcp_kms": null, 6 | "azure_kv": null, 7 | "hc_vault": null, 8 | "age": [ 9 | { 10 | "recipient": "age1cqrnyaj8tcu6svafggn5f45juq5lnvghqlqsazvp5pvzt5wa2els9ak7pv", 11 | "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqNjZWWElmR05SdytOdHFK\nTk9zYzZqOHE5bklDK3hPQWFRNENXaWdNdlZRCkhEZkRIblhnNWt0bzVVZHdMek1O\ndzloY2xZMjAyWXVQakh6T0gwRGs1WWcKLS0tIC9hQnpmVGNPYW41ZWowWVNmZXd6\nTmp5TmM4V1ZkL1luLzdJVDZUVGY4dmcKC0UYPnzalLoWaHrrm4LXC65Y0T/hN8Zi\nD1biodER0lbrgT8sCIo4+Fuv4+r19CVehVlk/5qCL26xBVyfM67Xsw==\n-----END AGE ENCRYPTED FILE-----\n" 12 | } 13 | ], 14 | "lastmodified": "2024-08-05T10:13:48Z", 15 | "mac": "ENC[AES256_GCM,data:kEzy1Azd+MnlZ/H+2iF8Qxo31e+Wl4WUgCzFbHkqvJ9Nt6gmxWqdicMBrEFn4xXt0b5Siu2nDW8WYtgX05cuvVU5i2d9qcYCvkFdQ5Tk9ai9NC2D4NQlz/8RUYfzbh6kJRlPgDAIsWIhtqYTii12onSO7LHM9EDOIvY2Wdizwjk=,iv:3jxJnpEh9jHIK1BsdmzU/jL7pS6H2u7NrGTso+Fc3NM=,tag:nqW7n7GPXOQuXiWJiH05/g==,type:str]", 16 | "pgp": null, 17 | "unencrypted_suffix": "_unencrypted", 18 | "version": "3.9.0" 19 | } 20 | } -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_elemental/flutter_elemental.dart'; 2 | import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; 3 | import 'package:google_mlkit_entity_extraction/google_mlkit_entity_extraction.dart'; 4 | import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; 5 | import 'package:vouchervault/voucher_form/index.dart'; 6 | 7 | typedef BarcodeIO = EIO; 8 | 9 | class BarcodeScannerService { 10 | const BarcodeScannerService({ 11 | required this.textRecognizer, 12 | required this.barcodeScanner, 13 | required this.entityExtractor, 14 | }); 15 | 16 | final TextRecognizer textRecognizer; 17 | final BarcodeScanner barcodeScanner; 18 | final EntityExtractor entityExtractor; 19 | 20 | BarcodeIO> scan(InputImage image) => EIO 21 | .tryCatch( 22 | () => barcodeScanner.processImage(image), 23 | (err, stackTrace) => MlError.mlkitError(op: 'scan', err: err), 24 | ) 25 | .map((_) => _.toIList()); 26 | 27 | BarcodeIO ocr(InputImage image) => EIO.tryCatch( 28 | () => textRecognizer.processImage(image), 29 | (error, stackTrace) => MlError.mlkitError(op: 'ocr', err: error), 30 | ); 31 | 32 | BarcodeIO> extractEntities( 33 | String text, { 34 | List filter = const [], 35 | }) => 36 | EIO 37 | .tryCatch( 38 | () => entityExtractor.annotateText(text, entityTypesFilter: filter), 39 | (error, stackTrace) => MlError.mlkitError( 40 | op: 'extractEntities', 41 | err: error, 42 | ), 43 | ) 44 | .map((_) => _.toIList()); 45 | 46 | BarcodeIO extractAll( 47 | InputImage image, { 48 | bool embellish = false, 49 | }) => 50 | ZIO 51 | .logDebug( 52 | "extractAll", 53 | annotations: {'embellish': embellish}, 54 | ) 55 | .zipRight(scan(image)) 56 | .flatMap( 57 | (_) => _.firstOption 58 | .map((t) => BarcodeResult(barcode: t)) 59 | .asZIO 60 | .mapError((_) => const MlError.barcodeNotFound()), 61 | ) 62 | .flatMap( 63 | (_) => embellish 64 | ? _embellishResult(image: image, result: _) 65 | : ZIO.succeed(_), 66 | ) 67 | .tap( 68 | (_) => ZIO.logDebug("extractAll", annotations: {'result': _}), 69 | ); 70 | 71 | BarcodeIO _embellishResult({ 72 | required InputImage image, 73 | required BarcodeResult result, 74 | }) => 75 | ocr(image) 76 | .flatMap2( 77 | (_) => extractEntities(_.text, filter: [ 78 | EntityType.money, 79 | EntityType.dateTime, 80 | ]), 81 | ) 82 | .map( 83 | (_) => result.copyWith( 84 | merchant: extractMerchant(_.$1), 85 | balance: extractBalance(_.$2), 86 | expires: extractExpires(_.$1, _.$2), 87 | ), 88 | ); 89 | 90 | BarcodeIO extractAllFromFile(bool embellish) => pickInputImage 91 | .mapError(MlError.pickerError) 92 | .flatMap((_) => extractAll(_, embellish: embellish)); 93 | } 94 | 95 | // === layers 96 | final barcodeScannerLayer = Layer.scoped([ 97 | _acquireScanner, 98 | _acquireTextRecognizer, 99 | _acquireEntityExtractor, 100 | ] // 101 | .collect 102 | .map( 103 | (_) => BarcodeScannerService( 104 | barcodeScanner: _[0] as BarcodeScanner, 105 | textRecognizer: _[1] as TextRecognizer, 106 | entityExtractor: _[2] as EntityExtractor, 107 | ), 108 | )); 109 | 110 | final barcodeScannerAtom = barcodeScannerLayer.atomSyncOnly; 111 | 112 | final _acquireScanner = IO(BarcodeScanner.new) // 113 | .acquireRelease((_) => IO(_.close).asUnit); 114 | 115 | final _acquireTextRecognizer = IO(TextRecognizer.new) // 116 | .acquireRelease((_) => IO(_.close).asUnit); 117 | 118 | final _acquireEntityExtractor = 119 | IO(() => EntityExtractor(language: EntityExtractorLanguage.english)) // 120 | .acquireRelease((_) => IO(_.close).asUnit); 121 | -------------------------------------------------------------------------------- /lib/voucher_form/barcode_scanner/widgets/scanner_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:camera/camera.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_elemental/flutter_elemental.dart'; 5 | import 'package:flutter_hooks/flutter_hooks.dart'; 6 | import 'package:fluttertoast/fluttertoast.dart'; 7 | import 'package:functional_widget_annotation/functional_widget_annotation.dart'; 8 | import 'package:vouchervault/app/index.dart'; 9 | import 'package:vouchervault/voucher_form/index.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | 12 | part 'scanner_dialog.g.dart'; 13 | 14 | @hwidget 15 | Widget _scannerDialog( 16 | BuildContext context, { 17 | required void Function(BarcodeResult) onScan, 18 | }) { 19 | final scanner = useAtom(barcodeScannerAtom); 20 | final smartScan = useAtom(appSettings.select((a) => a.smartScan)); 21 | final setCameraPaused = context.setAtom(cameraPaused); 22 | final controller = useAtom(initializedCameraController); 23 | 24 | // Listen for scans 25 | final barcodeResults = useAtom(barcodeResultProvider); 26 | useEffect( 27 | () => barcodeResults.take(1).listen(onScan).cancel, 28 | [barcodeResults], 29 | ); 30 | 31 | // File picker 32 | final onPressedPicker = useCallback(() async { 33 | setCameraPaused(true); 34 | 35 | scanner 36 | .extractAllFromFile(smartScan) 37 | .tap((_) => ZIO(() => onScan(_))) 38 | .tapError( 39 | (_) => ZIO(() { 40 | Fluttertoast.showToast(msg: _.friendlyMessage); 41 | 42 | // Only un-pause on failure 43 | setCameraPaused(false); 44 | }), 45 | ) 46 | .runContext(context); 47 | }, [onScan, smartScan, setCameraPaused]); 48 | 49 | // Toggle flash 50 | final onPressedFlash = useCallback(() { 51 | controller.map((c) { 52 | if (c.value.flashMode != FlashMode.torch) { 53 | c.setFlashMode(FlashMode.torch); 54 | } else { 55 | c.setFlashMode(FlashMode.off); 56 | } 57 | }); 58 | }, [controller]); 59 | 60 | return AnnotatedRegion( 61 | value: SystemUiOverlayStyle.light, 62 | child: _PreviewDialog( 63 | controller: controller.whenOrElse(data: Option.of, orElse: Option.none), 64 | onPressedPicker: onPressedPicker, 65 | onPressedFlash: onPressedFlash, 66 | ), 67 | ); 68 | } 69 | 70 | @swidget 71 | Widget __previewDialog( 72 | BuildContext context, { 73 | required Option controller, 74 | required void Function() onPressedPicker, 75 | required void Function() onPressedFlash, 76 | }) => 77 | Scaffold( 78 | resizeToAvoidBottomInset: false, 79 | body: Stack( 80 | children: [ 81 | controller.match( 82 | () => Container(color: Colors.black), 83 | (c) => Positioned.fill( 84 | child: FittedBox( 85 | fit: BoxFit.cover, 86 | child: _CameraPreview(controller: c), 87 | ), 88 | ), 89 | ), 90 | Positioned( 91 | bottom: 0, 92 | left: 0, 93 | right: 0, 94 | child: SafeArea( 95 | top: false, 96 | bottom: true, 97 | child: Padding( 98 | padding: EdgeInsets.all(AppTheme.space3), 99 | child: Row( 100 | children: [ 101 | const Spacer(), 102 | IconButton( 103 | color: Colors.white, 104 | icon: const Icon(Icons.add_photo_alternate), 105 | onPressed: onPressedPicker, 106 | ), 107 | ...controller.match( 108 | () => [], 109 | (controller) => [ 110 | SizedBox(width: AppTheme.space3), 111 | IconButton( 112 | color: Colors.white, 113 | onPressed: onPressedFlash, 114 | icon: _FlashIcon(controller: controller), 115 | ), 116 | ], 117 | ), 118 | SizedBox(width: AppTheme.space3), 119 | ElevatedButton( 120 | style: ElevatedButton.styleFrom( 121 | backgroundColor: Colors.white, 122 | foregroundColor: Colors.black, 123 | ), 124 | onPressed: () => Navigator.of(context).pop(), 125 | child: Text(AppLocalizations.of(context)!.cancel), 126 | ), 127 | ], 128 | ), 129 | ), 130 | ), 131 | ), 132 | ], 133 | ), 134 | ); 135 | 136 | @swidget 137 | Widget __cameraPreview({ 138 | required CameraController controller, 139 | }) => 140 | SizedBox( 141 | height: controller.value.previewSize!.width, 142 | width: controller.value.previewSize!.height, 143 | child: controller.buildPreview(), 144 | ); 145 | 146 | @hwidget 147 | Widget __flashIcon({ 148 | required CameraController controller, 149 | }) { 150 | final mode = useListenableSelector( 151 | controller, 152 | () => controller.value.flashMode, 153 | ); 154 | 155 | return mode != FlashMode.torch 156 | ? const Icon(Icons.flash_on) 157 | : const Icon(Icons.flash_off); 158 | } 159 | --------------------------------------------------------------------------------