diff --git a/.gitignore b/.gitignore index 3820a95..6f0d006 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,45 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata index e8cf1e4..47ab4a0 100644 --- a/.metadata +++ b/.metadata @@ -1,33 +1,33 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - - platform: android - create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - - platform: ios - create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + - platform: android + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + - platform: ios + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md index 47bc83b..00846e6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# e_receipt_mobile - -E-Receipt portal moble version - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +# e_receipt_mobile + +E-Receipt portal moble version + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..d4e0f0c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,28 +1,28 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore index be3943c..c908258 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,14 +1,14 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b4cffbf..4c23f4b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,44 +1,54 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "com.example.e_receipt_mobile" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.e_receipt_mobile" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - } - } -} - -flutter { - source = "../.." -} +import java.util.Properties +import java.io.FileInputStream + +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + + +android { + namespace = "com.example.e_receipt_mobile" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.e_receipt_mobile" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..8ffe024 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,7 +1,7 @@ - - - - + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bbcdf52..113b9b3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,45 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/e_receipt_mobile/MainActivity.kt b/android/app/src/main/kotlin/com/example/e_receipt_mobile/MainActivity.kt index 62b3f5d..3e90457 100644 --- a/android/app/src/main/kotlin/com/example/e_receipt_mobile/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/e_receipt_mobile/MainActivity.kt @@ -1,5 +1,5 @@ -package com.example.e_receipt_mobile - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() +package com.example.e_receipt_mobile + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index f74085f..1cb7aa2 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -1,12 +1,12 @@ - - - - - - - - + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f..8403758 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -1,12 +1,12 @@ - - - - - - - - + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..f076017 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..41c41b8 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..ce12930 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..571adf4 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..41137dd 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 06952be..360a160 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..5fac679 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 399f698..8ffe024 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,7 +1,7 @@ - - - - + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657..1f88145 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,24 +1,24 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties index fbee1d8..21dbfa5 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,2 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e4ef43f..db3f453 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ca7fe06..4dcef4b 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -1,26 +1,26 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false -} - -include(":app") +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/ios/.gitignore b/ios/.gitignore index 7a7f987..ad322bc 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -1,34 +1,34 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..b5586f2 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -1,26 +1,26 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 13.0 - - + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..dfd2626 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ -#include "Generated.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..a97381a 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ -#include "Generated.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7cf71b5..35429cc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -1,616 +1,616 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.eReceiptMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 919434a..c4b79bd 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 18d9810..fc6bf80 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - - - - IDEDidComputeMac32BitWarning - - - + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c..af0309c 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,8 +1,8 @@ - - - - - PreviewsEnabled - - - + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..bbabc4e 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,101 +1,101 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..59c6d39 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 18d9810..fc6bf80 100644 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - - - - IDEDidComputeMac32BitWarning - - - + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c..af0309c 100644 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,8 +1,8 @@ - - - - - PreviewsEnabled - - - + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..8be1cec 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,13 +1,13 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..1950fd8 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2..d08a4de 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725..65a94b5 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -1,5 +1,5 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + 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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index f2e259c..497371e 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,37 +1,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c2851..bbb83ca 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,26 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index eb83d0e..1c28040 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,49 +1,49 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - E Receipt Mobile - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - e_receipt_mobile - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + E Receipt Mobile + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + e_receipt_mobile + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h index 308a2a5..fae207f 100644 --- a/ios/Runner/Runner-Bridging-Header.h +++ b/ios/Runner/Runner-Bridging-Header.h @@ -1 +1 @@ -#import "GeneratedPluginRegistrant.h" +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift index 86a7c3b..4d206de 100644 --- a/ios/RunnerTests/RunnerTests.swift +++ b/ios/RunnerTests/RunnerTests.swift @@ -1,12 +1,12 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index e4ece6f..e4933d5 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -1,7 +1,7 @@ -class AppConfig { - const AppConfig._(); - - // static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443'; - static const String apiBaseUrl = 'https://receipt-nest.utsmyanmar.com'; - static const String apiSecret = 'y812J21lhha11OS'; -} +class AppConfig { + const AppConfig._(); + + // static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443'; + static const String apiBaseUrl = 'https://receipt-nest.utsmyanmar.com'; + static const String apiSecret = 'y812J21lhha11OS'; +} diff --git a/lib/core/network/api_key_http_client.dart b/lib/core/network/api_key_http_client.dart index 1ddf7ab..3328c71 100644 --- a/lib/core/network/api_key_http_client.dart +++ b/lib/core/network/api_key_http_client.dart @@ -1,24 +1,24 @@ -import 'package:http/http.dart' as http; - -class ApiKeyHttpClient extends http.BaseClient { - ApiKeyHttpClient({required http.Client innerClient, required this.apiSecret}) - : _innerClient = innerClient; - - final http.Client _innerClient; - final String apiSecret; - - @override - Future send(http.BaseRequest request) { - if (apiSecret.trim().isNotEmpty && - !request.headers.keys.any((k) => k.toLowerCase() == 'x-api-key')) { - request.headers['x-api-key'] = apiSecret.trim(); - } - return _innerClient.send(request); - } - - @override - void close() { - _innerClient.close(); - super.close(); - } -} +import 'package:http/http.dart' as http; + +class ApiKeyHttpClient extends http.BaseClient { + ApiKeyHttpClient({required http.Client innerClient, required this.apiSecret}) + : _innerClient = innerClient; + + final http.Client _innerClient; + final String apiSecret; + + @override + Future send(http.BaseRequest request) { + if (apiSecret.trim().isNotEmpty && + !request.headers.keys.any((k) => k.toLowerCase() == 'x-api-key')) { + request.headers['x-api-key'] = apiSecret.trim(); + } + return _innerClient.send(request); + } + + @override + void close() { + _innerClient.close(); + super.close(); + } +} diff --git a/lib/core/network/auth_http_client.dart b/lib/core/network/auth_http_client.dart index d210c44..f0d9c29 100644 --- a/lib/core/network/auth_http_client.dart +++ b/lib/core/network/auth_http_client.dart @@ -1,93 +1,93 @@ -import 'dart:async'; - -import 'package:http/http.dart' as http; - -typedef AccessTokenProvider = String? Function(); -typedef RefreshAccessToken = Future Function(); -typedef RequestUriPredicate = bool Function(Uri uri); - -class AuthHttpClient extends http.BaseClient { - AuthHttpClient({ - required http.Client innerClient, - required AccessTokenProvider accessTokenProvider, - required RefreshAccessToken refreshAccessToken, - RequestUriPredicate? shouldAttachToken, - RequestUriPredicate? shouldRefreshOnUnauthorized, - }) : _innerClient = innerClient, - _accessTokenProvider = accessTokenProvider, - _refreshAccessToken = refreshAccessToken, - _shouldAttachToken = shouldAttachToken ?? _always, - _shouldRefreshOnUnauthorized = - shouldRefreshOnUnauthorized ?? _always; - - final http.Client _innerClient; - final AccessTokenProvider _accessTokenProvider; - final RefreshAccessToken _refreshAccessToken; - final RequestUriPredicate _shouldAttachToken; - final RequestUriPredicate _shouldRefreshOnUnauthorized; - - Future? _ongoingRefresh; - - static bool _always(Uri _) => true; - - @override - Future send(http.BaseRequest request) async { - if (_shouldAttachToken(request.url)) { - final token = _accessTokenProvider(); - if (token != null && - token.trim().isNotEmpty && - !request.headers.containsKey('Authorization')) { - request.headers['Authorization'] = 'Bearer ${token.trim()}'; - } - } - - final firstResponse = await _innerClient.send(request); - if (firstResponse.statusCode != 401 || - !_shouldRefreshOnUnauthorized(request.url) || - request is! http.Request) { - return firstResponse; - } - - final refreshedToken = await _refreshTokenWithLock(); - if (refreshedToken == null || refreshedToken.trim().isEmpty) { - return firstResponse; - } - - await firstResponse.stream.drain(); - final retryRequest = _cloneRequest(request); - retryRequest.headers['Authorization'] = 'Bearer ${refreshedToken.trim()}'; - return _innerClient.send(retryRequest); - } - - @override - void close() { - _innerClient.close(); - super.close(); - } - - Future _refreshTokenWithLock() async { - final inFlight = _ongoingRefresh; - if (inFlight != null) { - return inFlight; - } - - final nextRefresh = _refreshAccessToken(); - _ongoingRefresh = nextRefresh; - try { - return await nextRefresh; - } finally { - _ongoingRefresh = null; - } - } - - http.Request _cloneRequest(http.Request original) { - final cloned = http.Request(original.method, original.url) - ..followRedirects = original.followRedirects - ..maxRedirects = original.maxRedirects - ..persistentConnection = original.persistentConnection - ..encoding = original.encoding - ..bodyBytes = original.bodyBytes; - cloned.headers.addAll(original.headers); - return cloned; - } -} +import 'dart:async'; + +import 'package:http/http.dart' as http; + +typedef AccessTokenProvider = String? Function(); +typedef RefreshAccessToken = Future Function(); +typedef RequestUriPredicate = bool Function(Uri uri); + +class AuthHttpClient extends http.BaseClient { + AuthHttpClient({ + required http.Client innerClient, + required AccessTokenProvider accessTokenProvider, + required RefreshAccessToken refreshAccessToken, + RequestUriPredicate? shouldAttachToken, + RequestUriPredicate? shouldRefreshOnUnauthorized, + }) : _innerClient = innerClient, + _accessTokenProvider = accessTokenProvider, + _refreshAccessToken = refreshAccessToken, + _shouldAttachToken = shouldAttachToken ?? _always, + _shouldRefreshOnUnauthorized = + shouldRefreshOnUnauthorized ?? _always; + + final http.Client _innerClient; + final AccessTokenProvider _accessTokenProvider; + final RefreshAccessToken _refreshAccessToken; + final RequestUriPredicate _shouldAttachToken; + final RequestUriPredicate _shouldRefreshOnUnauthorized; + + Future? _ongoingRefresh; + + static bool _always(Uri _) => true; + + @override + Future send(http.BaseRequest request) async { + if (_shouldAttachToken(request.url)) { + final token = _accessTokenProvider(); + if (token != null && + token.trim().isNotEmpty && + !request.headers.containsKey('Authorization')) { + request.headers['Authorization'] = 'Bearer ${token.trim()}'; + } + } + + final firstResponse = await _innerClient.send(request); + if (firstResponse.statusCode != 401 || + !_shouldRefreshOnUnauthorized(request.url) || + request is! http.Request) { + return firstResponse; + } + + final refreshedToken = await _refreshTokenWithLock(); + if (refreshedToken == null || refreshedToken.trim().isEmpty) { + return firstResponse; + } + + await firstResponse.stream.drain(); + final retryRequest = _cloneRequest(request); + retryRequest.headers['Authorization'] = 'Bearer ${refreshedToken.trim()}'; + return _innerClient.send(retryRequest); + } + + @override + void close() { + _innerClient.close(); + super.close(); + } + + Future _refreshTokenWithLock() async { + final inFlight = _ongoingRefresh; + if (inFlight != null) { + return inFlight; + } + + final nextRefresh = _refreshAccessToken(); + _ongoingRefresh = nextRefresh; + try { + return await nextRefresh; + } finally { + _ongoingRefresh = null; + } + } + + http.Request _cloneRequest(http.Request original) { + final cloned = http.Request(original.method, original.url) + ..followRedirects = original.followRedirects + ..maxRedirects = original.maxRedirects + ..persistentConnection = original.persistentConnection + ..encoding = original.encoding + ..bodyBytes = original.bodyBytes; + cloned.headers.addAll(original.headers); + return cloned; + } +} diff --git a/lib/core/network/logging_http_client.dart b/lib/core/network/logging_http_client.dart index c5ea7f5..17d79c9 100644 --- a/lib/core/network/logging_http_client.dart +++ b/lib/core/network/logging_http_client.dart @@ -1,118 +1,118 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; - -class LoggingHttpClient extends http.BaseClient { - LoggingHttpClient([http.Client? innerClient]) - : _innerClient = innerClient ?? http.Client(); - - final http.Client _innerClient; - - @override - Future send(http.BaseRequest request) async { - final requestBody = request is http.Request ? request.body : null; - _logRequest(request, requestBody); - - try { - final response = await _innerClient.send(request); - final responseBytes = await response.stream.toBytes(); - final responseBody = _formatBodyForLogging( - responseBytes: responseBytes, - contentType: response.headers['content-type'], - ); - - _logResponse(response, responseBody); - - return http.StreamedResponse( - Stream>.fromIterable([responseBytes]), - response.statusCode, - contentLength: response.contentLength, - request: response.request, - headers: response.headers, - isRedirect: response.isRedirect, - persistentConnection: response.persistentConnection, - reasonPhrase: response.reasonPhrase, - ); - } catch (error, stackTrace) { - debugPrint('[API][ERROR] ${request.method} ${request.url}'); - debugPrint('[API][ERROR] $error'); - debugPrint('[API][ERROR] $stackTrace'); - rethrow; - } - } - - @override - void close() { - _innerClient.close(); - super.close(); - } - - void _logRequest(http.BaseRequest request, String? requestBody) { - final safeHeaders = _sanitizeHeaders(request.headers); - debugPrint('[API][REQUEST] ${request.method} ${request.url}'); - debugPrint('[API][REQUEST][HEADERS] $safeHeaders'); - if (requestBody != null && requestBody.isNotEmpty) { - debugPrint('[API][REQUEST][BODY] $requestBody'); - } - } - - void _logResponse(http.StreamedResponse response, String responseBody) { - final safeHeaders = _sanitizeHeaders(response.headers); - debugPrint( - '[API][RESPONSE] ${response.statusCode} ${response.request?.method} ${response.request?.url}', - ); - debugPrint('[API][RESPONSE][HEADERS] $safeHeaders'); - if (responseBody.isNotEmpty) { - debugPrint('[API][RESPONSE][BODY] $responseBody'); - } - } - - Map _sanitizeHeaders(Map headers) { - final safeHeaders = Map.from(headers); - const sensitiveKeys = { - 'authorization', - 'x-api-key', - 'api-key', - 'x-signature', - 'cookie', - 'set-cookie', - }; - - for (final key in safeHeaders.keys.toList()) { - if (sensitiveKeys.contains(key.toLowerCase())) { - safeHeaders[key] = '***'; - } - } - - return safeHeaders; - } - - String _formatBodyForLogging({ - required List responseBytes, - required String? contentType, - }) { - final normalizedType = contentType?.toLowerCase() ?? ''; - final isBinary = - normalizedType.contains('application/pdf') || - normalizedType.contains('application/octet-stream') || - normalizedType.startsWith('image/') || - normalizedType.startsWith('audio/') || - normalizedType.startsWith('video/'); - - if (isBinary) { - return ''; - } - - final decoded = utf8.decode(responseBytes, allowMalformed: true).trim(); - if (decoded.isEmpty) { - return ''; - } - - const maxChars = 4000; - if (decoded.length <= maxChars) { - return decoded; - } - return '${decoded.substring(0, maxChars)}…'; - } -} +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; + +class LoggingHttpClient extends http.BaseClient { + LoggingHttpClient([http.Client? innerClient]) + : _innerClient = innerClient ?? http.Client(); + + final http.Client _innerClient; + + @override + Future send(http.BaseRequest request) async { + final requestBody = request is http.Request ? request.body : null; + _logRequest(request, requestBody); + + try { + final response = await _innerClient.send(request); + final responseBytes = await response.stream.toBytes(); + final responseBody = _formatBodyForLogging( + responseBytes: responseBytes, + contentType: response.headers['content-type'], + ); + + _logResponse(response, responseBody); + + return http.StreamedResponse( + Stream>.fromIterable([responseBytes]), + response.statusCode, + contentLength: response.contentLength, + request: response.request, + headers: response.headers, + isRedirect: response.isRedirect, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase, + ); + } catch (error, stackTrace) { + debugPrint('[API][ERROR] ${request.method} ${request.url}'); + debugPrint('[API][ERROR] $error'); + debugPrint('[API][ERROR] $stackTrace'); + rethrow; + } + } + + @override + void close() { + _innerClient.close(); + super.close(); + } + + void _logRequest(http.BaseRequest request, String? requestBody) { + final safeHeaders = _sanitizeHeaders(request.headers); + debugPrint('[API][REQUEST] ${request.method} ${request.url}'); + debugPrint('[API][REQUEST][HEADERS] $safeHeaders'); + if (requestBody != null && requestBody.isNotEmpty) { + debugPrint('[API][REQUEST][BODY] $requestBody'); + } + } + + void _logResponse(http.StreamedResponse response, String responseBody) { + final safeHeaders = _sanitizeHeaders(response.headers); + debugPrint( + '[API][RESPONSE] ${response.statusCode} ${response.request?.method} ${response.request?.url}', + ); + debugPrint('[API][RESPONSE][HEADERS] $safeHeaders'); + if (responseBody.isNotEmpty) { + debugPrint('[API][RESPONSE][BODY] $responseBody'); + } + } + + Map _sanitizeHeaders(Map headers) { + final safeHeaders = Map.from(headers); + const sensitiveKeys = { + 'authorization', + 'x-api-key', + 'api-key', + 'x-signature', + 'cookie', + 'set-cookie', + }; + + for (final key in safeHeaders.keys.toList()) { + if (sensitiveKeys.contains(key.toLowerCase())) { + safeHeaders[key] = '***'; + } + } + + return safeHeaders; + } + + String _formatBodyForLogging({ + required List responseBytes, + required String? contentType, + }) { + final normalizedType = contentType?.toLowerCase() ?? ''; + final isBinary = + normalizedType.contains('application/pdf') || + normalizedType.contains('application/octet-stream') || + normalizedType.startsWith('image/') || + normalizedType.startsWith('audio/') || + normalizedType.startsWith('video/'); + + if (isBinary) { + return ''; + } + + final decoded = utf8.decode(responseBytes, allowMalformed: true).trim(); + if (decoded.isEmpty) { + return ''; + } + + const maxChars = 4000; + if (decoded.length <= maxChars) { + return decoded; + } + return '${decoded.substring(0, maxChars)}…'; + } +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 3d13ea8..fde33bb 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - -class AppColors { - const AppColors._(); - - static const Color primary = Color(0xFF2A60AF); -} +import 'package:flutter/material.dart'; + +class AppColors { + const AppColors._(); + + static const Color primary = Color(0xFF2A60AF); +} diff --git a/lib/data/repositories/api_auth_repository.dart b/lib/data/repositories/api_auth_repository.dart index 96bc275..fef17fa 100644 --- a/lib/data/repositories/api_auth_repository.dart +++ b/lib/data/repositories/api_auth_repository.dart @@ -1,242 +1,242 @@ -import 'dart:convert'; - -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:e_receipt_mobile/domain/exceptions/auth_exceptions.dart'; -import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; -import 'package:http/http.dart' as http; - -class ApiAuthRepository implements AuthRepository { - ApiAuthRepository({ - required this.baseUrl, - required this.apiSecret, - http.Client? client, - }) : _client = client ?? http.Client(); - - final String baseUrl; - final String apiSecret; - final http.Client _client; - - @override - Future login({ - required String username, - required String password, - }) async { - final uri = Uri.parse('$baseUrl/auth/login'); - final requestBody = jsonEncode({ - 'username': username.trim().toLowerCase(), - 'password': password, - }); - - final response = await _client.post( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - }, - body: requestBody, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return _parseUserFromResponse( - response.body, - fallbackUsername: username.trim(), - ); - } - - final data = _asMap(response.body); - final payload = _extractPayload(data); - final require = - _pickString(data, const ['require', 'required']) ?? - _pickString(payload, const ['require', 'required']); - if (require == 'PASSWORD_UPDATE_REQUIRED') { - final userId = _extractUserId(data); - if (userId != null) { - throw PasswordUpdateRequiredException( - userId: userId, - message: _extractErrorMessage(response.body), - ); - } - } - - throw Exception(_extractErrorMessage(response.body)); - } - - @override - Future refreshSession({ - required String username, - required String refreshToken, - required String role, - }) async { - final uri = Uri.parse('$baseUrl/auth/refresh-token'); - final requestBody = jsonEncode({ - 'refreshToken': refreshToken, - }); - - final response = await _client.post( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - }, - body: requestBody, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return _parseUserFromResponse( - response.body, - fallbackUsername: username, - fallbackRefreshToken: refreshToken, - fallbackRole: role, - ); - } - - throw Exception(_extractErrorMessage(response.body)); - } - - @override - Future resetPassword({ - required String userId, - required String password, - }) async { - final uri = Uri.parse('$baseUrl/auth/reset-password'); - final requestBody = jsonEncode({ - 'userId': userId, - 'password': password, - }); - - final response = await _client.put( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - }, - body: requestBody, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return; - } - - throw Exception(_extractErrorMessage(response.body)); - } - - @override - Future logout({required String refreshToken}) async { - final uri = Uri.parse('$baseUrl/auth/logout'); - final requestBody = jsonEncode({ - 'refreshToken': refreshToken, - }); - - final response = await _client.post( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - }, - body: requestBody, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return; - } - - throw Exception(_extractErrorMessage(response.body)); - } - - Map _asMap(String body) { - try { - final decoded = jsonDecode(body); - if (decoded is Map) { - return decoded; - } - return {}; - } catch (_) { - return {}; - } - } - - String _extractErrorMessage(String body) { - final data = _asMap(body); - final message = data['message'] ?? data['error'] ?? data['detail']; - if (message == null) { - return 'Login failed'; - } - return message.toString(); - } - - String? _extractUserId(Map data) { - final rootUser = data['user']; - if (rootUser is Map) { - final id = _pickString(rootUser, const ['id', 'userId', 'user_id']); - if (id != null) { - return id; - } - } - - final payload = _extractPayload(data); - final payloadUser = payload['user']; - if (payloadUser is Map) { - return _pickString(payloadUser, const ['id', 'userId', 'user_id']); - } - - return _pickString(payload, const ['userId', 'user_id', 'id']); - } - - LoginUser _parseUserFromResponse( - String body, { - required String fallbackUsername, - String? fallbackRefreshToken, - String? fallbackRole, - }) { - final data = _asMap(body); - final payload = _extractPayload(data); - final apiUsername = _pickString(payload, const ['username', 'email']); - final token = _pickString(payload, const ['token', 'accessToken']); - final refreshToken = _pickString( - payload, - const ['refreshToken', 'refresh_token'], - ); - final role = _pickString(payload, const ['role']); - - if (token == null) { - throw Exception('Auth response is missing token'); - } - - final effectiveRefreshToken = refreshToken ?? fallbackRefreshToken; - if (effectiveRefreshToken == null) { - throw Exception('Auth response is missing refresh token'); - } - - final effectiveRole = role ?? fallbackRole; - if (effectiveRole == null || effectiveRole.trim().isEmpty) { - throw Exception('Auth response is missing role'); - } - - return LoginUser( - username: apiUsername?.trim().isNotEmpty == true - ? apiUsername!.trim() - : fallbackUsername.trim(), - token: token, - refreshToken: effectiveRefreshToken, - role: effectiveRole, - ); - } - - Map _extractPayload(Map data) { - final dynamic nested = data['data'] ?? data['result'] ?? data['payload']; - if (nested is Map) { - return nested; - } - return data; - } - - String? _pickString(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value != null && value.toString().trim().isNotEmpty) { - return value.toString().trim(); - } - } - return null; - } -} +import 'dart:convert'; + +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:e_receipt_mobile/domain/exceptions/auth_exceptions.dart'; +import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; +import 'package:http/http.dart' as http; + +class ApiAuthRepository implements AuthRepository { + ApiAuthRepository({ + required this.baseUrl, + required this.apiSecret, + http.Client? client, + }) : _client = client ?? http.Client(); + + final String baseUrl; + final String apiSecret; + final http.Client _client; + + @override + Future login({ + required String username, + required String password, + }) async { + final uri = Uri.parse('$baseUrl/auth/login'); + final requestBody = jsonEncode({ + 'username': username.trim().toLowerCase(), + 'password': password, + }); + + final response = await _client.post( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + }, + body: requestBody, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseUserFromResponse( + response.body, + fallbackUsername: username.trim(), + ); + } + + final data = _asMap(response.body); + final payload = _extractPayload(data); + final require = + _pickString(data, const ['require', 'required']) ?? + _pickString(payload, const ['require', 'required']); + if (require == 'PASSWORD_UPDATE_REQUIRED') { + final userId = _extractUserId(data); + if (userId != null) { + throw PasswordUpdateRequiredException( + userId: userId, + message: _extractErrorMessage(response.body), + ); + } + } + + throw Exception(_extractErrorMessage(response.body)); + } + + @override + Future refreshSession({ + required String username, + required String refreshToken, + required String role, + }) async { + final uri = Uri.parse('$baseUrl/auth/refresh-token'); + final requestBody = jsonEncode({ + 'refreshToken': refreshToken, + }); + + final response = await _client.post( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + }, + body: requestBody, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseUserFromResponse( + response.body, + fallbackUsername: username, + fallbackRefreshToken: refreshToken, + fallbackRole: role, + ); + } + + throw Exception(_extractErrorMessage(response.body)); + } + + @override + Future resetPassword({ + required String userId, + required String password, + }) async { + final uri = Uri.parse('$baseUrl/auth/reset-password'); + final requestBody = jsonEncode({ + 'userId': userId, + 'password': password, + }); + + final response = await _client.put( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + }, + body: requestBody, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return; + } + + throw Exception(_extractErrorMessage(response.body)); + } + + @override + Future logout({required String refreshToken}) async { + final uri = Uri.parse('$baseUrl/auth/logout'); + final requestBody = jsonEncode({ + 'refreshToken': refreshToken, + }); + + final response = await _client.post( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + }, + body: requestBody, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return; + } + + throw Exception(_extractErrorMessage(response.body)); + } + + Map _asMap(String body) { + try { + final decoded = jsonDecode(body); + if (decoded is Map) { + return decoded; + } + return {}; + } catch (_) { + return {}; + } + } + + String _extractErrorMessage(String body) { + final data = _asMap(body); + final message = data['message'] ?? data['error'] ?? data['detail']; + if (message == null) { + return 'Login failed'; + } + return message.toString(); + } + + String? _extractUserId(Map data) { + final rootUser = data['user']; + if (rootUser is Map) { + final id = _pickString(rootUser, const ['id', 'userId', 'user_id']); + if (id != null) { + return id; + } + } + + final payload = _extractPayload(data); + final payloadUser = payload['user']; + if (payloadUser is Map) { + return _pickString(payloadUser, const ['id', 'userId', 'user_id']); + } + + return _pickString(payload, const ['userId', 'user_id', 'id']); + } + + LoginUser _parseUserFromResponse( + String body, { + required String fallbackUsername, + String? fallbackRefreshToken, + String? fallbackRole, + }) { + final data = _asMap(body); + final payload = _extractPayload(data); + final apiUsername = _pickString(payload, const ['username', 'email']); + final token = _pickString(payload, const ['token', 'accessToken']); + final refreshToken = _pickString( + payload, + const ['refreshToken', 'refresh_token'], + ); + final role = _pickString(payload, const ['role']); + + if (token == null) { + throw Exception('Auth response is missing token'); + } + + final effectiveRefreshToken = refreshToken ?? fallbackRefreshToken; + if (effectiveRefreshToken == null) { + throw Exception('Auth response is missing refresh token'); + } + + final effectiveRole = role ?? fallbackRole; + if (effectiveRole == null || effectiveRole.trim().isEmpty) { + throw Exception('Auth response is missing role'); + } + + return LoginUser( + username: apiUsername?.trim().isNotEmpty == true + ? apiUsername!.trim() + : fallbackUsername.trim(), + token: token, + refreshToken: effectiveRefreshToken, + role: effectiveRole, + ); + } + + Map _extractPayload(Map data) { + final dynamic nested = data['data'] ?? data['result'] ?? data['payload']; + if (nested is Map) { + return nested; + } + return data; + } + + String? _pickString(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value != null && value.toString().trim().isNotEmpty) { + return value.toString().trim(); + } + } + return null; + } +} diff --git a/lib/data/repositories/api_merchant_repository.dart b/lib/data/repositories/api_merchant_repository.dart index 5582d9b..149ebaa 100644 --- a/lib/data/repositories/api_merchant_repository.dart +++ b/lib/data/repositories/api_merchant_repository.dart @@ -1,270 +1,270 @@ -import 'dart:convert'; - -import 'package:e_receipt_mobile/domain/entities/merchant.dart'; -import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:http/http.dart' as http; - -class ApiMerchantRepository implements MerchantRepository { - ApiMerchantRepository({ - required this.baseUrl, - required this.apiSecret, - required http.Client client, - }) : _client = client; - - final String baseUrl; - final String apiSecret; - final http.Client _client; - - @override - Future> getMerchants({ - required String token, - int page = 1, - int limit = 10, - String searchTerm = '', - }) async { - final queryParameters = { - 'page': page.toString(), - 'limit': limit.toString(), - if (searchTerm.trim().isNotEmpty) 'searchTerm': searchTerm.trim(), - }; - final uri = Uri.parse( - '$baseUrl/merchant', - ).replace(queryParameters: queryParameters); - final response = await _client.get( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - 'Authorization': 'Bearer ${token.trim()}', - }, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return _parseMerchants(response.body); - } - - throw Exception(_extractErrorMessage(response.body)); - } - - @override - Future> getTerminalsByMerchantId({ - required String token, - required String merchantId, - }) async { - final uri = Uri.parse('$baseUrl/terminal/list/$merchantId'); - final response = await _client.get( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - 'Authorization': 'Bearer ${token.trim()}', - }, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return _parseTerminals(response.body); - } - - throw Exception(_extractErrorMessage(response.body)); - } - - List _parseMerchants(String body) { - final decoded = _tryDecode(body); - final list = _extractList(decoded); - return list.map(_merchantFromItem).whereType().toList(); - } - - List _parseTerminals(String body) { - final decoded = _tryDecode(body); - final list = _extractTerminalList(decoded); - return list.map(_terminalFromItem).whereType().toList(); - } - - dynamic _tryDecode(String body) { - try { - return jsonDecode(body); - } catch (_) { - return null; - } - } - - List _extractList(dynamic decoded) { - if (decoded is List) { - return decoded; - } - if (decoded is Map) { - final directMerchants = decoded['merchants']; - if (directMerchants is List) { - return directMerchants; - } - - final data = - decoded['data'] ?? - decoded['result'] ?? - decoded['payload'] ?? - decoded['merchantData']; - if (data is List) { - return data; - } - if (data is Map) { - final nestedMerchants = data['merchants']; - if (nestedMerchants is List) { - return nestedMerchants; - } - } - } - return const []; - } - - List _extractTerminalList(dynamic decoded) { - if (decoded is List) { - return decoded; - } - if (decoded is Map) { - final directTerminals = decoded['terminals']; - if (directTerminals is List) { - return directTerminals; - } - - final data = - decoded['data'] ?? - decoded['result'] ?? - decoded['payload'] ?? - decoded['terminalData']; - if (data is List) { - return data; - } - if (data is Map) { - final nestedTerminals = data['terminals']; - if (nestedTerminals is List) { - return nestedTerminals; - } - } - } - return const []; - } - - Merchant? _merchantFromItem(dynamic item) { - if (item is! Map) { - return null; - } - - return Merchant( - id: _pickString(item, const ['id', 'merchantId', 'merchant_id']), - name: _pickString(item, const ['name', 'merchantName', 'merchant_name']), - mobile: _pickString(item, const ['mobile']), - description: _pickString(item, const ['description']), - address: _pickString(item, const ['address']), - address2: _pickString(item, const ['address2']), - address3: _pickString(item, const ['address3']), - phone: _pickString(item, const ['phone']), - mids: _pickString(item, const ['mids']), - createdBy: _pickString(item, const ['createdBy', 'created_by']), - updatedBy: _pickString(item, const ['updatedBy', 'updated_by']), - createdAt: _pickString(item, const ['createdAt', 'created_at']), - updatedAt: _pickString(item, const ['updatedAt', 'updated_at']), - ); - } - - Terminal? _terminalFromItem(dynamic item) { - if (item is! Map) { - return null; - } - - return Terminal( - id: _pickString(item, const ['id', 'terminalId', 'terminal_id']), - serial: _pickString(item, const ['serial']), - address: _pickString(item, const ['address']), - address2: _pickString(item, const ['address2']), - address3: _pickString(item, const ['address3']), - name: _pickString(item, const ['name', 'terminalName', 'terminal_name']), - appId: _pickString(item, const ['appId', 'app_id']), - tid: _pickString(item, const ['tid', 'terminalNo', 'terminal_no']), - mids: _pickStringList(item, const ['mid', 'mids']), - merchantId: _pickString(item, const ['merchantId', 'merchant_id']), - status: _pickString(item, const ['status']), - totalTransactions: _pickInt(item, const [ - 'totalTransactions', - 'total_transactions', - ]), - totalAmount: _pickNum(item, const ['totalAmount', 'total_amount']), - createdAt: _pickString(item, const ['createdAt', 'created_at']), - updatedAt: _pickString(item, const ['updatedAt', 'updated_at']), - ); - } - - String? _pickString(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value != null && value.toString().trim().isNotEmpty) { - return value.toString().trim(); - } - } - return null; - } - - List? _pickStringList(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value is List) { - final results = value - .where((e) => e != null && e.toString().trim().isNotEmpty) - .map((e) => e.toString().trim()) - .toList(); - return results.isEmpty ? null : results; - } - if (value != null && value.toString().trim().isNotEmpty) { - return [value.toString().trim()]; - } - } - return null; - } - - int? _pickInt(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value is int) { - return value; - } - if (value is num) { - return value.toInt(); - } - if (value != null) { - final parsed = int.tryParse(value.toString().trim()); - if (parsed != null) { - return parsed; - } - } - } - return null; - } - - num? _pickNum(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value is num) { - return value; - } - if (value != null) { - final parsed = num.tryParse(value.toString().trim()); - if (parsed != null) { - return parsed; - } - } - } - return null; - } - - String _extractErrorMessage(String body) { - final decoded = _tryDecode(body); - if (decoded is Map) { - final message = - decoded['message'] ?? decoded['error'] ?? decoded['detail']; - if (message != null) { - return message.toString(); - } - } - return 'Failed to load merchants'; - } -} +import 'dart:convert'; + +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:http/http.dart' as http; + +class ApiMerchantRepository implements MerchantRepository { + ApiMerchantRepository({ + required this.baseUrl, + required this.apiSecret, + required http.Client client, + }) : _client = client; + + final String baseUrl; + final String apiSecret; + final http.Client _client; + + @override + Future> getMerchants({ + required String token, + int page = 1, + int limit = 10, + String searchTerm = '', + }) async { + final queryParameters = { + 'page': page.toString(), + 'limit': limit.toString(), + if (searchTerm.trim().isNotEmpty) 'searchTerm': searchTerm.trim(), + }; + final uri = Uri.parse( + '$baseUrl/merchant', + ).replace(queryParameters: queryParameters); + final response = await _client.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + 'Authorization': 'Bearer ${token.trim()}', + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseMerchants(response.body); + } + + throw Exception(_extractErrorMessage(response.body)); + } + + @override + Future> getTerminalsByMerchantId({ + required String token, + required String merchantId, + }) async { + final uri = Uri.parse('$baseUrl/terminal/list/$merchantId'); + final response = await _client.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + 'Authorization': 'Bearer ${token.trim()}', + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseTerminals(response.body); + } + + throw Exception(_extractErrorMessage(response.body)); + } + + List _parseMerchants(String body) { + final decoded = _tryDecode(body); + final list = _extractList(decoded); + return list.map(_merchantFromItem).whereType().toList(); + } + + List _parseTerminals(String body) { + final decoded = _tryDecode(body); + final list = _extractTerminalList(decoded); + return list.map(_terminalFromItem).whereType().toList(); + } + + dynamic _tryDecode(String body) { + try { + return jsonDecode(body); + } catch (_) { + return null; + } + } + + List _extractList(dynamic decoded) { + if (decoded is List) { + return decoded; + } + if (decoded is Map) { + final directMerchants = decoded['merchants']; + if (directMerchants is List) { + return directMerchants; + } + + final data = + decoded['data'] ?? + decoded['result'] ?? + decoded['payload'] ?? + decoded['merchantData']; + if (data is List) { + return data; + } + if (data is Map) { + final nestedMerchants = data['merchants']; + if (nestedMerchants is List) { + return nestedMerchants; + } + } + } + return const []; + } + + List _extractTerminalList(dynamic decoded) { + if (decoded is List) { + return decoded; + } + if (decoded is Map) { + final directTerminals = decoded['terminals']; + if (directTerminals is List) { + return directTerminals; + } + + final data = + decoded['data'] ?? + decoded['result'] ?? + decoded['payload'] ?? + decoded['terminalData']; + if (data is List) { + return data; + } + if (data is Map) { + final nestedTerminals = data['terminals']; + if (nestedTerminals is List) { + return nestedTerminals; + } + } + } + return const []; + } + + Merchant? _merchantFromItem(dynamic item) { + if (item is! Map) { + return null; + } + + return Merchant( + id: _pickString(item, const ['id', 'merchantId', 'merchant_id']), + name: _pickString(item, const ['name', 'merchantName', 'merchant_name']), + mobile: _pickString(item, const ['mobile']), + description: _pickString(item, const ['description']), + address: _pickString(item, const ['address']), + address2: _pickString(item, const ['address2']), + address3: _pickString(item, const ['address3']), + phone: _pickString(item, const ['phone']), + mids: _pickString(item, const ['mids']), + createdBy: _pickString(item, const ['createdBy', 'created_by']), + updatedBy: _pickString(item, const ['updatedBy', 'updated_by']), + createdAt: _pickString(item, const ['createdAt', 'created_at']), + updatedAt: _pickString(item, const ['updatedAt', 'updated_at']), + ); + } + + Terminal? _terminalFromItem(dynamic item) { + if (item is! Map) { + return null; + } + + return Terminal( + id: _pickString(item, const ['id', 'terminalId', 'terminal_id']), + serial: _pickString(item, const ['serial']), + address: _pickString(item, const ['address']), + address2: _pickString(item, const ['address2']), + address3: _pickString(item, const ['address3']), + name: _pickString(item, const ['name', 'terminalName', 'terminal_name']), + appId: _pickString(item, const ['appId', 'app_id']), + tid: _pickString(item, const ['tid', 'terminalNo', 'terminal_no']), + mids: _pickStringList(item, const ['mid', 'mids']), + merchantId: _pickString(item, const ['merchantId', 'merchant_id']), + status: _pickString(item, const ['status']), + totalTransactions: _pickInt(item, const [ + 'totalTransactions', + 'total_transactions', + ]), + totalAmount: _pickNum(item, const ['totalAmount', 'total_amount']), + createdAt: _pickString(item, const ['createdAt', 'created_at']), + updatedAt: _pickString(item, const ['updatedAt', 'updated_at']), + ); + } + + String? _pickString(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value != null && value.toString().trim().isNotEmpty) { + return value.toString().trim(); + } + } + return null; + } + + List? _pickStringList(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value is List) { + final results = value + .where((e) => e != null && e.toString().trim().isNotEmpty) + .map((e) => e.toString().trim()) + .toList(); + return results.isEmpty ? null : results; + } + if (value != null && value.toString().trim().isNotEmpty) { + return [value.toString().trim()]; + } + } + return null; + } + + int? _pickInt(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value is int) { + return value; + } + if (value is num) { + return value.toInt(); + } + if (value != null) { + final parsed = int.tryParse(value.toString().trim()); + if (parsed != null) { + return parsed; + } + } + } + return null; + } + + num? _pickNum(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value is num) { + return value; + } + if (value != null) { + final parsed = num.tryParse(value.toString().trim()); + if (parsed != null) { + return parsed; + } + } + } + return null; + } + + String _extractErrorMessage(String body) { + final decoded = _tryDecode(body); + if (decoded is Map) { + final message = + decoded['message'] ?? decoded['error'] ?? decoded['detail']; + if (message != null) { + return message.toString(); + } + } + return 'Failed to load merchants'; + } +} diff --git a/lib/data/repositories/api_receipt_repository.dart b/lib/data/repositories/api_receipt_repository.dart index 0bf8a86..95b3c72 100644 --- a/lib/data/repositories/api_receipt_repository.dart +++ b/lib/data/repositories/api_receipt_repository.dart @@ -1,139 +1,139 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:e_receipt_mobile/domain/entities/receipt_content.dart'; -import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart'; -import 'package:http/http.dart' as http; - -class ApiReceiptRepository implements ReceiptRepository { - ApiReceiptRepository({ - required this.baseUrl, - required this.apiSecret, - required http.Client client, - }) : _client = client; - - final String baseUrl; - final String apiSecret; - final http.Client _client; - - @override - Future getTransactionReceipt({ - required String token, - required String transactionId, - required String copyFor, - }) async { - final uri = _normalizeLocalhostForAndroid( - Uri.parse('$baseUrl/transaction/pdf').replace( - queryParameters: { - 'transactionId': transactionId, - 'copyFor': copyFor, - }, - ), - ); - - final request = http.Request('GET', uri); - request.headers.addAll({ - 'Accept': 'application/pdf,text/html', - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - if (token.trim().isNotEmpty) 'Authorization': 'Bearer ${token.trim()}', - }); - - final streamed = await _client.send(request); - final bytes = await streamed.stream.toBytes(); - final contentType = streamed.headers['content-type']?.trim() ?? ''; - - if (streamed.statusCode < 200 || streamed.statusCode >= 300) { - final body = const Utf8Decoder( - allowMalformed: true, - ).convert(bytes).trim(); - throw Exception( - body.isNotEmpty && body.length <= 500 - ? body - : 'Failed to load receipt (${streamed.statusCode}) $contentType', - ); - } - - if (_looksLikePdf(bytes: bytes, contentType: contentType)) { - return ReceiptPdfContent(bytes); - } - - final html = const Utf8Decoder(allowMalformed: true).convert(bytes); - return ReceiptHtmlContent(html: html, contentType: contentType); - } - - @override - Future getTransactionReceiptPdfBytes({ - required String token, - required String transactionId, - required String copyFor, - }) async { - final uri = _normalizeLocalhostForAndroid( - Uri.parse('$baseUrl/transaction/pdf').replace( - queryParameters: { - 'transactionId': transactionId, - 'copyFor': copyFor, - }, - ), - ); - - final request = http.Request('GET', uri); - request.headers.addAll({ - 'Accept': 'application/pdf', - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - if (token.trim().isNotEmpty) 'Authorization': 'Bearer ${token.trim()}', - }); - - final streamed = await _client.send(request); - final bytes = await streamed.stream.toBytes(); - final contentType = streamed.headers['content-type']?.trim() ?? ''; - - if (streamed.statusCode < 200 || streamed.statusCode >= 300) { - final body = const Utf8Decoder( - allowMalformed: true, - ).convert(bytes).trim(); - throw Exception( - body.isNotEmpty && body.length <= 500 - ? body - : 'Failed to load receipt PDF (${streamed.statusCode}) $contentType', - ); - } - - if (_looksLikePdf(bytes: bytes, contentType: contentType)) { - return bytes; - } - - final body = const Utf8Decoder(allowMalformed: true).convert(bytes).trim(); - throw Exception( - body.isNotEmpty && body.length <= 500 - ? body - : 'Server did not return a PDF ($contentType)', - ); - } - - Uri _normalizeLocalhostForAndroid(Uri uri) { - if (!Platform.isAndroid) { - return uri; - } - if (uri.host != 'localhost') { - return uri; - } - return uri.replace(host: '10.0.2.2'); - } - - bool _looksLikePdf({required List bytes, required String contentType}) { - final type = contentType.toLowerCase(); - if (type.contains('application/pdf')) { - return true; - } - if (bytes.length < 4) { - return false; - } - return bytes[0] == 0x25 && // % - bytes[1] == 0x50 && // P - bytes[2] == 0x44 && // D - bytes[3] == 0x46; // F - } -} +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:e_receipt_mobile/domain/entities/receipt_content.dart'; +import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart'; +import 'package:http/http.dart' as http; + +class ApiReceiptRepository implements ReceiptRepository { + ApiReceiptRepository({ + required this.baseUrl, + required this.apiSecret, + required http.Client client, + }) : _client = client; + + final String baseUrl; + final String apiSecret; + final http.Client _client; + + @override + Future getTransactionReceipt({ + required String token, + required String transactionId, + required String copyFor, + }) async { + final uri = _normalizeLocalhostForAndroid( + Uri.parse('$baseUrl/transaction/pdf').replace( + queryParameters: { + 'transactionId': transactionId, + 'copyFor': copyFor, + }, + ), + ); + + final request = http.Request('GET', uri); + request.headers.addAll({ + 'Accept': 'application/pdf,text/html', + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + if (token.trim().isNotEmpty) 'Authorization': 'Bearer ${token.trim()}', + }); + + final streamed = await _client.send(request); + final bytes = await streamed.stream.toBytes(); + final contentType = streamed.headers['content-type']?.trim() ?? ''; + + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + final body = const Utf8Decoder( + allowMalformed: true, + ).convert(bytes).trim(); + throw Exception( + body.isNotEmpty && body.length <= 500 + ? body + : 'Failed to load receipt (${streamed.statusCode}) $contentType', + ); + } + + if (_looksLikePdf(bytes: bytes, contentType: contentType)) { + return ReceiptPdfContent(bytes); + } + + final html = const Utf8Decoder(allowMalformed: true).convert(bytes); + return ReceiptHtmlContent(html: html, contentType: contentType); + } + + @override + Future getTransactionReceiptPdfBytes({ + required String token, + required String transactionId, + required String copyFor, + }) async { + final uri = _normalizeLocalhostForAndroid( + Uri.parse('$baseUrl/transaction/pdf').replace( + queryParameters: { + 'transactionId': transactionId, + 'copyFor': copyFor, + }, + ), + ); + + final request = http.Request('GET', uri); + request.headers.addAll({ + 'Accept': 'application/pdf', + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + if (token.trim().isNotEmpty) 'Authorization': 'Bearer ${token.trim()}', + }); + + final streamed = await _client.send(request); + final bytes = await streamed.stream.toBytes(); + final contentType = streamed.headers['content-type']?.trim() ?? ''; + + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + final body = const Utf8Decoder( + allowMalformed: true, + ).convert(bytes).trim(); + throw Exception( + body.isNotEmpty && body.length <= 500 + ? body + : 'Failed to load receipt PDF (${streamed.statusCode}) $contentType', + ); + } + + if (_looksLikePdf(bytes: bytes, contentType: contentType)) { + return bytes; + } + + final body = const Utf8Decoder(allowMalformed: true).convert(bytes).trim(); + throw Exception( + body.isNotEmpty && body.length <= 500 + ? body + : 'Server did not return a PDF ($contentType)', + ); + } + + Uri _normalizeLocalhostForAndroid(Uri uri) { + if (!Platform.isAndroid) { + return uri; + } + if (uri.host != 'localhost') { + return uri; + } + return uri.replace(host: '10.0.2.2'); + } + + bool _looksLikePdf({required List bytes, required String contentType}) { + final type = contentType.toLowerCase(); + if (type.contains('application/pdf')) { + return true; + } + if (bytes.length < 4) { + return false; + } + return bytes[0] == 0x25 && // % + bytes[1] == 0x50 && // P + bytes[2] == 0x44 && // D + bytes[3] == 0x46; // F + } +} diff --git a/lib/data/repositories/api_transaction_repository.dart b/lib/data/repositories/api_transaction_repository.dart index bd945bd..d63e93b 100644 --- a/lib/data/repositories/api_transaction_repository.dart +++ b/lib/data/repositories/api_transaction_repository.dart @@ -1,148 +1,148 @@ -import 'dart:convert'; - -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; -import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; -import 'package:http/http.dart' as http; - -class ApiTransactionRepository implements TransactionRepository { - ApiTransactionRepository({ - required this.baseUrl, - required this.apiSecret, - required http.Client client, - }) : _client = client; - - final String baseUrl; - final String apiSecret; - final http.Client _client; - - @override - Future> getTransactions({ - required String token, - required String serial, - required String range, - int page = 1, - int limit = 8, - String searchTerm = '', - String sort = '', - }) async { - final uri = Uri.parse('$baseUrl/transaction').replace( - queryParameters: { - 'page': page.toString(), - 'limit': limit.toString(), - 'serial': serial, - 'range': range, - if (searchTerm.trim().isNotEmpty) 'search': searchTerm.trim(), - if (sort.trim().isNotEmpty) 'sort': sort.trim(), - }, - ); - - final response = await _client.get( - uri, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiSecret, - 'Authorization': 'Bearer ${token.trim()}', - }, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - return _parseTransactions(response.body); - } - - throw Exception(_extractErrorMessage(response.body)); - } - - List _parseTransactions(String body) { - final decoded = _tryDecode(body); - final list = _extractTransactionList(decoded); - return list - .map((item) => _transactionFromItem(item)) - .whereType() - .toList(); - } - - dynamic _tryDecode(String body) { - try { - return jsonDecode(body); - } catch (_) { - return null; - } - } - - List _extractTransactionList(dynamic decoded) { - if (decoded is List) { - return decoded; - } - if (decoded is Map) { - final direct = decoded['transactions']; - if (direct is List) { - return direct; - } - - final data = decoded['data'] ?? decoded['result'] ?? decoded['payload']; - if (data is List) { - return data; - } - if (data is Map) { - final nested = data['transactions']; - if (nested is List) { - return nested; - } - } - } - return const []; - } - - TransactionRecord? _transactionFromItem(dynamic item) { - if (item is! Map) { - return null; - } - - return TransactionRecord( - id: _pickString(item, const ['id', 'transactionId', 'transaction_id']), - rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no', 'DE37']), - amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount', 'DE4']), - status: _pickString(item, const ['status', 'description', 'DE39']), - type: _pickString(item, const ['type', 'transactionType', 'DE3']), - createdAt: _pickString(item, const ['createdAt', 'created_at', 'CREATED_AT']), - raw: item, - ); - } - - String? _pickString(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value != null && value.toString().trim().isNotEmpty) { - return value.toString().trim(); - } - } - return null; - } - - num? _pickNum(Map data, List keys) { - for (final key in keys) { - final value = data[key]; - if (value is num) { - return value; - } - if (value != null) { - final parsed = num.tryParse(value.toString().trim()); - if (parsed != null) { - return parsed; - } - } - } - return null; - } - - String _extractErrorMessage(String body) { - final decoded = _tryDecode(body); - if (decoded is Map) { - final message = decoded['message'] ?? decoded['error'] ?? decoded['detail']; - if (message != null) { - return message.toString(); - } - } - return 'Failed to load transactions'; - } -} +import 'dart:convert'; + +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; +import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; +import 'package:http/http.dart' as http; + +class ApiTransactionRepository implements TransactionRepository { + ApiTransactionRepository({ + required this.baseUrl, + required this.apiSecret, + required http.Client client, + }) : _client = client; + + final String baseUrl; + final String apiSecret; + final http.Client _client; + + @override + Future> getTransactions({ + required String token, + required String serial, + required String range, + int page = 1, + int limit = 8, + String searchTerm = '', + String sort = '', + }) async { + final uri = Uri.parse('$baseUrl/transaction').replace( + queryParameters: { + 'page': page.toString(), + 'limit': limit.toString(), + 'serial': serial, + 'range': range, + if (searchTerm.trim().isNotEmpty) 'search': searchTerm.trim(), + if (sort.trim().isNotEmpty) 'sort': sort.trim(), + }, + ); + + final response = await _client.get( + uri, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiSecret, + 'Authorization': 'Bearer ${token.trim()}', + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return _parseTransactions(response.body); + } + + throw Exception(_extractErrorMessage(response.body)); + } + + List _parseTransactions(String body) { + final decoded = _tryDecode(body); + final list = _extractTransactionList(decoded); + return list + .map((item) => _transactionFromItem(item)) + .whereType() + .toList(); + } + + dynamic _tryDecode(String body) { + try { + return jsonDecode(body); + } catch (_) { + return null; + } + } + + List _extractTransactionList(dynamic decoded) { + if (decoded is List) { + return decoded; + } + if (decoded is Map) { + final direct = decoded['transactions']; + if (direct is List) { + return direct; + } + + final data = decoded['data'] ?? decoded['result'] ?? decoded['payload']; + if (data is List) { + return data; + } + if (data is Map) { + final nested = data['transactions']; + if (nested is List) { + return nested; + } + } + } + return const []; + } + + TransactionRecord? _transactionFromItem(dynamic item) { + if (item is! Map) { + return null; + } + + return TransactionRecord( + id: _pickString(item, const ['id', 'transactionId', 'transaction_id']), + rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no', 'DE37']), + amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount', 'DE4']), + status: _pickString(item, const ['status', 'description', 'DE39']), + type: _pickString(item, const ['type', 'transactionType', 'DE3']), + createdAt: _pickString(item, const ['createdAt', 'created_at', 'CREATED_AT']), + raw: item, + ); + } + + String? _pickString(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value != null && value.toString().trim().isNotEmpty) { + return value.toString().trim(); + } + } + return null; + } + + num? _pickNum(Map data, List keys) { + for (final key in keys) { + final value = data[key]; + if (value is num) { + return value; + } + if (value != null) { + final parsed = num.tryParse(value.toString().trim()); + if (parsed != null) { + return parsed; + } + } + } + return null; + } + + String _extractErrorMessage(String body) { + final decoded = _tryDecode(body); + if (decoded is Map) { + final message = decoded['message'] ?? decoded['error'] ?? decoded['detail']; + if (message != null) { + return message.toString(); + } + } + return 'Failed to load transactions'; + } +} diff --git a/lib/data/repositories/mock_auth_repository.dart b/lib/data/repositories/mock_auth_repository.dart index 3ed6875..045f135 100644 --- a/lib/data/repositories/mock_auth_repository.dart +++ b/lib/data/repositories/mock_auth_repository.dart @@ -1,70 +1,70 @@ -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; - -class MockAuthRepository implements AuthRepository { - @override - Future login({ - required String username, - required String password, - }) async { - await Future.delayed(const Duration(milliseconds: 800)); - - if (username.trim().isEmpty || password.isEmpty) { - throw Exception('Username and password are required.'); - } - - if (password.length < 6) { - throw Exception('Password must be at least 6 characters.'); - } - - return LoginUser( - username: username.trim(), - token: 'mock-access-token', - refreshToken: 'mock-refresh-token', - role: username.trim().toLowerCase() == 'admin' ? 'admin' : 'user', - ); - } - - @override - Future refreshSession({ - required String username, - required String refreshToken, - required String role, - }) async { - await Future.delayed(const Duration(milliseconds: 300)); - - if (refreshToken.trim().isEmpty) { - throw Exception('Refresh token is required.'); - } - - return LoginUser( - username: username.trim(), - token: 'mock-access-token-refreshed', - refreshToken: 'mock-refresh-token-refreshed', - role: role, - ); - } - - @override - Future resetPassword({ - required String userId, - required String password, - }) async { - await Future.delayed(const Duration(milliseconds: 500)); - - if (userId.trim().isEmpty) { - throw Exception('User ID is required.'); - } - if (password.length < 8) { - throw Exception('Password must be at least 8 characters.'); - } - } - - @override - Future logout({required String refreshToken}) async { - await Future.delayed(const Duration(milliseconds: 250)); - if (refreshToken.trim().isEmpty) { - throw Exception('Refresh token is required.'); - } - } -} +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; + +class MockAuthRepository implements AuthRepository { + @override + Future login({ + required String username, + required String password, + }) async { + await Future.delayed(const Duration(milliseconds: 800)); + + if (username.trim().isEmpty || password.isEmpty) { + throw Exception('Username and password are required.'); + } + + if (password.length < 6) { + throw Exception('Password must be at least 6 characters.'); + } + + return LoginUser( + username: username.trim(), + token: 'mock-access-token', + refreshToken: 'mock-refresh-token', + role: username.trim().toLowerCase() == 'admin' ? 'admin' : 'user', + ); + } + + @override + Future refreshSession({ + required String username, + required String refreshToken, + required String role, + }) async { + await Future.delayed(const Duration(milliseconds: 300)); + + if (refreshToken.trim().isEmpty) { + throw Exception('Refresh token is required.'); + } + + return LoginUser( + username: username.trim(), + token: 'mock-access-token-refreshed', + refreshToken: 'mock-refresh-token-refreshed', + role: role, + ); + } + + @override + Future resetPassword({ + required String userId, + required String password, + }) async { + await Future.delayed(const Duration(milliseconds: 500)); + + if (userId.trim().isEmpty) { + throw Exception('User ID is required.'); + } + if (password.length < 8) { + throw Exception('Password must be at least 8 characters.'); + } + } + + @override + Future logout({required String refreshToken}) async { + await Future.delayed(const Duration(milliseconds: 250)); + if (refreshToken.trim().isEmpty) { + throw Exception('Refresh token is required.'); + } + } +} diff --git a/lib/domain/entities/login_user.dart b/lib/domain/entities/login_user.dart index c0e1f07..c4ff371 100644 --- a/lib/domain/entities/login_user.dart +++ b/lib/domain/entities/login_user.dart @@ -1,17 +1,17 @@ -class LoginUser { - const LoginUser({ - required this.username, - required this.token, - required this.refreshToken, - required this.role, - }); - - final String username; - final String token; - final String refreshToken; - final String role; - - bool get isAdmin => role.toLowerCase() == 'admin'; - - bool get isCashier => role.toLowerCase() == 'cashier'; -} +class LoginUser { + const LoginUser({ + required this.username, + required this.token, + required this.refreshToken, + required this.role, + }); + + final String username; + final String token; + final String refreshToken; + final String role; + + bool get isAdmin => role.toLowerCase() == 'admin'; + + bool get isCashier => role.toLowerCase() == 'cashier'; +} diff --git a/lib/domain/entities/merchant.dart b/lib/domain/entities/merchant.dart index 6482bc0..8d89f08 100644 --- a/lib/domain/entities/merchant.dart +++ b/lib/domain/entities/merchant.dart @@ -1,31 +1,31 @@ -class Merchant { - const Merchant({ - this.id, - this.name, - this.mobile, - this.description, - this.address, - this.address2, - this.address3, - this.phone, - this.mids, - this.createdBy, - this.updatedBy, - this.createdAt, - this.updatedAt, - }); - - final String? id; - final String? name; - final String? mobile; - final String? description; - final String? address; - final String? address2; - final String? address3; - final String? phone; - final String? mids; - final String? createdBy; - final String? updatedBy; - final String? createdAt; - final String? updatedAt; -} +class Merchant { + const Merchant({ + this.id, + this.name, + this.mobile, + this.description, + this.address, + this.address2, + this.address3, + this.phone, + this.mids, + this.createdBy, + this.updatedBy, + this.createdAt, + this.updatedAt, + }); + + final String? id; + final String? name; + final String? mobile; + final String? description; + final String? address; + final String? address2; + final String? address3; + final String? phone; + final String? mids; + final String? createdBy; + final String? updatedBy; + final String? createdAt; + final String? updatedAt; +} diff --git a/lib/domain/entities/receipt_content.dart b/lib/domain/entities/receipt_content.dart index 949213a..d0b41bc 100644 --- a/lib/domain/entities/receipt_content.dart +++ b/lib/domain/entities/receipt_content.dart @@ -1,18 +1,18 @@ -import 'dart:typed_data'; - -sealed class ReceiptContent { - const ReceiptContent(); -} - -class ReceiptPdfContent extends ReceiptContent { - const ReceiptPdfContent(this.bytes); - - final Uint8List bytes; -} - -class ReceiptHtmlContent extends ReceiptContent { - const ReceiptHtmlContent({required this.html, required this.contentType}); - - final String html; - final String contentType; -} +import 'dart:typed_data'; + +sealed class ReceiptContent { + const ReceiptContent(); +} + +class ReceiptPdfContent extends ReceiptContent { + const ReceiptPdfContent(this.bytes); + + final Uint8List bytes; +} + +class ReceiptHtmlContent extends ReceiptContent { + const ReceiptHtmlContent({required this.html, required this.contentType}); + + final String html; + final String contentType; +} diff --git a/lib/domain/entities/terminal.dart b/lib/domain/entities/terminal.dart index 246df94..703a981 100644 --- a/lib/domain/entities/terminal.dart +++ b/lib/domain/entities/terminal.dart @@ -1,35 +1,35 @@ -class Terminal { - const Terminal({ - this.id, - this.serial, - this.address, - this.address2, - this.address3, - this.name, - this.appId, - this.tid, - this.mids, - this.merchantId, - this.status, - this.totalTransactions, - this.totalAmount, - this.createdAt, - this.updatedAt, - }); - - final String? id; - final String? serial; - final String? address; - final String? address2; - final String? address3; - final String? name; - final String? appId; - final String? tid; - final List? mids; - final String? merchantId; - final String? status; - final int? totalTransactions; - final num? totalAmount; - final String? createdAt; - final String? updatedAt; -} +class Terminal { + const Terminal({ + this.id, + this.serial, + this.address, + this.address2, + this.address3, + this.name, + this.appId, + this.tid, + this.mids, + this.merchantId, + this.status, + this.totalTransactions, + this.totalAmount, + this.createdAt, + this.updatedAt, + }); + + final String? id; + final String? serial; + final String? address; + final String? address2; + final String? address3; + final String? name; + final String? appId; + final String? tid; + final List? mids; + final String? merchantId; + final String? status; + final int? totalTransactions; + final num? totalAmount; + final String? createdAt; + final String? updatedAt; +} diff --git a/lib/domain/entities/transaction_record.dart b/lib/domain/entities/transaction_record.dart index 91f4b12..0bbe64c 100644 --- a/lib/domain/entities/transaction_record.dart +++ b/lib/domain/entities/transaction_record.dart @@ -1,19 +1,19 @@ -class TransactionRecord { - const TransactionRecord({ - this.id, - this.rrn, - this.amount, - this.status, - this.type, - this.createdAt, - this.raw, - }); - - final String? id; - final String? rrn; - final num? amount; - final String? status; - final String? type; - final String? createdAt; - final Map? raw; -} +class TransactionRecord { + const TransactionRecord({ + this.id, + this.rrn, + this.amount, + this.status, + this.type, + this.createdAt, + this.raw, + }); + + final String? id; + final String? rrn; + final num? amount; + final String? status; + final String? type; + final String? createdAt; + final Map? raw; +} diff --git a/lib/domain/exceptions/auth_exceptions.dart b/lib/domain/exceptions/auth_exceptions.dart index 02f9832..f42da54 100644 --- a/lib/domain/exceptions/auth_exceptions.dart +++ b/lib/domain/exceptions/auth_exceptions.dart @@ -1,13 +1,13 @@ -class PasswordUpdateRequiredException implements Exception { - const PasswordUpdateRequiredException({ - required this.userId, - this.message = 'Password update required', - }); - - final String userId; - final String message; - - @override - String toString() => message; -} - +class PasswordUpdateRequiredException implements Exception { + const PasswordUpdateRequiredException({ + required this.userId, + this.message = 'Password update required', + }); + + final String userId; + final String message; + + @override + String toString() => message; +} + diff --git a/lib/domain/repositories/auth_repository.dart b/lib/domain/repositories/auth_repository.dart index cb898ec..5c0a4b6 100644 --- a/lib/domain/repositories/auth_repository.dart +++ b/lib/domain/repositories/auth_repository.dart @@ -1,15 +1,15 @@ -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; - -abstract class AuthRepository { - Future login({required String username, required String password}); - - Future refreshSession({ - required String username, - required String refreshToken, - required String role, - }); - - Future resetPassword({required String userId, required String password}); - - Future logout({required String refreshToken}); -} +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; + +abstract class AuthRepository { + Future login({required String username, required String password}); + + Future refreshSession({ + required String username, + required String refreshToken, + required String role, + }); + + Future resetPassword({required String userId, required String password}); + + Future logout({required String refreshToken}); +} diff --git a/lib/domain/repositories/merchant_repository.dart b/lib/domain/repositories/merchant_repository.dart index a41a566..df7e8a0 100644 --- a/lib/domain/repositories/merchant_repository.dart +++ b/lib/domain/repositories/merchant_repository.dart @@ -1,16 +1,16 @@ -import 'package:e_receipt_mobile/domain/entities/merchant.dart'; -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; - -abstract class MerchantRepository { - Future> getMerchants({ - required String token, - int page = 1, - int limit = 10, - String searchTerm = '', - }); - - Future> getTerminalsByMerchantId({ - required String token, - required String merchantId, - }); -} +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; + +abstract class MerchantRepository { + Future> getMerchants({ + required String token, + int page = 1, + int limit = 10, + String searchTerm = '', + }); + + Future> getTerminalsByMerchantId({ + required String token, + required String merchantId, + }); +} diff --git a/lib/domain/repositories/receipt_repository.dart b/lib/domain/repositories/receipt_repository.dart index f2f7c52..44a6dd7 100644 --- a/lib/domain/repositories/receipt_repository.dart +++ b/lib/domain/repositories/receipt_repository.dart @@ -1,17 +1,17 @@ -import 'dart:typed_data'; - -import 'package:e_receipt_mobile/domain/entities/receipt_content.dart'; - -abstract class ReceiptRepository { - Future getTransactionReceipt({ - required String token, - required String transactionId, - required String copyFor, - }); - - Future getTransactionReceiptPdfBytes({ - required String token, - required String transactionId, - required String copyFor, - }); -} +import 'dart:typed_data'; + +import 'package:e_receipt_mobile/domain/entities/receipt_content.dart'; + +abstract class ReceiptRepository { + Future getTransactionReceipt({ + required String token, + required String transactionId, + required String copyFor, + }); + + Future getTransactionReceiptPdfBytes({ + required String token, + required String transactionId, + required String copyFor, + }); +} diff --git a/lib/domain/repositories/transaction_repository.dart b/lib/domain/repositories/transaction_repository.dart index 105a340..d697a04 100644 --- a/lib/domain/repositories/transaction_repository.dart +++ b/lib/domain/repositories/transaction_repository.dart @@ -1,13 +1,13 @@ -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; - -abstract class TransactionRepository { - Future> getTransactions({ - required String token, - required String serial, - required String range, - int page = 1, - int limit = 8, - String searchTerm = '', - String sort = '', - }); -} +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; + +abstract class TransactionRepository { + Future> getTransactions({ + required String token, + required String serial, + required String range, + int page = 1, + int limit = 8, + String searchTerm = '', + String sort = '', + }); +} diff --git a/lib/main.dart b/lib/main.dart index b4b2dcf..c5df4df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,96 +1,96 @@ -import 'package:e_receipt_mobile/core/theme/app_colors.dart'; -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/home/home_screen.dart'; -import 'package:e_receipt_mobile/presentation/login/login_page.dart'; -import 'package:e_receipt_mobile/presentation/settings/theme_mode_provider.dart'; -import 'package:e_receipt_mobile/presentation/terminal/terminal_next_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - ); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: AppColors.primary, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), - ); - runApp(const ProviderScope(child: EReceiptApp())); -} - -class EReceiptApp extends ConsumerWidget { - const EReceiptApp({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch(appThemeModeProvider); - - return MaterialApp( - title: 'E-Receipt', - debugShowCheckedModeBanner: false, - themeMode: themeMode, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - ), - primaryColor: AppColors.primary, - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: AppColors.primary, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), - ), - useMaterial3: true, - ), - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - brightness: Brightness.dark, - ), - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: AppColors.primary, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), - ), - useMaterial3: true, - ), - home: const AppRoot(), - ); - } -} - -class AppRoot extends ConsumerWidget { - const AppRoot({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(sessionControllerProvider); - if (user == null) { - return const LoginPage(); - } - - final role = user.role.trim().toLowerCase(); - if (role == 'cashier') { - final serial = user.username.trim().toUpperCase(); - final terminal = Terminal(serial: serial, name: serial); - return TerminalNextScreen(merchantName: serial, terminal: terminal); - } - - return HomeScreen(user: user); - } -} +import 'package:e_receipt_mobile/core/theme/app_colors.dart'; +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/home/home_screen.dart'; +import 'package:e_receipt_mobile/presentation/login/login_page.dart'; +import 'package:e_receipt_mobile/presentation/settings/theme_mode_provider.dart'; +import 'package:e_receipt_mobile/presentation/terminal/terminal_next_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: AppColors.primary, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), + ); + runApp(const ProviderScope(child: EReceiptApp())); +} + +class EReceiptApp extends ConsumerWidget { + const EReceiptApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(appThemeModeProvider); + + return MaterialApp( + title: 'E-Receipt', + debugShowCheckedModeBanner: false, + themeMode: themeMode, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + ), + primaryColor: AppColors.primary, + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: AppColors.primary, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), + ), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + brightness: Brightness.dark, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: AppColors.primary, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), + ), + useMaterial3: true, + ), + home: const AppRoot(), + ); + } +} + +class AppRoot extends ConsumerWidget { + const AppRoot({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(sessionControllerProvider); + if (user == null) { + return const LoginPage(); + } + + final role = user.role.trim().toLowerCase(); + if (role == 'cashier') { + final serial = user.username.trim().toUpperCase(); + final terminal = Terminal(serial: serial, name: serial); + return TerminalNextScreen(merchantName: serial, terminal: terminal); + } + + return HomeScreen(user: user); + } +} diff --git a/lib/presentation/auth/logout_state.dart b/lib/presentation/auth/logout_state.dart index 7f1a581..07a1314 100644 --- a/lib/presentation/auth/logout_state.dart +++ b/lib/presentation/auth/logout_state.dart @@ -1,24 +1,24 @@ -import 'package:flutter/foundation.dart'; - -@immutable -class LogoutState { - const LogoutState({ - this.isLoading = false, - this.lastError, - }); - - final bool isLoading; - final Object? lastError; - - LogoutState copyWith({ - bool? isLoading, - Object? lastError, - bool clearError = false, - }) { - return LogoutState( - isLoading: isLoading ?? this.isLoading, - lastError: clearError ? null : lastError ?? this.lastError, - ); - } -} - +import 'package:flutter/foundation.dart'; + +@immutable +class LogoutState { + const LogoutState({ + this.isLoading = false, + this.lastError, + }); + + final bool isLoading; + final Object? lastError; + + LogoutState copyWith({ + bool? isLoading, + Object? lastError, + bool clearError = false, + }) { + return LogoutState( + isLoading: isLoading ?? this.isLoading, + lastError: clearError ? null : lastError ?? this.lastError, + ); + } +} + diff --git a/lib/presentation/auth/logout_view_model.dart b/lib/presentation/auth/logout_view_model.dart index 1796714..af33daf 100644 --- a/lib/presentation/auth/logout_view_model.dart +++ b/lib/presentation/auth/logout_view_model.dart @@ -1,54 +1,54 @@ -import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:e_receipt_mobile/presentation/auth/logout_state.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final logoutViewModelProvider = - StateNotifierProvider((ref) { - return LogoutViewModel( - authRepository: ref.watch(authRepositoryProvider), - sessionController: ref.watch(sessionControllerProvider.notifier), - sessionUserProvider: () => ref.read(sessionControllerProvider), - ); -}); - -class LogoutViewModel extends StateNotifier { - LogoutViewModel({ - required AuthRepository authRepository, - required SessionController sessionController, - required SessionUserProvider sessionUserProvider, - }) : _authRepository = authRepository, - _sessionController = sessionController, - _sessionUserProvider = sessionUserProvider, - super(const LogoutState()); - - final AuthRepository _authRepository; - final SessionController _sessionController; - final SessionUserProvider _sessionUserProvider; - - Future logout() async { - if (state.isLoading) { - return; - } - - state = state.copyWith(isLoading: true, clearError: true); - - final sessionUser = _sessionUserProvider(); - final refreshToken = sessionUser?.refreshToken.trim() ?? ''; - - try { - if (refreshToken.isNotEmpty) { - await _authRepository.logout(refreshToken: refreshToken); - } - } catch (error) { - state = state.copyWith(lastError: error); - } finally { - _sessionController.clearUser(); - state = state.copyWith(isLoading: false); - } - } -} - -typedef SessionUserProvider = LoginUser? Function(); +import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:e_receipt_mobile/presentation/auth/logout_state.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final logoutViewModelProvider = + StateNotifierProvider((ref) { + return LogoutViewModel( + authRepository: ref.watch(authRepositoryProvider), + sessionController: ref.watch(sessionControllerProvider.notifier), + sessionUserProvider: () => ref.read(sessionControllerProvider), + ); +}); + +class LogoutViewModel extends StateNotifier { + LogoutViewModel({ + required AuthRepository authRepository, + required SessionController sessionController, + required SessionUserProvider sessionUserProvider, + }) : _authRepository = authRepository, + _sessionController = sessionController, + _sessionUserProvider = sessionUserProvider, + super(const LogoutState()); + + final AuthRepository _authRepository; + final SessionController _sessionController; + final SessionUserProvider _sessionUserProvider; + + Future logout() async { + if (state.isLoading) { + return; + } + + state = state.copyWith(isLoading: true, clearError: true); + + final sessionUser = _sessionUserProvider(); + final refreshToken = sessionUser?.refreshToken.trim() ?? ''; + + try { + if (refreshToken.isNotEmpty) { + await _authRepository.logout(refreshToken: refreshToken); + } + } catch (error) { + state = state.copyWith(lastError: error); + } finally { + _sessionController.clearUser(); + state = state.copyWith(isLoading: false); + } + } +} + +typedef SessionUserProvider = LoginUser? Function(); diff --git a/lib/presentation/auth/session_controller.dart b/lib/presentation/auth/session_controller.dart index dd48627..862a9fa 100644 --- a/lib/presentation/auth/session_controller.dart +++ b/lib/presentation/auth/session_controller.dart @@ -1,19 +1,19 @@ -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final sessionControllerProvider = - StateNotifierProvider((ref) { - return SessionController(); - }); - -class SessionController extends StateNotifier { - SessionController() : super(null); - - void setUser(LoginUser user) { - state = user; - } - - void clearUser() { - state = null; - } -} +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final sessionControllerProvider = + StateNotifierProvider((ref) { + return SessionController(); + }); + +class SessionController extends StateNotifier { + SessionController() : super(null); + + void setUser(LoginUser user) { + state = user; + } + + void clearUser() { + state = null; + } +} diff --git a/lib/presentation/components/rounded_input.dart b/lib/presentation/components/rounded_input.dart index d8863e7..5556cf5 100644 --- a/lib/presentation/components/rounded_input.dart +++ b/lib/presentation/components/rounded_input.dart @@ -1,80 +1,80 @@ -import 'package:flutter/material.dart'; - -Widget roundedInput({ - required TextEditingController controller, - required String hint, - required IconData icon, - bool isPassword = false, - String? Function(String?)? validator, -}) { - return RoundedInput( - controller: controller, - hint: hint, - icon: icon, - isPassword: isPassword, - validator: validator, - ); -} - -class RoundedInput extends StatefulWidget { - const RoundedInput({ - super.key, - required this.controller, - required this.hint, - required this.icon, - this.isPassword = false, - this.validator, - }); - - final TextEditingController controller; - final String hint; - final IconData icon; - final bool isPassword; - final String? Function(String?)? validator; - - @override - State createState() => _RoundedInputState(); -} - -class _RoundedInputState extends State { - late bool _obscured; - - @override - void initState() { - super.initState(); - _obscured = widget.isPassword; - } - - @override - Widget build(BuildContext context) { - return TextFormField( - controller: widget.controller, - obscureText: _obscured, - validator: widget.validator, - decoration: InputDecoration( - hintText: widget.hint, - prefixIcon: Icon(widget.icon, color: Colors.grey), - suffixIcon: widget.isPassword - ? IconButton( - onPressed: () { - setState(() { - _obscured = !_obscured; - }); - }, - icon: Icon( - _obscured ? Icons.visibility : Icons.visibility_off, - color: Colors.grey, - ), - ) - : null, - filled: true, - fillColor: const Color(0xFFE9EEF3), - contentPadding: const EdgeInsets.symmetric(vertical: 16), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(30), - borderSide: BorderSide.none, - ), - ), - ); - } -} +import 'package:flutter/material.dart'; + +Widget roundedInput({ + required TextEditingController controller, + required String hint, + required IconData icon, + bool isPassword = false, + String? Function(String?)? validator, +}) { + return RoundedInput( + controller: controller, + hint: hint, + icon: icon, + isPassword: isPassword, + validator: validator, + ); +} + +class RoundedInput extends StatefulWidget { + const RoundedInput({ + super.key, + required this.controller, + required this.hint, + required this.icon, + this.isPassword = false, + this.validator, + }); + + final TextEditingController controller; + final String hint; + final IconData icon; + final bool isPassword; + final String? Function(String?)? validator; + + @override + State createState() => _RoundedInputState(); +} + +class _RoundedInputState extends State { + late bool _obscured; + + @override + void initState() { + super.initState(); + _obscured = widget.isPassword; + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: widget.controller, + obscureText: _obscured, + validator: widget.validator, + decoration: InputDecoration( + hintText: widget.hint, + prefixIcon: Icon(widget.icon, color: Colors.grey), + suffixIcon: widget.isPassword + ? IconButton( + onPressed: () { + setState(() { + _obscured = !_obscured; + }); + }, + icon: Icon( + _obscured ? Icons.visibility : Icons.visibility_off, + color: Colors.grey, + ), + ) + : null, + filled: true, + fillColor: const Color(0xFFE9EEF3), + contentPadding: const EdgeInsets.symmetric(vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ); + } +} diff --git a/lib/presentation/home/home_pagination_providers.dart b/lib/presentation/home/home_pagination_providers.dart index b022413..2635820 100644 --- a/lib/presentation/home/home_pagination_providers.dart +++ b/lib/presentation/home/home_pagination_providers.dart @@ -1,7 +1,7 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final merchantPageSizeProvider = Provider((ref) => 10); - -final merchantSearchQueryProvider = StateProvider.autoDispose((ref) { - return ''; -}); +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final merchantPageSizeProvider = Provider((ref) => 10); + +final merchantSearchQueryProvider = StateProvider.autoDispose((ref) { + return ''; +}); diff --git a/lib/presentation/home/home_screen.dart b/lib/presentation/home/home_screen.dart index 014fdc7..2f75560 100644 --- a/lib/presentation/home/home_screen.dart +++ b/lib/presentation/home/home_screen.dart @@ -1,264 +1,214 @@ -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:e_receipt_mobile/presentation/auth/logout_view_model.dart'; -import 'package:e_receipt_mobile/presentation/home/home_pagination_providers.dart'; -import 'package:e_receipt_mobile/presentation/home/merchant_paging_view_model.dart'; -import 'package:e_receipt_mobile/presentation/home/widgets/home_drawer.dart'; -import 'package:e_receipt_mobile/presentation/home/widgets/merchant_header.dart'; -import 'package:e_receipt_mobile/presentation/home/widgets/merchant_list_view.dart'; -import 'package:e_receipt_mobile/presentation/settings/settings_screen.dart'; -import 'package:e_receipt_mobile/presentation/terminal/terminal_selection_screen.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class HomeScreen extends ConsumerWidget { - const HomeScreen({required this.user, super.key}); - - final LoginUser user; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pagingState = ref.watch(merchantPagingViewModelProvider); - final pagingViewModel = ref.read(merchantPagingViewModelProvider.notifier); - final logoutState = ref.watch(logoutViewModelProvider); - final colorScheme = Theme.of(context).colorScheme; - final query = ref.watch(merchantSearchQueryProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('Merchants'), - actions: [ - IconButton( - tooltip: 'Refresh', - onPressed: pagingState.isLoading - ? null - : () => pagingViewModel.refresh(), - icon: const Icon(Icons.refresh), - ), - ], - ), - drawer: HomeDrawer( - user: user, - onProfile: () { - Navigator.of(context).pop(); - _showProfile(context, user); - }, - onReports: () { - Navigator.of(context).pop(); - _showComingSoon(context, 'Reports'); - }, - onSettings: () { - Navigator.of(context).pop(); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SettingsScreen()), - ); - }, - onHelp: () { - Navigator.of(context).pop(); - _showComingSoon(context, 'Help'); - }, - onLogout: () async { - if (logoutState.isLoading) { - return; - } - - final shouldLogout = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Logout'), - content: const Text('Are you sure you want to logout?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Logout'), - ), - ], - ); - }, - ); - - if (shouldLogout != true || !context.mounted) { - return; - } - - Navigator.of(context).pop(); // close drawer - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Signed out'), - behavior: SnackBarBehavior.floating, - ), - ); - await ref.read(logoutViewModelProvider.notifier).logout(); - }, - ), - body: pagingState.isLoading && pagingState.items.isEmpty - ? const Center(child: CircularProgressIndicator()) - : pagingState.errorMessage != null && pagingState.items.isEmpty - ? Center( - child: Padding( - padding: EdgeInsets.fromLTRB( - 24, - 24, - 24, - MediaQuery.viewPaddingOf(context).bottom + 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.wifi_off_outlined, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'Failed to load merchants', - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Text( - pagingState.errorMessage!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 14), - FilledButton.tonal( - onPressed: pagingViewModel.refresh, - child: const Text('Try again'), - ), - ], - ), - ), - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MerchantHeader( - loadedCount: pagingState.items.length, - query: query, - onQueryChanged: (value) { - ref.read(merchantSearchQueryProvider.notifier).state = - value; - pagingViewModel.setSearchTerm(value); - }, - onClear: () { - ref.read(merchantSearchQueryProvider.notifier).state = ''; - pagingViewModel.setSearchTerm(''); - }, - ), - Expanded( - child: MerchantListView( - merchants: pagingState.items, - hasMore: pagingState.hasMore, - isLoadingMore: pagingState.isLoadingMore, - onRefresh: pagingViewModel.refresh, - onMerchantTap: (merchant) { - final id = merchant.id; - if (id == null) { - return; - } - _openTerminalSelection(context, id, merchant.name ?? '-'); - }, - onLoadMore: pagingViewModel.loadMore, - onEndReached: pagingViewModel.loadMore, - ), - ), - if (pagingState.errorMessage != null && - pagingState.items.isNotEmpty) - Padding( - padding: EdgeInsets.fromLTRB( - 16, - 8, - 16, - MediaQuery.viewPaddingOf(context).bottom + 8, - ), - child: Material( - color: colorScheme.errorContainer, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: colorScheme.onErrorContainer, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - pagingState.errorMessage!, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: colorScheme.onErrorContainer, - ), - ), - ), - const SizedBox(width: 8), - TextButton( - onPressed: pagingViewModel.loadMore, - child: const Text('Retry'), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - void _showComingSoon(BuildContext context, String feature) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('$feature is coming soon'))); - } - - void _showProfile(BuildContext context, LoginUser user) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Profile'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Username: ${user.username}'), - const SizedBox(height: 8), - Text('Role: ${user.role}'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], - ); - }, - ); - } - - void _openTerminalSelection( - BuildContext context, - String merchantId, - String merchantName, - ) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => TerminalSelectionScreen( - merchantId: merchantId, - merchantName: merchantName, - ), - ), - ); - } -} +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:e_receipt_mobile/presentation/auth/logout_view_model.dart'; +import 'package:e_receipt_mobile/presentation/home/home_pagination_providers.dart'; +import 'package:e_receipt_mobile/presentation/home/merchant_paging_view_model.dart'; +import 'package:e_receipt_mobile/presentation/home/widgets/home_drawer.dart'; +import 'package:e_receipt_mobile/presentation/home/widgets/merchant_header.dart'; +import 'package:e_receipt_mobile/presentation/home/widgets/merchant_list_view.dart'; +import 'package:e_receipt_mobile/presentation/terminal/terminal_selection_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class HomeScreen extends ConsumerWidget { + const HomeScreen({required this.user, super.key}); + + final LoginUser user; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pagingState = ref.watch(merchantPagingViewModelProvider); + final pagingViewModel = ref.read(merchantPagingViewModelProvider.notifier); + final logoutState = ref.watch(logoutViewModelProvider); + final colorScheme = Theme.of(context).colorScheme; + final query = ref.watch(merchantSearchQueryProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Merchants'), + actions: [ + IconButton( + tooltip: 'Refresh', + onPressed: pagingState.isLoading + ? null + : () => pagingViewModel.refresh(), + icon: const Icon(Icons.refresh), + ), + ], + ), + drawer: HomeDrawer( + user: user, + onLogout: () async { + if (logoutState.isLoading) { + return; + } + final shouldLogout = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Logout'), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Logout'), + ), + ], + ); + }, + ); + + if (shouldLogout != true || !context.mounted) { + return; + } + + Navigator.of(context).pop(); // close drawer + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Signed out'), + behavior: SnackBarBehavior.floating, + ), + ); + await ref.read(logoutViewModelProvider.notifier).logout(); + }, + ), + body: pagingState.isLoading && pagingState.items.isEmpty + ? const Center(child: CircularProgressIndicator()) + : pagingState.errorMessage != null && pagingState.items.isEmpty + ? Center( + child: Padding( + padding: EdgeInsets.fromLTRB( + 24, + 24, + 24, + MediaQuery.viewPaddingOf(context).bottom + 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.wifi_off_outlined, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'Failed to load merchants', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + pagingState.errorMessage!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 14), + FilledButton.tonal( + onPressed: pagingViewModel.refresh, + child: const Text('Try again'), + ), + ], + ), + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MerchantHeader( + loadedCount: pagingState.items.length, + query: query, + onQueryChanged: (value) { + ref.read(merchantSearchQueryProvider.notifier).state = + value; + pagingViewModel.setSearchTerm(value); + }, + onClear: () { + ref.read(merchantSearchQueryProvider.notifier).state = ''; + pagingViewModel.setSearchTerm(''); + }, + ), + Expanded( + child: MerchantListView( + merchants: pagingState.items, + hasMore: pagingState.hasMore, + isLoadingMore: pagingState.isLoadingMore, + onRefresh: pagingViewModel.refresh, + onMerchantTap: (merchant) { + final id = merchant.id; + if (id == null) { + return; + } + _openTerminalSelection(context, id, merchant.name ?? '-'); + }, + onLoadMore: pagingViewModel.loadMore, + onEndReached: pagingViewModel.loadMore, + ), + ), + if (pagingState.errorMessage != null && + pagingState.items.isNotEmpty) + Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + MediaQuery.viewPaddingOf(context).bottom + 8, + ), + child: Material( + color: colorScheme.errorContainer, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: colorScheme.onErrorContainer, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + pagingState.errorMessage!, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: colorScheme.onErrorContainer, + ), + ), + ), + const SizedBox(width: 8), + TextButton( + onPressed: pagingViewModel.loadMore, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + + + void _openTerminalSelection( + BuildContext context, + String merchantId, + String merchantName, + ) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TerminalSelectionScreen( + merchantId: merchantId, + merchantName: merchantName, + ), + ), + ); + } +} diff --git a/lib/presentation/home/home_view_model.dart b/lib/presentation/home/home_view_model.dart index 8ccbd89..5508d31 100644 --- a/lib/presentation/home/home_view_model.dart +++ b/lib/presentation/home/home_view_model.dart @@ -1,32 +1,32 @@ -import 'package:e_receipt_mobile/core/config/app_config.dart'; -import 'package:e_receipt_mobile/data/repositories/api_merchant_repository.dart'; -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final merchantRepositoryProvider = Provider((ref) { - return ApiMerchantRepository( - baseUrl: AppConfig.apiBaseUrl, - apiSecret: AppConfig.apiSecret, - client: ref.watch(authenticatedHttpClientProvider), - ); -}); - -final terminalListProvider = FutureProvider.family, String>(( - ref, - merchantId, -) async { - final sessionUser = ref.watch(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - return ref - .watch(merchantRepositoryProvider) - .getTerminalsByMerchantId( - token: sessionUser.token, - merchantId: merchantId, - ); -}); +import 'package:e_receipt_mobile/core/config/app_config.dart'; +import 'package:e_receipt_mobile/data/repositories/api_merchant_repository.dart'; +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final merchantRepositoryProvider = Provider((ref) { + return ApiMerchantRepository( + baseUrl: AppConfig.apiBaseUrl, + apiSecret: AppConfig.apiSecret, + client: ref.watch(authenticatedHttpClientProvider), + ); +}); + +final terminalListProvider = FutureProvider.family, String>(( + ref, + merchantId, +) async { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + return ref + .watch(merchantRepositoryProvider) + .getTerminalsByMerchantId( + token: sessionUser.token, + merchantId: merchantId, + ); +}); diff --git a/lib/presentation/home/merchant_paging_state.dart b/lib/presentation/home/merchant_paging_state.dart index 06c4612..1e011a8 100644 --- a/lib/presentation/home/merchant_paging_state.dart +++ b/lib/presentation/home/merchant_paging_state.dart @@ -1,49 +1,49 @@ -import 'package:e_receipt_mobile/domain/entities/merchant.dart'; -import 'package:flutter/foundation.dart'; - -@immutable -class MerchantPagingState { - const MerchantPagingState({ - this.items = const [], - this.page = 1, - this.limit = 10, - this.searchTerm = '', - this.isLoading = false, - this.isLoadingMore = false, - this.hasMore = true, - this.errorMessage, - }); - - final List items; - final int page; - final int limit; - final String searchTerm; - final bool isLoading; - final bool isLoadingMore; - final bool hasMore; - final String? errorMessage; - - MerchantPagingState copyWith({ - List? items, - int? page, - int? limit, - String? searchTerm, - bool? isLoading, - bool? isLoadingMore, - bool? hasMore, - String? errorMessage, - bool clearError = false, - }) { - return MerchantPagingState( - items: items ?? this.items, - page: page ?? this.page, - limit: limit ?? this.limit, - searchTerm: searchTerm ?? this.searchTerm, - isLoading: isLoading ?? this.isLoading, - isLoadingMore: isLoadingMore ?? this.isLoadingMore, - hasMore: hasMore ?? this.hasMore, - errorMessage: clearError ? null : errorMessage ?? this.errorMessage, - ); - } -} - +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class MerchantPagingState { + const MerchantPagingState({ + this.items = const [], + this.page = 1, + this.limit = 10, + this.searchTerm = '', + this.isLoading = false, + this.isLoadingMore = false, + this.hasMore = true, + this.errorMessage, + }); + + final List items; + final int page; + final int limit; + final String searchTerm; + final bool isLoading; + final bool isLoadingMore; + final bool hasMore; + final String? errorMessage; + + MerchantPagingState copyWith({ + List? items, + int? page, + int? limit, + String? searchTerm, + bool? isLoading, + bool? isLoadingMore, + bool? hasMore, + String? errorMessage, + bool clearError = false, + }) { + return MerchantPagingState( + items: items ?? this.items, + page: page ?? this.page, + limit: limit ?? this.limit, + searchTerm: searchTerm ?? this.searchTerm, + isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasMore: hasMore ?? this.hasMore, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + ); + } +} + diff --git a/lib/presentation/home/merchant_paging_view_model.dart b/lib/presentation/home/merchant_paging_view_model.dart index bede687..dd4e124 100644 --- a/lib/presentation/home/merchant_paging_view_model.dart +++ b/lib/presentation/home/merchant_paging_view_model.dart @@ -1,130 +1,130 @@ -import 'dart:async'; - -import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/home/home_pagination_providers.dart'; -import 'package:e_receipt_mobile/presentation/home/home_view_model.dart'; -import 'package:e_receipt_mobile/presentation/home/merchant_paging_state.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final merchantPagingViewModelProvider = - StateNotifierProvider.autoDispose< - MerchantPagingViewModel, - MerchantPagingState - >((ref) { - final sessionUser = ref.watch(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - final limit = ref.watch(merchantPageSizeProvider); - final viewModel = MerchantPagingViewModel( - repository: ref.watch(merchantRepositoryProvider), - token: sessionUser.token, - limit: limit, - searchTerm: '', - ); - ref.onDispose(viewModel.dispose); - viewModel.loadInitial(); - return viewModel; - }); - -class MerchantPagingViewModel extends StateNotifier { - MerchantPagingViewModel({ - required MerchantRepository repository, - required String token, - required int limit, - required String searchTerm, - }) : _repository = repository, - _token = token, - super(MerchantPagingState(limit: limit, searchTerm: searchTerm)); - - final MerchantRepository _repository; - final String _token; - - Timer? _searchDebounce; - - void dispose() { - _searchDebounce?.cancel(); - } - - Future loadInitial() async { - await refresh(searchTerm: state.searchTerm); - } - - Future refresh({String? searchTerm}) async { - final nextSearch = searchTerm ?? state.searchTerm; - state = state.copyWith( - isLoading: true, - isLoadingMore: false, - page: 1, - items: const [], - hasMore: true, - searchTerm: nextSearch, - clearError: true, - ); - - try { - final results = await _repository.getMerchants( - token: _token, - page: 1, - limit: state.limit, - searchTerm: nextSearch, - ); - state = state.copyWith( - isLoading: false, - items: results, - page: 1, - hasMore: results.length >= state.limit, - ); - } catch (error) { - state = state.copyWith( - isLoading: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } - - void setSearchTerm(String value) { - final next = value; - if (next == state.searchTerm) { - return; - } - - _searchDebounce?.cancel(); - _searchDebounce = Timer(const Duration(milliseconds: 350), () { - refresh(searchTerm: next); - }); - } - - Future loadMore() async { - if (state.isLoading || state.isLoadingMore || !state.hasMore) { - return; - } - - state = state.copyWith(isLoadingMore: true, clearError: true); - final nextPage = state.page + 1; - - try { - final results = await _repository.getMerchants( - token: _token, - page: nextPage, - limit: state.limit, - searchTerm: state.searchTerm, - ); - - final merged = [...state.items, ...results]; - state = state.copyWith( - isLoadingMore: false, - items: merged, - page: nextPage, - hasMore: results.length >= state.limit, - ); - } catch (error) { - state = state.copyWith( - isLoadingMore: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } -} +import 'dart:async'; + +import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/home/home_pagination_providers.dart'; +import 'package:e_receipt_mobile/presentation/home/home_view_model.dart'; +import 'package:e_receipt_mobile/presentation/home/merchant_paging_state.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final merchantPagingViewModelProvider = + StateNotifierProvider.autoDispose< + MerchantPagingViewModel, + MerchantPagingState + >((ref) { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + final limit = ref.watch(merchantPageSizeProvider); + final viewModel = MerchantPagingViewModel( + repository: ref.watch(merchantRepositoryProvider), + token: sessionUser.token, + limit: limit, + searchTerm: '', + ); + ref.onDispose(viewModel.dispose); + viewModel.loadInitial(); + return viewModel; + }); + +class MerchantPagingViewModel extends StateNotifier { + MerchantPagingViewModel({ + required MerchantRepository repository, + required String token, + required int limit, + required String searchTerm, + }) : _repository = repository, + _token = token, + super(MerchantPagingState(limit: limit, searchTerm: searchTerm)); + + final MerchantRepository _repository; + final String _token; + + Timer? _searchDebounce; + + void dispose() { + _searchDebounce?.cancel(); + } + + Future loadInitial() async { + await refresh(searchTerm: state.searchTerm); + } + + Future refresh({String? searchTerm}) async { + final nextSearch = searchTerm ?? state.searchTerm; + state = state.copyWith( + isLoading: true, + isLoadingMore: false, + page: 1, + items: const [], + hasMore: true, + searchTerm: nextSearch, + clearError: true, + ); + + try { + final results = await _repository.getMerchants( + token: _token, + page: 1, + limit: state.limit, + searchTerm: nextSearch, + ); + state = state.copyWith( + isLoading: false, + items: results, + page: 1, + hasMore: results.length >= state.limit, + ); + } catch (error) { + state = state.copyWith( + isLoading: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } + + void setSearchTerm(String value) { + final next = value; + if (next == state.searchTerm) { + return; + } + + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 350), () { + refresh(searchTerm: next); + }); + } + + Future loadMore() async { + if (state.isLoading || state.isLoadingMore || !state.hasMore) { + return; + } + + state = state.copyWith(isLoadingMore: true, clearError: true); + final nextPage = state.page + 1; + + try { + final results = await _repository.getMerchants( + token: _token, + page: nextPage, + limit: state.limit, + searchTerm: state.searchTerm, + ); + + final merged = [...state.items, ...results]; + state = state.copyWith( + isLoadingMore: false, + items: merged, + page: nextPage, + hasMore: results.length >= state.limit, + ); + } catch (error) { + state = state.copyWith( + isLoadingMore: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } +} diff --git a/lib/presentation/home/widgets/home_drawer.dart b/lib/presentation/home/widgets/home_drawer.dart index 219f865..e422a88 100644 --- a/lib/presentation/home/widgets/home_drawer.dart +++ b/lib/presentation/home/widgets/home_drawer.dart @@ -1,173 +1,139 @@ -import 'package:e_receipt_mobile/core/theme/app_colors.dart'; -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:flutter/material.dart'; - -class HomeDrawer extends StatelessWidget { +import 'package:e_receipt_mobile/core/theme/app_colors.dart'; +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:flutter/material.dart'; + +class HomeDrawer extends StatelessWidget { const HomeDrawer({ required this.user, - required this.onProfile, - required this.onReports, - required this.onSettings, - required this.onHelp, required this.onLogout, super.key, }); final LoginUser user; - final VoidCallback onProfile; - final VoidCallback onReports; - final VoidCallback onSettings; - final VoidCallback onHelp; final VoidCallback onLogout; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Drawer( - child: SafeArea( + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Drawer( + child: SafeArea( child: Column( children: [ _DrawerHeader(user: user), - Expanded( - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 8), - children: [ - _DrawerItem( - icon: Icons.person_outline, - label: 'Profile', - onTap: onProfile, - ), - _DrawerItem( - icon: Icons.analytics_outlined, - label: 'Reports', - onTap: onReports, - ), - _DrawerItem( - icon: Icons.settings_outlined, - label: 'Settings', - onTap: onSettings, - ), - _DrawerItem( - icon: Icons.help_outline, - label: 'Help', - onTap: onHelp, - ), - ], - ), - ), + const Spacer(), Divider( height: 1, thickness: 1, color: colorScheme.outlineVariant.withOpacity(0.6), ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), - child: _DrawerItem( - icon: Icons.logout, - label: 'Logout', - isDestructive: true, - onTap: onLogout, - ), - ), - ], - ), - ), - ); - } -} - -class _DrawerHeader extends StatelessWidget { - const _DrawerHeader({required this.user}); - - final LoginUser user; - - @override - Widget build(BuildContext context) { - final initials = (user.username.isNotEmpty ? user.username[0] : 'U') - .toUpperCase(); - - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppColors.primary, Color(0xFF1E5FD6)], - ), - ), - child: Row( - children: [ - CircleAvatar( - radius: 22, - backgroundColor: Colors.white, - child: Text( - initials, - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w700, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.username, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - fontSize: 16, - ), - ), - const SizedBox(height: 2), - Text( - 'Role: ${user.role}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white70), - ), - ], - ), - ), - ], - ), - ); - } -} - -class _DrawerItem extends StatelessWidget { - const _DrawerItem({ - required this.icon, - required this.label, - required this.onTap, - this.isDestructive = false, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - final bool isDestructive; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final foreground = isDestructive - ? colorScheme.error - : colorScheme.onSurface; - - return ListTile( - leading: Icon(icon, color: foreground), - title: Text( - label, - style: TextStyle(fontWeight: FontWeight.w600, color: foreground), - ), - onTap: onTap, - ); - } -} + Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), + child: _DrawerItem( + icon: Icons.logout, + label: 'Logout', + isDestructive: true, + onTap: onLogout, + ), + ), + ], + ), + ), + ); + } +} + +class _DrawerHeader extends StatelessWidget { + const _DrawerHeader({required this.user}); + + final LoginUser user; + + @override + Widget build(BuildContext context) { + final initials = (user.username.isNotEmpty ? user.username[0] : 'U') + .toUpperCase(); + + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.primary, Color(0xFF1E5FD6)], + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: Colors.white, + child: Text( + initials, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user.username, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + const SizedBox(height: 2), + Text( + 'Role: ${user.role}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white70), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _DrawerItem extends StatelessWidget { + const _DrawerItem({ + required this.icon, + required this.label, + required this.onTap, + this.isDestructive = false, + }); + + final IconData icon; + final String label; + final VoidCallback onTap; + final bool isDestructive; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final foreground = isDestructive + ? colorScheme.error + : colorScheme.onSurface; + + return ListTile( + leading: Icon(icon, color: foreground), + title: Text( + label, + style: TextStyle(fontWeight: FontWeight.w600, color: foreground), + ), + onTap: onTap, + ); + } +} diff --git a/lib/presentation/home/widgets/merchant_header.dart b/lib/presentation/home/widgets/merchant_header.dart index 8009d62..9fcb8d1 100644 --- a/lib/presentation/home/widgets/merchant_header.dart +++ b/lib/presentation/home/widgets/merchant_header.dart @@ -1,82 +1,82 @@ -import 'package:flutter/material.dart'; - -class MerchantHeader extends StatelessWidget { - const MerchantHeader({ - required this.loadedCount, - required this.query, - required this.onQueryChanged, - required this.onClear, - super.key, - }); - - final int loadedCount; - final String query; - final ValueChanged onQueryChanged; - final VoidCallback onClear; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Merchants', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 2), - Text( - 'Select a merchant to continue', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - Chip( - label: Text('$loadedCount'), - side: BorderSide(color: colorScheme.outlineVariant), - backgroundColor: colorScheme.surfaceContainerHigh, - ), - ], - ), - const SizedBox(height: 12), - SearchBar( - leading: const Icon(Icons.search), - hintText: 'Search merchants', - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 14), - ), - backgroundColor: WidgetStatePropertyAll( - colorScheme.surfaceContainerHigh, - ), - elevation: const WidgetStatePropertyAll(0), - constraints: const BoxConstraints(minHeight: 52), - trailing: [ - if (query.isNotEmpty) - IconButton( - tooltip: 'Clear', - onPressed: onClear, - icon: const Icon(Icons.close), - ), - ], - onChanged: onQueryChanged, - ), - ], - ), - ); - } -} +import 'package:flutter/material.dart'; + +class MerchantHeader extends StatelessWidget { + const MerchantHeader({ + required this.loadedCount, + required this.query, + required this.onQueryChanged, + required this.onClear, + super.key, + }); + + final int loadedCount; + final String query; + final ValueChanged onQueryChanged; + final VoidCallback onClear; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Merchants', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + 'Select a merchant to continue', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Chip( + label: Text('$loadedCount'), + side: BorderSide(color: colorScheme.outlineVariant), + backgroundColor: colorScheme.surfaceContainerHigh, + ), + ], + ), + const SizedBox(height: 12), + SearchBar( + leading: const Icon(Icons.search), + hintText: 'Search merchants', + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 14), + ), + backgroundColor: WidgetStatePropertyAll( + colorScheme.surfaceContainerHigh, + ), + elevation: const WidgetStatePropertyAll(0), + constraints: const BoxConstraints(minHeight: 52), + trailing: [ + if (query.isNotEmpty) + IconButton( + tooltip: 'Clear', + onPressed: onClear, + icon: const Icon(Icons.close), + ), + ], + onChanged: onQueryChanged, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/home/widgets/merchant_list_view.dart b/lib/presentation/home/widgets/merchant_list_view.dart index 38f5000..3b19266 100644 --- a/lib/presentation/home/widgets/merchant_list_view.dart +++ b/lib/presentation/home/widgets/merchant_list_view.dart @@ -1,233 +1,233 @@ -import 'package:e_receipt_mobile/domain/entities/merchant.dart'; -import 'package:flutter/material.dart'; - -class MerchantListView extends StatelessWidget { - const MerchantListView({ - required this.merchants, - required this.onRefresh, - required this.onMerchantTap, - required this.hasMore, - required this.isLoadingMore, - this.onLoadMore, - this.onEndReached, - this.endReachedThreshold = 240, - super.key, - }); - - final List merchants; - final Future Function() onRefresh; - final void Function(Merchant merchant) onMerchantTap; - final bool hasMore; - final bool isLoadingMore; - final VoidCallback? onLoadMore; - final VoidCallback? onEndReached; - final double endReachedThreshold; - - @override - Widget build(BuildContext context) { - final bottomInset = MediaQuery.viewPaddingOf(context).bottom; - final colorScheme = Theme.of(context).colorScheme; - - final list = merchants.isEmpty - ? ListView( - padding: EdgeInsets.fromLTRB(16, 96, 16, bottomInset + 24), - physics: const AlwaysScrollableScrollPhysics(), - children: [ - Icon( - Icons.storefront_outlined, - size: 56, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'No merchants found', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - 'Try a different search term or pull down to refresh.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ) - : ListView.separated( - padding: EdgeInsets.fromLTRB(16, 8, 16, bottomInset + 16), - physics: const AlwaysScrollableScrollPhysics(), - itemCount: merchants.length + (hasMore ? 1 : 0), - separatorBuilder: (_, __) => const SizedBox(height: 10), - itemBuilder: (context, index) { - if (index >= merchants.length) { - return _PaginationFooter( - isLoadingMore: isLoadingMore, - onLoadMore: onLoadMore, - ); - } - - final merchant = merchants[index]; - final name = merchant.name?.trim().isEmpty ?? true - ? '-' - : merchant.name!.trim(); - final address = (merchant.address?.trim().isEmpty ?? true) - ? 'No address' - : merchant.address!.trim(); - final enabled = merchant.id != null; - - return Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.55), - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(18), - onTap: enabled ? () => onMerchantTap(merchant) : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - child: Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: enabled - ? colorScheme.primary.withOpacity(0.14) - : colorScheme.surfaceContainerHigh, - ), - child: Icon( - Icons.storefront_outlined, - color: enabled - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith( - fontWeight: FontWeight.w700, - color: enabled - ? null - : colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 2), - Text( - address, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: enabled - ? colorScheme.onSurfaceVariant - : colorScheme.onSurfaceVariant - .withOpacity(0.8), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Icon( - Icons.chevron_right, - color: enabled - ? colorScheme.onSurfaceVariant - : colorScheme.onSurfaceVariant.withOpacity(0.4), - ), - ], - ), - ), - ), - ); - }, - ); - - final wrapped = onEndReached == null || merchants.isEmpty - ? list - : NotificationListener( - onNotification: (notification) { - if (!hasMore || isLoadingMore) { - return false; - } - if (notification.metrics.axis != Axis.vertical) { - return false; - } - if (notification.metrics.maxScrollExtent <= 0) { - return false; - } - if (notification.metrics.pixels >= - notification.metrics.maxScrollExtent - endReachedThreshold) { - onEndReached?.call(); - } - return false; - }, - child: list, - ); - - return RefreshIndicator.adaptive(onRefresh: onRefresh, child: wrapped); - } -} - -class _PaginationFooter extends StatelessWidget { - const _PaginationFooter({ - required this.isLoadingMore, - required this.onLoadMore, - }); - - final bool isLoadingMore; - final VoidCallback? onLoadMore; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Expanded( - child: Text( - isLoadingMore ? 'Loading more...' : 'More results available', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - ), - if (isLoadingMore) - const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - else - FilledButton.tonal( - onPressed: onLoadMore, - child: const Text('Load more'), - ), - ], - ), - ), - ); - } -} +import 'package:e_receipt_mobile/domain/entities/merchant.dart'; +import 'package:flutter/material.dart'; + +class MerchantListView extends StatelessWidget { + const MerchantListView({ + required this.merchants, + required this.onRefresh, + required this.onMerchantTap, + required this.hasMore, + required this.isLoadingMore, + this.onLoadMore, + this.onEndReached, + this.endReachedThreshold = 240, + super.key, + }); + + final List merchants; + final Future Function() onRefresh; + final void Function(Merchant merchant) onMerchantTap; + final bool hasMore; + final bool isLoadingMore; + final VoidCallback? onLoadMore; + final VoidCallback? onEndReached; + final double endReachedThreshold; + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + final colorScheme = Theme.of(context).colorScheme; + + final list = merchants.isEmpty + ? ListView( + padding: EdgeInsets.fromLTRB(16, 96, 16, bottomInset + 24), + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Icon( + Icons.storefront_outlined, + size: 56, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'No merchants found', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + 'Try a different search term or pull down to refresh.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ) + : ListView.separated( + padding: EdgeInsets.fromLTRB(16, 8, 16, bottomInset + 16), + physics: const AlwaysScrollableScrollPhysics(), + itemCount: merchants.length + (hasMore ? 1 : 0), + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + if (index >= merchants.length) { + return _PaginationFooter( + isLoadingMore: isLoadingMore, + onLoadMore: onLoadMore, + ); + } + + final merchant = merchants[index]; + final name = merchant.name?.trim().isEmpty ?? true + ? '-' + : merchant.name!.trim(); + final address = (merchant.address?.trim().isEmpty ?? true) + ? 'No address' + : merchant.address!.trim(); + final enabled = merchant.id != null; + + return Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.55), + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: enabled ? () => onMerchantTap(merchant) : null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + child: Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: enabled + ? colorScheme.primary.withOpacity(0.14) + : colorScheme.surfaceContainerHigh, + ), + child: Icon( + Icons.storefront_outlined, + color: enabled + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.w700, + color: enabled + ? null + : colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + address, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: enabled + ? colorScheme.onSurfaceVariant + : colorScheme.onSurfaceVariant + .withOpacity(0.8), + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Icon( + Icons.chevron_right, + color: enabled + ? colorScheme.onSurfaceVariant + : colorScheme.onSurfaceVariant.withOpacity(0.4), + ), + ], + ), + ), + ), + ); + }, + ); + + final wrapped = onEndReached == null || merchants.isEmpty + ? list + : NotificationListener( + onNotification: (notification) { + if (!hasMore || isLoadingMore) { + return false; + } + if (notification.metrics.axis != Axis.vertical) { + return false; + } + if (notification.metrics.maxScrollExtent <= 0) { + return false; + } + if (notification.metrics.pixels >= + notification.metrics.maxScrollExtent - endReachedThreshold) { + onEndReached?.call(); + } + return false; + }, + child: list, + ); + + return RefreshIndicator.adaptive(onRefresh: onRefresh, child: wrapped); + } +} + +class _PaginationFooter extends StatelessWidget { + const _PaginationFooter({ + required this.isLoadingMore, + required this.onLoadMore, + }); + + final bool isLoadingMore; + final VoidCallback? onLoadMore; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: Text( + isLoadingMore ? 'Loading more...' : 'More results available', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ), + if (isLoadingMore) + const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + FilledButton.tonal( + onPressed: onLoadMore, + child: const Text('Load more'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/login/login_form_providers.dart b/lib/presentation/login/login_form_providers.dart index 49af0f9..1033ccc 100644 --- a/lib/presentation/login/login_form_providers.dart +++ b/lib/presentation/login/login_form_providers.dart @@ -1,40 +1,40 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final loginFormKeyProvider = Provider.autoDispose>((ref) { - return GlobalKey(); -}); - -final loginUsernameControllerProvider = - Provider.autoDispose((ref) { - final controller = TextEditingController(); - ref.onDispose(controller.dispose); - return controller; - }); - -final loginPasswordControllerProvider = - Provider.autoDispose((ref) { - final controller = TextEditingController(); - ref.onDispose(controller.dispose); - return controller; - }); - -final loginUsernameFocusNodeProvider = Provider.autoDispose((ref) { - final focusNode = FocusNode(); - ref.onDispose(focusNode.dispose); - return focusNode; -}); - -final loginPasswordFocusNodeProvider = Provider.autoDispose((ref) { - final focusNode = FocusNode(); - ref.onDispose(focusNode.dispose); - return focusNode; -}); - -final loginObscurePasswordProvider = StateProvider.autoDispose( - (ref) => true, -); - -final loginAttemptedSubmitProvider = StateProvider.autoDispose( - (ref) => false, -); +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final loginFormKeyProvider = Provider.autoDispose>((ref) { + return GlobalKey(); +}); + +final loginUsernameControllerProvider = + Provider.autoDispose((ref) { + final controller = TextEditingController(); + ref.onDispose(controller.dispose); + return controller; + }); + +final loginPasswordControllerProvider = + Provider.autoDispose((ref) { + final controller = TextEditingController(); + ref.onDispose(controller.dispose); + return controller; + }); + +final loginUsernameFocusNodeProvider = Provider.autoDispose((ref) { + final focusNode = FocusNode(); + ref.onDispose(focusNode.dispose); + return focusNode; +}); + +final loginPasswordFocusNodeProvider = Provider.autoDispose((ref) { + final focusNode = FocusNode(); + ref.onDispose(focusNode.dispose); + return focusNode; +}); + +final loginObscurePasswordProvider = StateProvider.autoDispose( + (ref) => true, +); + +final loginAttemptedSubmitProvider = StateProvider.autoDispose( + (ref) => false, +); diff --git a/lib/presentation/login/login_page.dart b/lib/presentation/login/login_page.dart index badbbbe..3f17efb 100644 --- a/lib/presentation/login/login_page.dart +++ b/lib/presentation/login/login_page.dart @@ -1,329 +1,329 @@ -import 'package:e_receipt_mobile/presentation/login/login_form_providers.dart'; -import 'package:e_receipt_mobile/presentation/login/login_state.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:e_receipt_mobile/presentation/login/widgets/login_background.dart'; -import 'package:e_receipt_mobile/presentation/login/widgets/login_error_banner.dart'; -import 'package:e_receipt_mobile/presentation/reset_password/reset_password_page.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class LoginPage extends ConsumerWidget { - const LoginPage({super.key}); - - void _submit({ - required WidgetRef ref, - required LoginState state, - required GlobalKey formKey, - required TextEditingController usernameController, - required TextEditingController passwordController, - }) { - if (state.isLoading) { - return; - } - - ref.read(loginViewModelProvider.notifier).clearMessages(); - FocusManager.instance.primaryFocus?.unfocus(); - ref.read(loginAttemptedSubmitProvider.notifier).state = true; - - if (!(formKey.currentState?.validate() ?? false)) { - return; - } - - TextInput.finishAutofillContext(); - ref.read(loginViewModelProvider.notifier).login( - username: usernameController.text, - password: passwordController.text, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(loginViewModelProvider); - final colorScheme = Theme.of(context).colorScheme; - - ref.listen(loginViewModelProvider, (previous, next) { - final action = next.requiredAction; - if (action == null || action == previous?.requiredAction) { - return; - } - - switch (action) { - case LoginRequiredAction.passwordUpdateRequired: - final userId = (next.requiredUserId ?? '').trim(); - if (userId.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Password update required (missing user ID)'), - behavior: SnackBarBehavior.floating, - ), - ); - ref.read(loginViewModelProvider.notifier).clearRequirement(); - return; - } - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Password update required'), - behavior: SnackBarBehavior.floating, - ), - ); - ref.read(loginViewModelProvider.notifier).clearRequirement(); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => ResetPasswordPage(userId: userId)), - ); - } - }); - - final formKey = ref.watch(loginFormKeyProvider); - final usernameController = ref.watch(loginUsernameControllerProvider); - final passwordController = ref.watch(loginPasswordControllerProvider); - final usernameFocusNode = ref.watch(loginUsernameFocusNodeProvider); - final passwordFocusNode = ref.watch(loginPasswordFocusNodeProvider); - final obscurePassword = ref.watch(loginObscurePasswordProvider); - final attemptedSubmit = ref.watch(loginAttemptedSubmitProvider); - - return Scaffold( - body: AnnotatedRegion( - value: Theme.of(context).brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - child: LoginBackground( - child: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 24, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 440), - child: Card( - elevation: 0, - color: colorScheme.surface.withOpacity( - Theme.of(context).brightness == Brightness.dark - ? 0.75 - : 0.92, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.35), - ), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), - child: AutofillGroup( - child: Form( - key: formKey, - autovalidateMode: attemptedSubmit - ? AutovalidateMode.onUserInteraction - : AutovalidateMode.disabled, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primary, - colorScheme.tertiary, - ], - ), - ), - child: Icon( - Icons.receipt_long, - color: colorScheme.onPrimary, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'E-Receipt', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - Text( - 'Sign in to continue', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: colorScheme - .onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 18), - LoginErrorBanner(message: state.errorMessage), - if (state.errorMessage != null) - const SizedBox(height: 14), - TextFormField( - controller: usernameController, - focusNode: usernameFocusNode, - enabled: !state.isLoading, - textInputAction: TextInputAction.next, - autofillHints: const [AutofillHints.username], - onFieldSubmitted: (_) => - passwordFocusNode.requestFocus(), - validator: (value) { - if ((value ?? '').trim().isEmpty) { - return 'Username is required'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Username', - hintText: 'Enter your username', - prefixIcon: const Icon(Icons.person_outline), - filled: true, - fillColor: colorScheme.surfaceContainerHighest - .withOpacity( - Theme.of(context).brightness == - Brightness.dark - ? 0.55 - : 0.9, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - ), - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: passwordController, - focusNode: passwordFocusNode, - enabled: !state.isLoading, - textInputAction: TextInputAction.done, - autofillHints: const [AutofillHints.password], - obscureText: obscurePassword, - onFieldSubmitted: (_) => _submit( - ref: ref, - state: state, - formKey: formKey, - usernameController: usernameController, - passwordController: passwordController, - ), - validator: (value) { - if ((value ?? '').isEmpty) { - return 'Password is required'; - } - if (value!.length < 6) { - return 'Password must be at least 6 characters'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Password', - hintText: 'Enter your password', - prefixIcon: const Icon(Icons.lock_outline), - filled: true, - fillColor: colorScheme.surfaceContainerHighest - .withOpacity( - Theme.of(context).brightness == - Brightness.dark - ? 0.55 - : 0.9, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - ), - suffixIcon: IconButton( - tooltip: obscurePassword - ? 'Show password' - : 'Hide password', - onPressed: state.isLoading - ? null - : () => ref - .read( - loginObscurePasswordProvider - .notifier, - ) - .state = - !obscurePassword, - icon: Icon( - obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - ), - ), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: state.isLoading - ? null - : () => _submit( - ref: ref, - state: state, - formKey: formKey, - usernameController: usernameController, - passwordController: passwordController, - ), - icon: state.isLoading - ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.login), - label: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - child: Text( - state.isLoading - ? 'Signing in...' - : 'Sign in', - style: const TextStyle( - fontWeight: FontWeight.w600, - ), - ), - ), - ), - const SizedBox(height: 12), - Text( - "By continuing, you agree to your organization's policies.", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} +import 'package:e_receipt_mobile/presentation/login/login_form_providers.dart'; +import 'package:e_receipt_mobile/presentation/login/login_state.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:e_receipt_mobile/presentation/login/widgets/login_background.dart'; +import 'package:e_receipt_mobile/presentation/login/widgets/login_error_banner.dart'; +import 'package:e_receipt_mobile/presentation/reset_password/reset_password_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class LoginPage extends ConsumerWidget { + const LoginPage({super.key}); + + void _submit({ + required WidgetRef ref, + required LoginState state, + required GlobalKey formKey, + required TextEditingController usernameController, + required TextEditingController passwordController, + }) { + if (state.isLoading) { + return; + } + + ref.read(loginViewModelProvider.notifier).clearMessages(); + FocusManager.instance.primaryFocus?.unfocus(); + ref.read(loginAttemptedSubmitProvider.notifier).state = true; + + if (!(formKey.currentState?.validate() ?? false)) { + return; + } + + TextInput.finishAutofillContext(); + ref.read(loginViewModelProvider.notifier).login( + username: usernameController.text, + password: passwordController.text, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(loginViewModelProvider); + final colorScheme = Theme.of(context).colorScheme; + + ref.listen(loginViewModelProvider, (previous, next) { + final action = next.requiredAction; + if (action == null || action == previous?.requiredAction) { + return; + } + + switch (action) { + case LoginRequiredAction.passwordUpdateRequired: + final userId = (next.requiredUserId ?? '').trim(); + if (userId.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password update required (missing user ID)'), + behavior: SnackBarBehavior.floating, + ), + ); + ref.read(loginViewModelProvider.notifier).clearRequirement(); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Password update required'), + behavior: SnackBarBehavior.floating, + ), + ); + ref.read(loginViewModelProvider.notifier).clearRequirement(); + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ResetPasswordPage(userId: userId)), + ); + } + }); + + final formKey = ref.watch(loginFormKeyProvider); + final usernameController = ref.watch(loginUsernameControllerProvider); + final passwordController = ref.watch(loginPasswordControllerProvider); + final usernameFocusNode = ref.watch(loginUsernameFocusNodeProvider); + final passwordFocusNode = ref.watch(loginPasswordFocusNodeProvider); + final obscurePassword = ref.watch(loginObscurePasswordProvider); + final attemptedSubmit = ref.watch(loginAttemptedSubmitProvider); + + return Scaffold( + body: AnnotatedRegion( + value: Theme.of(context).brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, + child: LoginBackground( + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 24, + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Card( + elevation: 0, + color: colorScheme.surface.withOpacity( + Theme.of(context).brightness == Brightness.dark + ? 0.75 + : 0.92, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.35), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), + child: AutofillGroup( + child: Form( + key: formKey, + autovalidateMode: attemptedSubmit + ? AutovalidateMode.onUserInteraction + : AutovalidateMode.disabled, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.tertiary, + ], + ), + ), + child: Icon( + Icons.receipt_long, + color: colorScheme.onPrimary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'E-Receipt', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Sign in to continue', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 18), + LoginErrorBanner(message: state.errorMessage), + if (state.errorMessage != null) + const SizedBox(height: 14), + TextFormField( + controller: usernameController, + focusNode: usernameFocusNode, + enabled: !state.isLoading, + textInputAction: TextInputAction.next, + autofillHints: const [AutofillHints.username], + onFieldSubmitted: (_) => + passwordFocusNode.requestFocus(), + validator: (value) { + if ((value ?? '').trim().isEmpty) { + return 'Username is required'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Username', + hintText: 'Enter your username', + prefixIcon: const Icon(Icons.person_outline), + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withOpacity( + Theme.of(context).brightness == + Brightness.dark + ? 0.55 + : 0.9, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: passwordController, + focusNode: passwordFocusNode, + enabled: !state.isLoading, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.password], + obscureText: obscurePassword, + onFieldSubmitted: (_) => _submit( + ref: ref, + state: state, + formKey: formKey, + usernameController: usernameController, + passwordController: passwordController, + ), + validator: (value) { + if ((value ?? '').isEmpty) { + return 'Password is required'; + } + if (value!.length < 6) { + return 'Password must be at least 6 characters'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Password', + hintText: 'Enter your password', + prefixIcon: const Icon(Icons.lock_outline), + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withOpacity( + Theme.of(context).brightness == + Brightness.dark + ? 0.55 + : 0.9, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + ), + suffixIcon: IconButton( + tooltip: obscurePassword + ? 'Show password' + : 'Hide password', + onPressed: state.isLoading + ? null + : () => ref + .read( + loginObscurePasswordProvider + .notifier, + ) + .state = + !obscurePassword, + icon: Icon( + obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + ), + ), + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: state.isLoading + ? null + : () => _submit( + ref: ref, + state: state, + formKey: formKey, + usernameController: usernameController, + passwordController: passwordController, + ), + icon: state.isLoading + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.login), + label: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + state.isLoading + ? 'Signing in...' + : 'Sign in', + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: 12), + Text( + "By continuing, you agree to your organization's policies.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/login/login_state.dart b/lib/presentation/login/login_state.dart index 1c88300..3d44890 100644 --- a/lib/presentation/login/login_state.dart +++ b/lib/presentation/login/login_state.dart @@ -1,52 +1,52 @@ -import 'package:e_receipt_mobile/domain/entities/login_user.dart'; -import 'package:flutter/foundation.dart'; - -enum LoginRequiredAction { passwordUpdateRequired } - -@immutable -class LoginState { - const LoginState({ - this.isLoading = false, - this.errorMessage, - this.successMessage, - this.user, - this.requiredAction, - this.requiredUserId, - }); - - final bool isLoading; - final String? errorMessage; - final String? successMessage; - final LoginUser? user; - final LoginRequiredAction? requiredAction; - final String? requiredUserId; - - LoginState copyWith({ - bool? isLoading, - String? errorMessage, - String? successMessage, - LoginUser? user, - LoginRequiredAction? requiredAction, - String? requiredUserId, - bool clearError = false, - bool clearSuccess = false, - bool clearUser = false, - bool clearRequired = false, - }) { - return LoginState( - isLoading: isLoading ?? this.isLoading, - errorMessage: clearError ? null : errorMessage ?? this.errorMessage, - successMessage: clearSuccess - ? null - : successMessage ?? this.successMessage, - user: clearUser ? null : user ?? this.user, - requiredAction: clearRequired ? null : requiredAction ?? this.requiredAction, - requiredUserId: clearRequired ? null : requiredUserId ?? this.requiredUserId, - ); - } - - @override - String toString() { - return 'LoginState(isLoading: $isLoading, errorMessage: $errorMessage, successMessage: $successMessage, user: $user, requiredAction: $requiredAction, requiredUserId: $requiredUserId)'; - } -} +import 'package:e_receipt_mobile/domain/entities/login_user.dart'; +import 'package:flutter/foundation.dart'; + +enum LoginRequiredAction { passwordUpdateRequired } + +@immutable +class LoginState { + const LoginState({ + this.isLoading = false, + this.errorMessage, + this.successMessage, + this.user, + this.requiredAction, + this.requiredUserId, + }); + + final bool isLoading; + final String? errorMessage; + final String? successMessage; + final LoginUser? user; + final LoginRequiredAction? requiredAction; + final String? requiredUserId; + + LoginState copyWith({ + bool? isLoading, + String? errorMessage, + String? successMessage, + LoginUser? user, + LoginRequiredAction? requiredAction, + String? requiredUserId, + bool clearError = false, + bool clearSuccess = false, + bool clearUser = false, + bool clearRequired = false, + }) { + return LoginState( + isLoading: isLoading ?? this.isLoading, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + successMessage: clearSuccess + ? null + : successMessage ?? this.successMessage, + user: clearUser ? null : user ?? this.user, + requiredAction: clearRequired ? null : requiredAction ?? this.requiredAction, + requiredUserId: clearRequired ? null : requiredUserId ?? this.requiredUserId, + ); + } + + @override + String toString() { + return 'LoginState(isLoading: $isLoading, errorMessage: $errorMessage, successMessage: $successMessage, user: $user, requiredAction: $requiredAction, requiredUserId: $requiredUserId)'; + } +} diff --git a/lib/presentation/login/login_view_model.dart b/lib/presentation/login/login_view_model.dart index 5e23e4d..048f5c5 100644 --- a/lib/presentation/login/login_view_model.dart +++ b/lib/presentation/login/login_view_model.dart @@ -1,142 +1,142 @@ -import 'package:e_receipt_mobile/core/config/app_config.dart'; -import 'package:e_receipt_mobile/core/network/api_key_http_client.dart'; -import 'package:e_receipt_mobile/core/network/auth_http_client.dart'; -import 'package:e_receipt_mobile/core/network/logging_http_client.dart'; -import 'package:e_receipt_mobile/data/repositories/api_auth_repository.dart'; -import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; -import 'package:e_receipt_mobile/domain/exceptions/auth_exceptions.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/login/login_state.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:http/http.dart' as http; - -final rawHttpClientProvider = Provider((ref) { - final client = ApiKeyHttpClient( - innerClient: LoggingHttpClient(), - apiSecret: AppConfig.apiSecret, - ); - ref.onDispose(client.close); - return client; -}); - -final authenticatedHttpClientProvider = Provider((ref) { - final client = AuthHttpClient( - innerClient: ApiKeyHttpClient( - innerClient: LoggingHttpClient(), - apiSecret: AppConfig.apiSecret, - ), - accessTokenProvider: () => ref.read(sessionControllerProvider)?.token, - shouldRefreshOnUnauthorized: (uri) { - final path = uri.path; - return !(path.endsWith('/auth/login') || - path.endsWith('/auth/refresh-token') || - path.endsWith('/receipt/auth/login') || - path.endsWith('/receipt/auth/refresh-token')); - }, - refreshAccessToken: () async { - final sessionUser = ref.read(sessionControllerProvider); - if (sessionUser == null) { - return null; - } - - try { - final refreshedUser = await ref - .read(authRepositoryProvider) - .refreshSession( - username: sessionUser.username, - refreshToken: sessionUser.refreshToken, - role: sessionUser.role, - ); - ref.read(sessionControllerProvider.notifier).setUser(refreshedUser); - return refreshedUser.token; - } catch (_) { - ref.read(sessionControllerProvider.notifier).clearUser(); - return null; - } - }, - ); - ref.onDispose(client.close); - return client; -}); - -final authRepositoryProvider = Provider((ref) { - return ApiAuthRepository( - baseUrl: AppConfig.apiBaseUrl, - apiSecret: AppConfig.apiSecret, - client: ref.watch(rawHttpClientProvider), - ); -}); - -final loginViewModelProvider = - StateNotifierProvider((ref) { - return LoginViewModel( - ref.watch(authRepositoryProvider), - ref.watch(sessionControllerProvider.notifier), - ); - }); - -class LoginViewModel extends StateNotifier { - LoginViewModel(this._authRepository, this._sessionController) - : super(const LoginState()); - - final AuthRepository _authRepository; - final SessionController _sessionController; - - Future login({ - required String username, - required String password, - }) async { - final trimmedUsername = username.trim(); - if (trimmedUsername.isEmpty || password.isEmpty) { - state = state.copyWith( - isLoading: false, - errorMessage: 'Username and password are required', - clearSuccess: true, - ); - return; - } - - state = state.copyWith( - isLoading: true, - clearError: true, - clearSuccess: true, - clearUser: true, - clearRequired: true, - ); - _sessionController.clearUser(); - - try { - final user = await _authRepository.login( - username: trimmedUsername, - password: password, - ); - _sessionController.setUser(user); - state = state.copyWith( - isLoading: false, - user: user, - successMessage: 'Welcome ${user.username} (${user.role})', - ); - } on PasswordUpdateRequiredException catch (error) { - state = state.copyWith( - isLoading: false, - requiredAction: LoginRequiredAction.passwordUpdateRequired, - requiredUserId: error.userId, - clearError: true, - clearSuccess: true, - ); - } catch (error) { - state = state.copyWith( - isLoading: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } - - void clearMessages() { - state = state.copyWith(clearError: true, clearSuccess: true); - } - - void clearRequirement() { - state = state.copyWith(clearRequired: true); - } -} +import 'package:e_receipt_mobile/core/config/app_config.dart'; +import 'package:e_receipt_mobile/core/network/api_key_http_client.dart'; +import 'package:e_receipt_mobile/core/network/auth_http_client.dart'; +import 'package:e_receipt_mobile/core/network/logging_http_client.dart'; +import 'package:e_receipt_mobile/data/repositories/api_auth_repository.dart'; +import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; +import 'package:e_receipt_mobile/domain/exceptions/auth_exceptions.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_state.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; + +final rawHttpClientProvider = Provider((ref) { + final client = ApiKeyHttpClient( + innerClient: LoggingHttpClient(), + apiSecret: AppConfig.apiSecret, + ); + ref.onDispose(client.close); + return client; +}); + +final authenticatedHttpClientProvider = Provider((ref) { + final client = AuthHttpClient( + innerClient: ApiKeyHttpClient( + innerClient: LoggingHttpClient(), + apiSecret: AppConfig.apiSecret, + ), + accessTokenProvider: () => ref.read(sessionControllerProvider)?.token, + shouldRefreshOnUnauthorized: (uri) { + final path = uri.path; + return !(path.endsWith('/auth/login') || + path.endsWith('/auth/refresh-token') || + path.endsWith('/receipt/auth/login') || + path.endsWith('/receipt/auth/refresh-token')); + }, + refreshAccessToken: () async { + final sessionUser = ref.read(sessionControllerProvider); + if (sessionUser == null) { + return null; + } + + try { + final refreshedUser = await ref + .read(authRepositoryProvider) + .refreshSession( + username: sessionUser.username, + refreshToken: sessionUser.refreshToken, + role: sessionUser.role, + ); + ref.read(sessionControllerProvider.notifier).setUser(refreshedUser); + return refreshedUser.token; + } catch (_) { + ref.read(sessionControllerProvider.notifier).clearUser(); + return null; + } + }, + ); + ref.onDispose(client.close); + return client; +}); + +final authRepositoryProvider = Provider((ref) { + return ApiAuthRepository( + baseUrl: AppConfig.apiBaseUrl, + apiSecret: AppConfig.apiSecret, + client: ref.watch(rawHttpClientProvider), + ); +}); + +final loginViewModelProvider = + StateNotifierProvider((ref) { + return LoginViewModel( + ref.watch(authRepositoryProvider), + ref.watch(sessionControllerProvider.notifier), + ); + }); + +class LoginViewModel extends StateNotifier { + LoginViewModel(this._authRepository, this._sessionController) + : super(const LoginState()); + + final AuthRepository _authRepository; + final SessionController _sessionController; + + Future login({ + required String username, + required String password, + }) async { + final trimmedUsername = username.trim(); + if (trimmedUsername.isEmpty || password.isEmpty) { + state = state.copyWith( + isLoading: false, + errorMessage: 'Username and password are required', + clearSuccess: true, + ); + return; + } + + state = state.copyWith( + isLoading: true, + clearError: true, + clearSuccess: true, + clearUser: true, + clearRequired: true, + ); + _sessionController.clearUser(); + + try { + final user = await _authRepository.login( + username: trimmedUsername, + password: password, + ); + _sessionController.setUser(user); + state = state.copyWith( + isLoading: false, + user: user, + successMessage: 'Welcome ${user.username} (${user.role})', + ); + } on PasswordUpdateRequiredException catch (error) { + state = state.copyWith( + isLoading: false, + requiredAction: LoginRequiredAction.passwordUpdateRequired, + requiredUserId: error.userId, + clearError: true, + clearSuccess: true, + ); + } catch (error) { + state = state.copyWith( + isLoading: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } + + void clearMessages() { + state = state.copyWith(clearError: true, clearSuccess: true); + } + + void clearRequirement() { + state = state.copyWith(clearRequired: true); + } +} diff --git a/lib/presentation/login/widgets/login_background.dart b/lib/presentation/login/widgets/login_background.dart index 7df812c..30f19b8 100644 --- a/lib/presentation/login/widgets/login_background.dart +++ b/lib/presentation/login/widgets/login_background.dart @@ -1,67 +1,67 @@ -import 'package:flutter/material.dart'; - -class LoginBackground extends StatelessWidget { - const LoginBackground({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Stack( - children: [ - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primary.withOpacity(0.18), - colorScheme.secondary.withOpacity(0.10), - colorScheme.surface, - ], - stops: const [0.0, 0.45, 1.0], - ), - ), - ), - ), - Positioned( - top: -120, - right: -120, - child: _GlowBlob(color: colorScheme.primary.withOpacity(0.20)), - ), - Positioned( - bottom: -150, - left: -150, - child: _GlowBlob(color: colorScheme.tertiary.withOpacity(0.18)), - ), - child, - ], - ); - } -} - -class _GlowBlob extends StatelessWidget { - const _GlowBlob({required this.color}); - - final Color color; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - child: Container( - height: 280, - width: 280, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - boxShadow: [ - BoxShadow(color: color, blurRadius: 80, spreadRadius: 40), - ], - ), - ), - ); - } -} +import 'package:flutter/material.dart'; + +class LoginBackground extends StatelessWidget { + const LoginBackground({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Stack( + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary.withOpacity(0.18), + colorScheme.secondary.withOpacity(0.10), + colorScheme.surface, + ], + stops: const [0.0, 0.45, 1.0], + ), + ), + ), + ), + Positioned( + top: -120, + right: -120, + child: _GlowBlob(color: colorScheme.primary.withOpacity(0.20)), + ), + Positioned( + bottom: -150, + left: -150, + child: _GlowBlob(color: colorScheme.tertiary.withOpacity(0.18)), + ), + child, + ], + ); + } +} + +class _GlowBlob extends StatelessWidget { + const _GlowBlob({required this.color}); + + final Color color; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + height: 280, + width: 280, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow(color: color, blurRadius: 80, spreadRadius: 40), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/login/widgets/login_error_banner.dart b/lib/presentation/login/widgets/login_error_banner.dart index 8ca00df..3bd05bc 100644 --- a/lib/presentation/login/widgets/login_error_banner.dart +++ b/lib/presentation/login/widgets/login_error_banner.dart @@ -1,44 +1,44 @@ -import 'package:flutter/material.dart'; - -class LoginErrorBanner extends StatelessWidget { - const LoginErrorBanner({required this.message, super.key}); - - final String? message; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: message == null - ? const SizedBox.shrink() - : Container( - key: ValueKey(message), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.errorContainer.withOpacity(0.9), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.error.withOpacity(0.25)), - ), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: colorScheme.onErrorContainer, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - message!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onErrorContainer, - ), - ), - ), - ], - ), - ), - ); - } -} +import 'package:flutter/material.dart'; + +class LoginErrorBanner extends StatelessWidget { + const LoginErrorBanner({required this.message, super.key}); + + final String? message; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: message == null + ? const SizedBox.shrink() + : Container( + key: ValueKey(message), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.errorContainer.withOpacity(0.9), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.error.withOpacity(0.25)), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: colorScheme.onErrorContainer, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + message!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/reset_password/reset_password_form_providers.dart b/lib/presentation/reset_password/reset_password_form_providers.dart index 032a23e..eff1f03 100644 --- a/lib/presentation/reset_password/reset_password_form_providers.dart +++ b/lib/presentation/reset_password/reset_password_form_providers.dart @@ -1,40 +1,40 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final resetPasswordFormKeyProvider = - Provider.autoDispose>((ref) { - return GlobalKey(); -}); - -final resetPasswordControllerProvider = - Provider.autoDispose((ref) { - final controller = TextEditingController(); - ref.onDispose(controller.dispose); - return controller; -}); - -final resetConfirmPasswordControllerProvider = - Provider.autoDispose((ref) { - final controller = TextEditingController(); - ref.onDispose(controller.dispose); - return controller; -}); - -final resetPasswordFocusNodeProvider = Provider.autoDispose((ref) { - final focusNode = FocusNode(); - ref.onDispose(focusNode.dispose); - return focusNode; -}); - -final resetConfirmPasswordFocusNodeProvider = - Provider.autoDispose((ref) { - final focusNode = FocusNode(); - ref.onDispose(focusNode.dispose); - return focusNode; -}); - -final resetObscurePasswordProvider = StateProvider.autoDispose((ref) => true); -final resetObscureConfirmPasswordProvider = - StateProvider.autoDispose((ref) => true); - -final resetAttemptedSubmitProvider = StateProvider.autoDispose((ref) => false); +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final resetPasswordFormKeyProvider = + Provider.autoDispose>((ref) { + return GlobalKey(); +}); + +final resetPasswordControllerProvider = + Provider.autoDispose((ref) { + final controller = TextEditingController(); + ref.onDispose(controller.dispose); + return controller; +}); + +final resetConfirmPasswordControllerProvider = + Provider.autoDispose((ref) { + final controller = TextEditingController(); + ref.onDispose(controller.dispose); + return controller; +}); + +final resetPasswordFocusNodeProvider = Provider.autoDispose((ref) { + final focusNode = FocusNode(); + ref.onDispose(focusNode.dispose); + return focusNode; +}); + +final resetConfirmPasswordFocusNodeProvider = + Provider.autoDispose((ref) { + final focusNode = FocusNode(); + ref.onDispose(focusNode.dispose); + return focusNode; +}); + +final resetObscurePasswordProvider = StateProvider.autoDispose((ref) => true); +final resetObscureConfirmPasswordProvider = + StateProvider.autoDispose((ref) => true); + +final resetAttemptedSubmitProvider = StateProvider.autoDispose((ref) => false); diff --git a/lib/presentation/reset_password/reset_password_page.dart b/lib/presentation/reset_password/reset_password_page.dart index 410104e..0c402bd 100644 --- a/lib/presentation/reset_password/reset_password_page.dart +++ b/lib/presentation/reset_password/reset_password_page.dart @@ -1,334 +1,334 @@ -import 'package:e_receipt_mobile/presentation/reset_password/reset_password_form_providers.dart'; -import 'package:e_receipt_mobile/presentation/reset_password/reset_password_view_model.dart'; -import 'package:e_receipt_mobile/presentation/login/widgets/login_background.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class ResetPasswordPage extends ConsumerWidget { - const ResetPasswordPage({required this.userId, super.key}); - - final String userId; - - bool _isStrongPassword(String value) { - return value.length >= 8 && - RegExp(r'[a-z]').hasMatch(value) && - RegExp(r'[A-Z]').hasMatch(value) && - RegExp(r'[0-9]').hasMatch(value) && - RegExp(r'[^A-Za-z0-9]').hasMatch(value); - } - - void _submit({ - required BuildContext context, - required WidgetRef ref, - required GlobalKey formKey, - required TextEditingController passwordController, - required TextEditingController confirmController, - required bool isLoading, - }) { - if (isLoading) { - return; - } - - ref.read(resetPasswordViewModelProvider.notifier).clearMessages(); - FocusManager.instance.primaryFocus?.unfocus(); - ref.read(resetAttemptedSubmitProvider.notifier).state = true; - - if (!(formKey.currentState?.validate() ?? false)) { - return; - } - - final password = passwordController.text; - final confirm = confirmController.text; - if (password != confirm) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Passwords do not match'), - behavior: SnackBarBehavior.floating, - ), - ); - return; - } - - ref - .read(resetPasswordViewModelProvider.notifier) - .resetPassword(userId: userId, password: password); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(resetPasswordViewModelProvider); - final colorScheme = Theme.of(context).colorScheme; - - ref.listen(resetPasswordViewModelProvider, (previous, next) { - final message = next.errorMessage; - if (message != null && message != previous?.errorMessage) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - behavior: SnackBarBehavior.floating, - ), - ); - } - final success = next.successMessage; - if (success != null && success != previous?.successMessage) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success), - behavior: SnackBarBehavior.floating, - ), - ); - Navigator.of(context).pop(); - } - }); - - final formKey = ref.watch(resetPasswordFormKeyProvider); - final passwordController = ref.watch(resetPasswordControllerProvider); - final confirmController = ref.watch(resetConfirmPasswordControllerProvider); - final passwordFocusNode = ref.watch(resetPasswordFocusNodeProvider); - final confirmFocusNode = ref.watch(resetConfirmPasswordFocusNodeProvider); - final obscure = ref.watch(resetObscurePasswordProvider); - final obscureConfirm = ref.watch(resetObscureConfirmPasswordProvider); - final attemptedSubmit = ref.watch(resetAttemptedSubmitProvider); - - return Scaffold( - appBar: AppBar(title: const Text('Reset password')), - body: AnnotatedRegion( - value: Theme.of(context).brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - child: LoginBackground( - child: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 440), - child: Card( - elevation: 0, - color: colorScheme.surface.withOpacity( - Theme.of(context).brightness == Brightness.dark ? 0.75 : 0.92, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.35), - ), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), - child: Form( - key: formKey, - autovalidateMode: attemptedSubmit - ? AutovalidateMode.onUserInteraction - : AutovalidateMode.disabled, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primary, - colorScheme.tertiary, - ], - ), - ), - child: const Icon( - Icons.lock_reset_outlined, - color: Colors.white, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Update your password', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.w800), - ), - Text( - 'Your account requires a password change.', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 18), - TextFormField( - controller: passwordController, - focusNode: passwordFocusNode, - enabled: !state.isLoading, - textInputAction: TextInputAction.next, - obscureText: obscure, - onFieldSubmitted: (_) => - FocusScope.of(context).requestFocus(confirmFocusNode), - validator: (value) { - final text = (value ?? '').trim(); - if (text.isEmpty) { - return 'New password is required'; - } - if (!_isStrongPassword(text)) { - return 'Use 8+ chars with upper, lower, number, and symbol'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'New password', - hintText: 'Enter a strong password', - prefixIcon: const Icon(Icons.lock_outline), - filled: true, - fillColor: colorScheme.surfaceContainerHighest - .withOpacity( - Theme.of(context).brightness == Brightness.dark - ? 0.55 - : 0.9, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - ), - suffixIcon: IconButton( - tooltip: - obscure ? 'Show password' : 'Hide password', - onPressed: state.isLoading - ? null - : () => ref - .read( - resetObscurePasswordProvider.notifier, - ) - .state = - !obscure, - icon: Icon( - obscure - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - ), - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: confirmController, - focusNode: confirmFocusNode, - enabled: !state.isLoading, - textInputAction: TextInputAction.done, - obscureText: obscureConfirm, - onFieldSubmitted: (_) => _submit( - context: context, - ref: ref, - formKey: formKey, - passwordController: passwordController, - confirmController: confirmController, - isLoading: state.isLoading, - ), - validator: (value) { - if ((value ?? '').trim().isEmpty) { - return 'Confirm password is required'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Confirm new password', - hintText: 'Re-enter your password', - prefixIcon: const Icon(Icons.lock_outline), - filled: true, - fillColor: colorScheme.surfaceContainerHighest - .withOpacity( - Theme.of(context).brightness == Brightness.dark - ? 0.55 - : 0.9, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(18), - ), - suffixIcon: IconButton( - tooltip: obscureConfirm - ? 'Show password' - : 'Hide password', - onPressed: state.isLoading - ? null - : () => ref - .read( - resetObscureConfirmPasswordProvider - .notifier, - ) - .state = - !obscureConfirm, - icon: Icon( - obscureConfirm - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - ), - ), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: state.isLoading - ? null - : () => _submit( - context: context, - ref: ref, - formKey: formKey, - passwordController: passwordController, - confirmController: confirmController, - isLoading: state.isLoading, - ), - icon: state.isLoading - ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.check), - label: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - state.isLoading ? 'Updating...' : 'Reset password', - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ), - const SizedBox(height: 10), - Text( - 'Tip: use a unique password you don’t use elsewhere.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} +import 'package:e_receipt_mobile/presentation/reset_password/reset_password_form_providers.dart'; +import 'package:e_receipt_mobile/presentation/reset_password/reset_password_view_model.dart'; +import 'package:e_receipt_mobile/presentation/login/widgets/login_background.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ResetPasswordPage extends ConsumerWidget { + const ResetPasswordPage({required this.userId, super.key}); + + final String userId; + + bool _isStrongPassword(String value) { + return value.length >= 8 && + RegExp(r'[a-z]').hasMatch(value) && + RegExp(r'[A-Z]').hasMatch(value) && + RegExp(r'[0-9]').hasMatch(value) && + RegExp(r'[^A-Za-z0-9]').hasMatch(value); + } + + void _submit({ + required BuildContext context, + required WidgetRef ref, + required GlobalKey formKey, + required TextEditingController passwordController, + required TextEditingController confirmController, + required bool isLoading, + }) { + if (isLoading) { + return; + } + + ref.read(resetPasswordViewModelProvider.notifier).clearMessages(); + FocusManager.instance.primaryFocus?.unfocus(); + ref.read(resetAttemptedSubmitProvider.notifier).state = true; + + if (!(formKey.currentState?.validate() ?? false)) { + return; + } + + final password = passwordController.text; + final confirm = confirmController.text; + if (password != confirm) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Passwords do not match'), + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + ref + .read(resetPasswordViewModelProvider.notifier) + .resetPassword(userId: userId, password: password); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(resetPasswordViewModelProvider); + final colorScheme = Theme.of(context).colorScheme; + + ref.listen(resetPasswordViewModelProvider, (previous, next) { + final message = next.errorMessage; + if (message != null && message != previous?.errorMessage) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + final success = next.successMessage; + if (success != null && success != previous?.successMessage) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success), + behavior: SnackBarBehavior.floating, + ), + ); + Navigator.of(context).pop(); + } + }); + + final formKey = ref.watch(resetPasswordFormKeyProvider); + final passwordController = ref.watch(resetPasswordControllerProvider); + final confirmController = ref.watch(resetConfirmPasswordControllerProvider); + final passwordFocusNode = ref.watch(resetPasswordFocusNodeProvider); + final confirmFocusNode = ref.watch(resetConfirmPasswordFocusNodeProvider); + final obscure = ref.watch(resetObscurePasswordProvider); + final obscureConfirm = ref.watch(resetObscureConfirmPasswordProvider); + final attemptedSubmit = ref.watch(resetAttemptedSubmitProvider); + + return Scaffold( + appBar: AppBar(title: const Text('Reset password')), + body: AnnotatedRegion( + value: Theme.of(context).brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, + child: LoginBackground( + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Card( + elevation: 0, + color: colorScheme.surface.withOpacity( + Theme.of(context).brightness == Brightness.dark ? 0.75 : 0.92, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.35), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), + child: Form( + key: formKey, + autovalidateMode: attemptedSubmit + ? AutovalidateMode.onUserInteraction + : AutovalidateMode.disabled, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.tertiary, + ], + ), + ), + child: const Icon( + Icons.lock_reset_outlined, + color: Colors.white, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Update your password', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.w800), + ), + Text( + 'Your account requires a password change.', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 18), + TextFormField( + controller: passwordController, + focusNode: passwordFocusNode, + enabled: !state.isLoading, + textInputAction: TextInputAction.next, + obscureText: obscure, + onFieldSubmitted: (_) => + FocusScope.of(context).requestFocus(confirmFocusNode), + validator: (value) { + final text = (value ?? '').trim(); + if (text.isEmpty) { + return 'New password is required'; + } + if (!_isStrongPassword(text)) { + return 'Use 8+ chars with upper, lower, number, and symbol'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'New password', + hintText: 'Enter a strong password', + prefixIcon: const Icon(Icons.lock_outline), + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withOpacity( + Theme.of(context).brightness == Brightness.dark + ? 0.55 + : 0.9, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + ), + suffixIcon: IconButton( + tooltip: + obscure ? 'Show password' : 'Hide password', + onPressed: state.isLoading + ? null + : () => ref + .read( + resetObscurePasswordProvider.notifier, + ) + .state = + !obscure, + icon: Icon( + obscure + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + ), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: confirmController, + focusNode: confirmFocusNode, + enabled: !state.isLoading, + textInputAction: TextInputAction.done, + obscureText: obscureConfirm, + onFieldSubmitted: (_) => _submit( + context: context, + ref: ref, + formKey: formKey, + passwordController: passwordController, + confirmController: confirmController, + isLoading: state.isLoading, + ), + validator: (value) { + if ((value ?? '').trim().isEmpty) { + return 'Confirm password is required'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Confirm new password', + hintText: 'Re-enter your password', + prefixIcon: const Icon(Icons.lock_outline), + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withOpacity( + Theme.of(context).brightness == Brightness.dark + ? 0.55 + : 0.9, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + ), + suffixIcon: IconButton( + tooltip: obscureConfirm + ? 'Show password' + : 'Hide password', + onPressed: state.isLoading + ? null + : () => ref + .read( + resetObscureConfirmPasswordProvider + .notifier, + ) + .state = + !obscureConfirm, + icon: Icon( + obscureConfirm + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + ), + ), + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: state.isLoading + ? null + : () => _submit( + context: context, + ref: ref, + formKey: formKey, + passwordController: passwordController, + confirmController: confirmController, + isLoading: state.isLoading, + ), + icon: state.isLoading + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.check), + label: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + state.isLoading ? 'Updating...' : 'Reset password', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + const SizedBox(height: 10), + Text( + 'Tip: use a unique password you don’t use elsewhere.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/reset_password/reset_password_state.dart b/lib/presentation/reset_password/reset_password_state.dart index 015da9f..d57d821 100644 --- a/lib/presentation/reset_password/reset_password_state.dart +++ b/lib/presentation/reset_password/reset_password_state.dart @@ -1,28 +1,28 @@ -import 'package:flutter/foundation.dart'; - -@immutable -class ResetPasswordState { - const ResetPasswordState({ - this.isLoading = false, - this.errorMessage, - this.successMessage, - }); - - final bool isLoading; - final String? errorMessage; - final String? successMessage; - - ResetPasswordState copyWith({ - bool? isLoading, - String? errorMessage, - String? successMessage, - bool clearError = false, - bool clearSuccess = false, - }) { - return ResetPasswordState( - isLoading: isLoading ?? this.isLoading, - errorMessage: clearError ? null : errorMessage ?? this.errorMessage, - successMessage: clearSuccess ? null : successMessage ?? this.successMessage, - ); - } -} +import 'package:flutter/foundation.dart'; + +@immutable +class ResetPasswordState { + const ResetPasswordState({ + this.isLoading = false, + this.errorMessage, + this.successMessage, + }); + + final bool isLoading; + final String? errorMessage; + final String? successMessage; + + ResetPasswordState copyWith({ + bool? isLoading, + String? errorMessage, + String? successMessage, + bool clearError = false, + bool clearSuccess = false, + }) { + return ResetPasswordState( + isLoading: isLoading ?? this.isLoading, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + successMessage: clearSuccess ? null : successMessage ?? this.successMessage, + ); + } +} diff --git a/lib/presentation/reset_password/reset_password_view_model.dart b/lib/presentation/reset_password/reset_password_view_model.dart index f754d70..ac8db86 100644 --- a/lib/presentation/reset_password/reset_password_view_model.dart +++ b/lib/presentation/reset_password/reset_password_view_model.dart @@ -1,54 +1,54 @@ -import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:e_receipt_mobile/presentation/reset_password/reset_password_state.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final resetPasswordViewModelProvider = - StateNotifierProvider.autoDispose( - (ref) { - return ResetPasswordViewModel(ref.watch(authRepositoryProvider)); - }, -); - -class ResetPasswordViewModel extends StateNotifier { - ResetPasswordViewModel(this._authRepository) : super(const ResetPasswordState()); - - final AuthRepository _authRepository; - - Future resetPassword({ - required String userId, - required String password, - }) async { - if (userId.trim().isEmpty) { - state = state.copyWith( - isLoading: false, - errorMessage: 'Missing user ID', - clearSuccess: true, - ); - return; - } - - state = state.copyWith( - isLoading: true, - clearError: true, - clearSuccess: true, - ); - - try { - await _authRepository.resetPassword(userId: userId.trim(), password: password); - state = state.copyWith( - isLoading: false, - successMessage: 'Password updated', - ); - } catch (error) { - state = state.copyWith( - isLoading: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } - - void clearMessages() { - state = state.copyWith(clearError: true, clearSuccess: true); - } -} +import 'package:e_receipt_mobile/domain/repositories/auth_repository.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:e_receipt_mobile/presentation/reset_password/reset_password_state.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final resetPasswordViewModelProvider = + StateNotifierProvider.autoDispose( + (ref) { + return ResetPasswordViewModel(ref.watch(authRepositoryProvider)); + }, +); + +class ResetPasswordViewModel extends StateNotifier { + ResetPasswordViewModel(this._authRepository) : super(const ResetPasswordState()); + + final AuthRepository _authRepository; + + Future resetPassword({ + required String userId, + required String password, + }) async { + if (userId.trim().isEmpty) { + state = state.copyWith( + isLoading: false, + errorMessage: 'Missing user ID', + clearSuccess: true, + ); + return; + } + + state = state.copyWith( + isLoading: true, + clearError: true, + clearSuccess: true, + ); + + try { + await _authRepository.resetPassword(userId: userId.trim(), password: password); + state = state.copyWith( + isLoading: false, + successMessage: 'Password updated', + ); + } catch (error) { + state = state.copyWith( + isLoading: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } + + void clearMessages() { + state = state.copyWith(clearError: true, clearSuccess: true); + } +} diff --git a/lib/presentation/settings/settings_screen.dart b/lib/presentation/settings/settings_screen.dart index 5c7ac81..4f002c5 100644 --- a/lib/presentation/settings/settings_screen.dart +++ b/lib/presentation/settings/settings_screen.dart @@ -1,65 +1,65 @@ -import 'package:e_receipt_mobile/presentation/settings/theme_mode_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class SettingsScreen extends ConsumerWidget { - const SettingsScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedMode = ref.watch(appThemeModeProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - centerTitle: true, - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - Text( - 'Theme', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Card( - child: Column( - children: [ - RadioListTile( - title: const Text('Light'), - value: ThemeMode.light, - groupValue: selectedMode, - onChanged: (value) { - if (value != null) { - ref.read(appThemeModeProvider.notifier).state = value; - } - }, - ), - RadioListTile( - title: const Text('Dark'), - value: ThemeMode.dark, - groupValue: selectedMode, - onChanged: (value) { - if (value != null) { - ref.read(appThemeModeProvider.notifier).state = value; - } - }, - ), - RadioListTile( - title: const Text('System'), - value: ThemeMode.system, - groupValue: selectedMode, - onChanged: (value) { - if (value != null) { - ref.read(appThemeModeProvider.notifier).state = value; - } - }, - ), - ], - ), - ), - ], - ), - ); - } -} +import 'package:e_receipt_mobile/presentation/settings/theme_mode_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class SettingsScreen extends ConsumerWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedMode = ref.watch(appThemeModeProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + centerTitle: true, + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + Text( + 'Theme', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Card( + child: Column( + children: [ + RadioListTile( + title: const Text('Light'), + value: ThemeMode.light, + groupValue: selectedMode, + onChanged: (value) { + if (value != null) { + ref.read(appThemeModeProvider.notifier).state = value; + } + }, + ), + RadioListTile( + title: const Text('Dark'), + value: ThemeMode.dark, + groupValue: selectedMode, + onChanged: (value) { + if (value != null) { + ref.read(appThemeModeProvider.notifier).state = value; + } + }, + ), + RadioListTile( + title: const Text('System'), + value: ThemeMode.system, + groupValue: selectedMode, + onChanged: (value) { + if (value != null) { + ref.read(appThemeModeProvider.notifier).state = value; + } + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/settings/theme_mode_provider.dart b/lib/presentation/settings/theme_mode_provider.dart index 040c546..4e2504c 100644 --- a/lib/presentation/settings/theme_mode_provider.dart +++ b/lib/presentation/settings/theme_mode_provider.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final appThemeModeProvider = StateProvider((ref) { - return ThemeMode.system; -}); +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final appThemeModeProvider = StateProvider((ref) { + return ThemeMode.system; +}); diff --git a/lib/presentation/terminal/receipt_view_model.dart b/lib/presentation/terminal/receipt_view_model.dart index e29893c..2e9d72a 100644 --- a/lib/presentation/terminal/receipt_view_model.dart +++ b/lib/presentation/terminal/receipt_view_model.dart @@ -1,78 +1,78 @@ -import 'dart:io'; - -import 'package:e_receipt_mobile/core/config/app_config.dart'; -import 'package:e_receipt_mobile/data/repositories/api_receipt_repository.dart'; -import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class TransactionReceiptQuery { - const TransactionReceiptQuery({ - required this.transactionId, - required this.copyFor, - }); - - final String transactionId; - final String copyFor; - - @override - bool operator ==(Object other) { - return other is TransactionReceiptQuery && - other.transactionId == transactionId && - other.copyFor == copyFor; - } - - @override - int get hashCode => Object.hash(transactionId, copyFor); -} - -sealed class ReceiptViewData { - const ReceiptViewData(); -} - -class ReceiptPdfViewData extends ReceiptViewData { - const ReceiptPdfViewData(this.file); - - final File file; -} - -final receiptRepositoryProvider = Provider((ref) { - return ApiReceiptRepository( - baseUrl: AppConfig.apiBaseUrl, - apiSecret: AppConfig.apiSecret, - client: ref.watch(authenticatedHttpClientProvider), - ); -}); - -final transactionReceiptViewDataProvider = - FutureProvider.family(( - ref, - query, - ) async { - try { - final sessionUser = ref.watch(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - final bytes = await ref - .watch(receiptRepositoryProvider) - .getTransactionReceiptPdfBytes( - token: sessionUser.token, - transactionId: query.transactionId, - copyFor: query.copyFor, - ); - - final dir = await Directory.systemTemp.createTemp('e_receipt_mobile'); - final file = File( - '${dir.path}/receipt_${query.transactionId}_${query.copyFor}.pdf', - ); - await file.writeAsBytes(bytes, flush: true); - return ReceiptPdfViewData(file); - } catch (e, st) { - debugPrint('[RECEIPT][ERROR] ${Error.safeToString(e)}\n$st'); - throw Exception(Error.safeToString(e)); - } - }); +import 'dart:io'; + +import 'package:e_receipt_mobile/core/config/app_config.dart'; +import 'package:e_receipt_mobile/data/repositories/api_receipt_repository.dart'; +import 'package:e_receipt_mobile/domain/repositories/receipt_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TransactionReceiptQuery { + const TransactionReceiptQuery({ + required this.transactionId, + required this.copyFor, + }); + + final String transactionId; + final String copyFor; + + @override + bool operator ==(Object other) { + return other is TransactionReceiptQuery && + other.transactionId == transactionId && + other.copyFor == copyFor; + } + + @override + int get hashCode => Object.hash(transactionId, copyFor); +} + +sealed class ReceiptViewData { + const ReceiptViewData(); +} + +class ReceiptPdfViewData extends ReceiptViewData { + const ReceiptPdfViewData(this.file); + + final File file; +} + +final receiptRepositoryProvider = Provider((ref) { + return ApiReceiptRepository( + baseUrl: AppConfig.apiBaseUrl, + apiSecret: AppConfig.apiSecret, + client: ref.watch(authenticatedHttpClientProvider), + ); +}); + +final transactionReceiptViewDataProvider = + FutureProvider.family(( + ref, + query, + ) async { + try { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + final bytes = await ref + .watch(receiptRepositoryProvider) + .getTransactionReceiptPdfBytes( + token: sessionUser.token, + transactionId: query.transactionId, + copyFor: query.copyFor, + ); + + final dir = await Directory.systemTemp.createTemp('e_receipt_mobile'); + final file = File( + '${dir.path}/receipt_${query.transactionId}_${query.copyFor}.pdf', + ); + await file.writeAsBytes(bytes, flush: true); + return ReceiptPdfViewData(file); + } catch (e, st) { + debugPrint('[RECEIPT][ERROR] ${Error.safeToString(e)}\n$st'); + throw Exception(Error.safeToString(e)); + } + }); diff --git a/lib/presentation/terminal/terminal_next_screen.dart b/lib/presentation/terminal/terminal_next_screen.dart index 04f1ccd..0f94be3 100644 --- a/lib/presentation/terminal/terminal_next_screen.dart +++ b/lib/presentation/terminal/terminal_next_screen.dart @@ -1,756 +1,756 @@ -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:e_receipt_mobile/presentation/auth/logout_view_model.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_state.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_view_model.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_receipt_screen.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class TerminalNextScreen extends ConsumerStatefulWidget { - const TerminalNextScreen({ - required this.merchantName, - required this.terminal, - super.key, - }); - - final String merchantName; - final Terminal terminal; - - @override - ConsumerState createState() => _TerminalNextScreenState(); -} - -class _TerminalNextScreenState extends ConsumerState { - static const String _selectedRange = '4m'; - - @override - Widget build(BuildContext context) { - final user = ref.watch(sessionControllerProvider); - final isCashier = (user?.role ?? '').trim().toLowerCase() == 'cashier'; - final logoutState = ref.watch(logoutViewModelProvider); - final serial = widget.terminal.serial?.trim() ?? ''; - final hasSerial = serial.isNotEmpty; - final query = TransactionQuery(serial: serial, range: _selectedRange); - final pagingState = hasSerial - ? ref.watch(transactionPagingViewModelProvider(query)) - : null; - final pagingViewModel = hasSerial - ? ref.read(transactionPagingViewModelProvider(query).notifier) - : null; - final colorScheme = Theme.of(context).colorScheme; - final terminalName = - _sanitizeMultiline(widget.terminal.name) ?? widget.terminal.tid ?? '-'; - - return Scaffold( - appBar: AppBar( - title: Text(terminalName), - actions: [ - if (isCashier) - IconButton( - tooltip: 'Account', - icon: const Icon(Icons.more_vert), - onPressed: logoutState.isLoading - ? null - : () => _showCashierMenu( - context, - username: (user?.username ?? '').trim(), - ), - ), - ], - ), - body: SafeArea( - child: RefreshIndicator.adaptive( - onRefresh: () async { - if (!hasSerial || pagingViewModel == null) { - return; - } - await pagingViewModel.refresh(); - }, - child: NotificationListener( - onNotification: (notification) { - if (!hasSerial || - pagingState == null || - pagingViewModel == null || - !pagingState.hasMore || - pagingState.isLoadingMore) { - return false; - } - if (notification.metrics.axis != Axis.vertical) { - return false; - } - if (notification.metrics.maxScrollExtent <= 0) { - return false; - } - if (notification.metrics.pixels >= - notification.metrics.maxScrollExtent - 240) { - pagingViewModel.loadMore(); - } - return false; - }, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), - child: Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(22), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.55), - ), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: colorScheme.primary.withOpacity( - 0.14, - ), - ), - child: Icon( - Icons.point_of_sale_outlined, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - widget.merchantName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - Text( - terminalName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: - colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - _InfoRow( - label: 'Serial', - value: widget.terminal.serial, - ), - _InfoRow( - label: 'Address', - value: _sanitizeMultiline( - widget.terminal.address, - ), - ), - ], - ), - ), - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: Text( - 'Transactions (4M)', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - ), - ), - if (!hasSerial) - SliverFillRemaining( - hasScrollBody: false, - child: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.info_outline, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'Serial is required to load transactions', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - 'This terminal does not have a serial number.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ) - else - ..._buildPagingSlivers( - context: context, - colorScheme: colorScheme, - pagingState: pagingState!, - pagingViewModel: pagingViewModel!, - ), - ], - ), - ), - ), - ), - ); - } - - Future _showCashierMenu( - BuildContext context, { - required String username, - }) async { - HapticFeedback.selectionClick(); - - final action = await showModalBottomSheet<_CashierMenuAction>( - context: context, - showDragHandle: true, - isScrollControlled: true, - useSafeArea: true, - builder: (context) { - final bottomInset = MediaQuery.viewPaddingOf(context).bottom; - final displayName = username.isEmpty ? 'Cashier' : username; - final initial = displayName.trim().isEmpty - ? '?' - : displayName.trim().characters.first.toUpperCase(); - - return Padding( - padding: EdgeInsets.fromLTRB(8, 0, 8, 16 + bottomInset), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: CircleAvatar(child: Text(initial)), - title: Text(displayName), - subtitle: const Text('Cashier'), - ), - const Divider(height: 1), - ListTile( - leading: const Icon(Icons.logout, color: Colors.redAccent), - title: const Text('Logout'), - onTap: () => Navigator.of(context).pop(_CashierMenuAction.logout), - ), - ], - ), - ); - }, - ); - - if (!mounted || action == null) { - return; - } - - switch (action) { - case _CashierMenuAction.logout: - await _logout(context); - } - } - - Future _logout(BuildContext context) async { - final shouldLogout = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Logout'), - content: const Text('Are you sure you want to logout?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - FilledButton.tonal( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Logout'), - ), - ], - ); - }, - ); - - if (shouldLogout != true || !context.mounted) { - return; - } - - HapticFeedback.mediumImpact(); - Navigator.of(context).popUntil((route) => route.isFirst); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Signed out'), - behavior: SnackBarBehavior.floating, - ), - ); - - await ref.read(logoutViewModelProvider.notifier).logout(); - } - - List _buildPagingSlivers({ - required BuildContext context, - required ColorScheme colorScheme, - required TransactionPagingState pagingState, - required TransactionPagingViewModel pagingViewModel, - }) { - if (pagingState.isLoading && pagingState.items.isEmpty) { - return const [ - SliverFillRemaining(child: Center(child: CircularProgressIndicator())), - ]; - } - - if (pagingState.errorMessage != null && pagingState.items.isEmpty) { - return [ - SliverFillRemaining( - hasScrollBody: false, - child: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.wifi_off_outlined, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'Failed to load transactions', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - pagingState.errorMessage!, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 14), - FilledButton.tonal( - onPressed: pagingViewModel.refresh, - child: const Text('Try again'), - ), - ], - ), - ), - ), - ), - ]; - } - - if (pagingState.items.isEmpty) { - return [ - SliverFillRemaining( - hasScrollBody: false, - child: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.receipt_long_outlined, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'No transactions found', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - 'Pull down to refresh.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ), - ]; - } - - final bottomInset = MediaQuery.viewPaddingOf(context).bottom; - final items = pagingState.items; - final hasFooter = pagingState.hasMore; - - return [ - SliverPadding( - padding: EdgeInsets.fromLTRB(16, 0, 16, bottomInset + 16), - sliver: SliverList.separated( - itemCount: items.length + (hasFooter ? 1 : 0), - separatorBuilder: (_, __) => const SizedBox(height: 10), - itemBuilder: (context, index) { - if (index >= items.length) { - return _TransactionPaginationFooter( - isLoadingMore: pagingState.isLoadingMore, - onLoadMore: pagingViewModel.loadMore, - ); - } - - final item = items[index]; - final raw = item.raw ?? const {}; - final amount = - _first(raw, const ['DE4']) ?? item.amount?.toString() ?? '-'; - final currency = _first(raw, const ['DE49']) ?? 'MMK'; - final status = _statusLabel( - _first(raw, const ['DE39']) ?? - item.status, - ); - final type = _first(raw, const ['DE3']) ?? item.type ?? '-'; - final rrn = _first(raw, const ['DE37']) ?? item.rrn ?? '-'; - final createdAt = - _first(raw, const ['CREATED_AT', 'de7_date']) ?? item.createdAt; - - return Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.55), - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(18), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => TransactionReceiptScreen( - merchantName: widget.merchantName, - terminal: widget.terminal, - transaction: item, - ), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - child: Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: colorScheme.primary.withOpacity(0.14), - ), - child: Icon( - Icons.receipt_long_outlined, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - '$amount $currency', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w800), - ), - ), - const SizedBox(width: 8), - _StatusChip(status: status), - ], - ), - const SizedBox(height: 4), - Text( - 'Type: $type • RRN: $rrn', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - if ((createdAt ?? '').trim().isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - createdAt!.trim(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ), - ), - const SizedBox(width: 8), - Icon( - Icons.chevron_right, - color: colorScheme.onSurfaceVariant, - ), - ], - ), - ), - ), - ); - }, - ), - ), - if (pagingState.errorMessage != null && pagingState.items.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.fromLTRB( - 16, - 8, - 16, - MediaQuery.viewPaddingOf(context).bottom + 8, - ), - child: Material( - color: colorScheme.errorContainer, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: colorScheme.onErrorContainer, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - pagingState.errorMessage!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onErrorContainer, - ), - ), - ), - const SizedBox(width: 8), - TextButton( - onPressed: pagingViewModel.loadMore, - child: const Text('Retry'), - ), - ], - ), - ), - ), - ), - ), - ]; - } - - String? _first(Map raw, List keys) { - for (final key in keys) { - final value = raw[key]; - if (value != null) { - final text = value.toString().trim(); - if (text.isNotEmpty) { - return text; - } - } - } - return null; - } - - String _statusLabel(String? status) { - final normalized = status?.trim().toUpperCase(); - if (normalized == 'A') { - return 'SUCCESS'; - } - if (normalized == 'E' ) { - return 'FAILED'; - } - return 'SUCCESS'; - - } - - String? _sanitizeMultiline(String? value) { - if (value == null) { - return null; - } - final cleaned = value - .replaceAll('\r', ' ') - .replaceAll('\n', ' ') - .replaceAll(r'\n', ' ') - .replaceAll(r'/n', ' ') - .replaceAll(RegExp(r'\s+'), ' ') - .trim(); - return cleaned.isEmpty ? null : cleaned; - } -} - -class _TransactionPaginationFooter extends StatelessWidget { - const _TransactionPaginationFooter({ - required this.isLoadingMore, - required this.onLoadMore, - }); - - final bool isLoadingMore; - final VoidCallback onLoadMore; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Expanded( - child: Text( - isLoadingMore ? 'Loading more...' : 'More results available', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - ), - if (isLoadingMore) - const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - else - FilledButton.tonal( - onPressed: onLoadMore, - child: const Text('Load more'), - ), - ], - ), - ), - ); - } -} - -class _InfoRow extends StatelessWidget { - const _InfoRow({required this.label, required this.value}); - - final String label; - final String? value; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final text = (value ?? '').trim().isEmpty ? '-' : value!.trim(); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - Expanded( - child: Text( - label, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - text, - textAlign: TextAlign.right, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -class _StatusChip extends StatelessWidget { - const _StatusChip({required this.status}); - - final String status; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final normalized = status.toUpperCase(); - final isSuccess = normalized == 'SUCCESS'; - - final background = isSuccess - ? Colors.green.withOpacity(0.12) - : colorScheme.errorContainer.withOpacity(0.9); - final foreground = isSuccess - ? Colors.green.shade700 - : colorScheme.onErrorContainer; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(999), - border: Border.all(color: foreground.withOpacity(0.25)), - ), - child: Text( - normalized.isEmpty ? '-' : normalized, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w800, - color: foreground, - ), - ), - ); - } -} - -enum _CashierMenuAction { logout } +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:e_receipt_mobile/presentation/auth/logout_view_model.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_state.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_view_model.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_receipt_screen.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TerminalNextScreen extends ConsumerStatefulWidget { + const TerminalNextScreen({ + required this.merchantName, + required this.terminal, + super.key, + }); + + final String merchantName; + final Terminal terminal; + + @override + ConsumerState createState() => _TerminalNextScreenState(); +} + +class _TerminalNextScreenState extends ConsumerState { + static const String _selectedRange = '4m'; + + @override + Widget build(BuildContext context) { + final user = ref.watch(sessionControllerProvider); + final isCashier = (user?.role ?? '').trim().toLowerCase() == 'cashier'; + final logoutState = ref.watch(logoutViewModelProvider); + final serial = widget.terminal.serial?.trim() ?? ''; + final hasSerial = serial.isNotEmpty; + final query = TransactionQuery(serial: serial, range: _selectedRange); + final pagingState = hasSerial + ? ref.watch(transactionPagingViewModelProvider(query)) + : null; + final pagingViewModel = hasSerial + ? ref.read(transactionPagingViewModelProvider(query).notifier) + : null; + final colorScheme = Theme.of(context).colorScheme; + final terminalName = + _sanitizeMultiline(widget.terminal.name) ?? widget.terminal.tid ?? '-'; + + return Scaffold( + appBar: AppBar( + title: Text(terminalName), + actions: [ + if (isCashier) + IconButton( + tooltip: 'Account', + icon: const Icon(Icons.more_vert), + onPressed: logoutState.isLoading + ? null + : () => _showCashierMenu( + context, + username: (user?.username ?? '').trim(), + ), + ), + ], + ), + body: SafeArea( + child: RefreshIndicator.adaptive( + onRefresh: () async { + if (!hasSerial || pagingViewModel == null) { + return; + } + await pagingViewModel.refresh(); + }, + child: NotificationListener( + onNotification: (notification) { + if (!hasSerial || + pagingState == null || + pagingViewModel == null || + !pagingState.hasMore || + pagingState.isLoadingMore) { + return false; + } + if (notification.metrics.axis != Axis.vertical) { + return false; + } + if (notification.metrics.maxScrollExtent <= 0) { + return false; + } + if (notification.metrics.pixels >= + notification.metrics.maxScrollExtent - 240) { + pagingViewModel.loadMore(); + } + return false; + }, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), + child: Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.55), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: colorScheme.primary.withOpacity( + 0.14, + ), + ), + child: Icon( + Icons.point_of_sale_outlined, + color: colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + widget.merchantName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + Text( + terminalName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: + colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + _InfoRow( + label: 'Serial', + value: widget.terminal.serial, + ), + _InfoRow( + label: 'Address', + value: _sanitizeMultiline( + widget.terminal.address, + ), + ), + ], + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text( + 'Transactions (4M)', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + ), + if (!hasSerial) + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'Serial is required to load transactions', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + 'This terminal does not have a serial number.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ) + else + ..._buildPagingSlivers( + context: context, + colorScheme: colorScheme, + pagingState: pagingState!, + pagingViewModel: pagingViewModel!, + ), + ], + ), + ), + ), + ), + ); + } + + Future _showCashierMenu( + BuildContext context, { + required String username, + }) async { + HapticFeedback.selectionClick(); + + final action = await showModalBottomSheet<_CashierMenuAction>( + context: context, + showDragHandle: true, + isScrollControlled: true, + useSafeArea: true, + builder: (context) { + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + final displayName = username.isEmpty ? 'Cashier' : username; + final initial = displayName.trim().isEmpty + ? '?' + : displayName.trim().characters.first.toUpperCase(); + + return Padding( + padding: EdgeInsets.fromLTRB(8, 0, 8, 16 + bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: CircleAvatar(child: Text(initial)), + title: Text(displayName), + subtitle: const Text('Cashier'), + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.logout, color: Colors.redAccent), + title: const Text('Logout'), + onTap: () => Navigator.of(context).pop(_CashierMenuAction.logout), + ), + ], + ), + ); + }, + ); + + if (!mounted || action == null) { + return; + } + + switch (action) { + case _CashierMenuAction.logout: + await _logout(context); + } + } + + Future _logout(BuildContext context) async { + final shouldLogout = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Logout'), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + FilledButton.tonal( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Logout'), + ), + ], + ); + }, + ); + + if (shouldLogout != true || !context.mounted) { + return; + } + + HapticFeedback.mediumImpact(); + Navigator.of(context).popUntil((route) => route.isFirst); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Signed out'), + behavior: SnackBarBehavior.floating, + ), + ); + + await ref.read(logoutViewModelProvider.notifier).logout(); + } + + List _buildPagingSlivers({ + required BuildContext context, + required ColorScheme colorScheme, + required TransactionPagingState pagingState, + required TransactionPagingViewModel pagingViewModel, + }) { + if (pagingState.isLoading && pagingState.items.isEmpty) { + return const [ + SliverFillRemaining(child: Center(child: CircularProgressIndicator())), + ]; + } + + if (pagingState.errorMessage != null && pagingState.items.isEmpty) { + return [ + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.wifi_off_outlined, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'Failed to load transactions', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + pagingState.errorMessage!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 14), + FilledButton.tonal( + onPressed: pagingViewModel.refresh, + child: const Text('Try again'), + ), + ], + ), + ), + ), + ), + ]; + } + + if (pagingState.items.isEmpty) { + return [ + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.receipt_long_outlined, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'No transactions found', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + 'Pull down to refresh.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ), + ]; + } + + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + final items = pagingState.items; + final hasFooter = pagingState.hasMore; + + return [ + SliverPadding( + padding: EdgeInsets.fromLTRB(16, 0, 16, bottomInset + 16), + sliver: SliverList.separated( + itemCount: items.length + (hasFooter ? 1 : 0), + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + if (index >= items.length) { + return _TransactionPaginationFooter( + isLoadingMore: pagingState.isLoadingMore, + onLoadMore: pagingViewModel.loadMore, + ); + } + + final item = items[index]; + final raw = item.raw ?? const {}; + final amount = + _first(raw, const ['DE4']) ?? item.amount?.toString() ?? '-'; + final currency = _first(raw, const ['DE49']) ?? 'MMK'; + final status = _statusLabel( + _first(raw, const ['DE39']) ?? + item.status, + ); + final type = _first(raw, const ['DE3']) ?? item.type ?? '-'; + final rrn = _first(raw, const ['DE37']) ?? item.rrn ?? '-'; + final createdAt = + _first(raw, const ['CREATED_AT', 'de7_date']) ?? item.createdAt; + + return Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.55), + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => TransactionReceiptScreen( + merchantName: widget.merchantName, + terminal: widget.terminal, + transaction: item, + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + child: Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: colorScheme.primary.withOpacity(0.14), + ), + child: Icon( + Icons.receipt_long_outlined, + color: colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '$amount $currency', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w800), + ), + ), + const SizedBox(width: 8), + _StatusChip(status: status), + ], + ), + const SizedBox(height: 4), + Text( + 'Type: $type • RRN: $rrn', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + if ((createdAt ?? '').trim().isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + createdAt!.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + const SizedBox(width: 8), + Icon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + }, + ), + ), + if (pagingState.errorMessage != null && pagingState.items.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.fromLTRB( + 16, + 8, + 16, + MediaQuery.viewPaddingOf(context).bottom + 8, + ), + child: Material( + color: colorScheme.errorContainer, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: colorScheme.onErrorContainer, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + pagingState.errorMessage!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onErrorContainer, + ), + ), + ), + const SizedBox(width: 8), + TextButton( + onPressed: pagingViewModel.loadMore, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ), + ]; + } + + String? _first(Map raw, List keys) { + for (final key in keys) { + final value = raw[key]; + if (value != null) { + final text = value.toString().trim(); + if (text.isNotEmpty) { + return text; + } + } + } + return null; + } + + String _statusLabel(String? status) { + final normalized = status?.trim().toUpperCase(); + if (normalized == 'A') { + return 'SUCCESS'; + } + if (normalized == 'E' ) { + return 'FAILED'; + } + return 'SUCCESS'; + + } + + String? _sanitizeMultiline(String? value) { + if (value == null) { + return null; + } + final cleaned = value + .replaceAll('\r', ' ') + .replaceAll('\n', ' ') + .replaceAll(r'\n', ' ') + .replaceAll(r'/n', ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + return cleaned.isEmpty ? null : cleaned; + } +} + +class _TransactionPaginationFooter extends StatelessWidget { + const _TransactionPaginationFooter({ + required this.isLoadingMore, + required this.onLoadMore, + }); + + final bool isLoadingMore; + final VoidCallback onLoadMore; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: Text( + isLoadingMore ? 'Loading more...' : 'More results available', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ), + if (isLoadingMore) + const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + FilledButton.tonal( + onPressed: onLoadMore, + child: const Text('Load more'), + ), + ], + ), + ), + ); + } +} + +class _InfoRow extends StatelessWidget { + const _InfoRow({required this.label, required this.value}); + + final String label; + final String? value; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final text = (value ?? '').trim().isEmpty ? '-' : value!.trim(); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + textAlign: TextAlign.right, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + +class _StatusChip extends StatelessWidget { + const _StatusChip({required this.status}); + + final String status; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final normalized = status.toUpperCase(); + final isSuccess = normalized == 'SUCCESS'; + + final background = isSuccess + ? Colors.green.withOpacity(0.12) + : colorScheme.errorContainer.withOpacity(0.9); + final foreground = isSuccess + ? Colors.green.shade700 + : colorScheme.onErrorContainer; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: foreground.withOpacity(0.25)), + ), + child: Text( + normalized.isEmpty ? '-' : normalized, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w800, + color: foreground, + ), + ), + ); + } +} + +enum _CashierMenuAction { logout } diff --git a/lib/presentation/terminal/terminal_selection_screen.dart b/lib/presentation/terminal/terminal_selection_screen.dart index d504769..e28b680 100644 --- a/lib/presentation/terminal/terminal_selection_screen.dart +++ b/lib/presentation/terminal/terminal_selection_screen.dart @@ -1,118 +1,118 @@ -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:e_receipt_mobile/presentation/home/home_view_model.dart'; -import 'package:e_receipt_mobile/presentation/terminal/terminal_next_screen.dart'; -import 'package:e_receipt_mobile/presentation/terminal/widgets/terminal_header.dart'; -import 'package:e_receipt_mobile/presentation/terminal/widgets/terminal_list_view.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class TerminalSelectionScreen extends ConsumerWidget { - const TerminalSelectionScreen({ - required this.merchantId, - required this.merchantName, - super.key, - }); - - final String merchantId; - final String merchantName; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final terminalsAsync = ref.watch(terminalListProvider(merchantId)); - final colorScheme = Theme.of(context).colorScheme; - - return Scaffold( - appBar: AppBar( - title: Text(merchantName), - actions: [ - IconButton( - tooltip: 'Refresh', - onPressed: terminalsAsync.isLoading - ? null - : () => ref.invalidate(terminalListProvider(merchantId)), - icon: const Icon(Icons.refresh), - ), - ], - ), - body: SafeArea( - bottom: true, - child: terminalsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Padding( - padding: EdgeInsets.fromLTRB( - 24, - 24, - 24, - MediaQuery.viewPaddingOf(context).bottom + 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.wifi_off_outlined, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - 'Failed to load terminals', - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Text( - '$error', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 14), - FilledButton.tonal( - onPressed: () => - ref.invalidate(terminalListProvider(merchantId)), - child: const Text('Try again'), - ), - ], - ), - ), - ), - data: (terminals) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TerminalHeader( - merchantName: merchantName, - totalCount: terminals.length, - ), - Expanded( - child: TerminalListView( - terminals: terminals, - emptyMessage: 'No terminals found', - onRefresh: () async { - await ref.refresh( - terminalListProvider(merchantId).future, - ); - }, - onTerminalTap: (terminal) => - _openNextScreen(context, terminal), - ), - ), - ], - ); - }, - ), - ), - ); - } - - void _openNextScreen(BuildContext context, Terminal terminal) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - TerminalNextScreen(merchantName: merchantName, terminal: terminal), - ), - ); - } -} +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:e_receipt_mobile/presentation/home/home_view_model.dart'; +import 'package:e_receipt_mobile/presentation/terminal/terminal_next_screen.dart'; +import 'package:e_receipt_mobile/presentation/terminal/widgets/terminal_header.dart'; +import 'package:e_receipt_mobile/presentation/terminal/widgets/terminal_list_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TerminalSelectionScreen extends ConsumerWidget { + const TerminalSelectionScreen({ + required this.merchantId, + required this.merchantName, + super.key, + }); + + final String merchantId; + final String merchantName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final terminalsAsync = ref.watch(terminalListProvider(merchantId)); + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + title: Text(merchantName), + actions: [ + IconButton( + tooltip: 'Refresh', + onPressed: terminalsAsync.isLoading + ? null + : () => ref.invalidate(terminalListProvider(merchantId)), + icon: const Icon(Icons.refresh), + ), + ], + ), + body: SafeArea( + bottom: true, + child: terminalsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: EdgeInsets.fromLTRB( + 24, + 24, + 24, + MediaQuery.viewPaddingOf(context).bottom + 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.wifi_off_outlined, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + 'Failed to load terminals', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + '$error', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 14), + FilledButton.tonal( + onPressed: () => + ref.invalidate(terminalListProvider(merchantId)), + child: const Text('Try again'), + ), + ], + ), + ), + ), + data: (terminals) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TerminalHeader( + merchantName: merchantName, + totalCount: terminals.length, + ), + Expanded( + child: TerminalListView( + terminals: terminals, + emptyMessage: 'No terminals found', + onRefresh: () async { + await ref.refresh( + terminalListProvider(merchantId).future, + ); + }, + onTerminalTap: (terminal) => + _openNextScreen(context, terminal), + ), + ), + ], + ); + }, + ), + ), + ); + } + + void _openNextScreen(BuildContext context, Terminal terminal) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + TerminalNextScreen(merchantName: merchantName, terminal: terminal), + ), + ); + } +} diff --git a/lib/presentation/terminal/transaction_pagination_providers.dart b/lib/presentation/terminal/transaction_pagination_providers.dart index aa14f09..e623148 100644 --- a/lib/presentation/terminal/transaction_pagination_providers.dart +++ b/lib/presentation/terminal/transaction_pagination_providers.dart @@ -1,4 +1,4 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final transactionPageSizeProvider = Provider((ref) => 8); - +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final transactionPageSizeProvider = Provider((ref) => 8); + diff --git a/lib/presentation/terminal/transaction_paging_state.dart b/lib/presentation/terminal/transaction_paging_state.dart index 311435d..e19f64f 100644 --- a/lib/presentation/terminal/transaction_paging_state.dart +++ b/lib/presentation/terminal/transaction_paging_state.dart @@ -1,45 +1,45 @@ -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; -import 'package:flutter/foundation.dart'; - -@immutable -class TransactionPagingState { - const TransactionPagingState({ - this.items = const [], - this.page = 1, - this.limit = 8, - this.isLoading = false, - this.isLoadingMore = false, - this.hasMore = true, - this.errorMessage, - }); - - final List items; - final int page; - final int limit; - final bool isLoading; - final bool isLoadingMore; - final bool hasMore; - final String? errorMessage; - - TransactionPagingState copyWith({ - List? items, - int? page, - int? limit, - bool? isLoading, - bool? isLoadingMore, - bool? hasMore, - String? errorMessage, - bool clearError = false, - }) { - return TransactionPagingState( - items: items ?? this.items, - page: page ?? this.page, - limit: limit ?? this.limit, - isLoading: isLoading ?? this.isLoading, - isLoadingMore: isLoadingMore ?? this.isLoadingMore, - hasMore: hasMore ?? this.hasMore, - errorMessage: clearError ? null : errorMessage ?? this.errorMessage, - ); - } -} - +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +class TransactionPagingState { + const TransactionPagingState({ + this.items = const [], + this.page = 1, + this.limit = 8, + this.isLoading = false, + this.isLoadingMore = false, + this.hasMore = true, + this.errorMessage, + }); + + final List items; + final int page; + final int limit; + final bool isLoading; + final bool isLoadingMore; + final bool hasMore; + final String? errorMessage; + + TransactionPagingState copyWith({ + List? items, + int? page, + int? limit, + bool? isLoading, + bool? isLoadingMore, + bool? hasMore, + String? errorMessage, + bool clearError = false, + }) { + return TransactionPagingState( + items: items ?? this.items, + page: page ?? this.page, + limit: limit ?? this.limit, + isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasMore: hasMore ?? this.hasMore, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + ); + } +} + diff --git a/lib/presentation/terminal/transaction_paging_view_model.dart b/lib/presentation/terminal/transaction_paging_view_model.dart index 0296fc9..e69da0a 100644 --- a/lib/presentation/terminal/transaction_paging_view_model.dart +++ b/lib/presentation/terminal/transaction_paging_view_model.dart @@ -1,151 +1,151 @@ -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; -import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_pagination_providers.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_state.dart'; -import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final transactionPagingViewModelProvider = - StateNotifierProvider.autoDispose.family< - TransactionPagingViewModel, - TransactionPagingState, - TransactionQuery - >((ref, query) { - final sessionUser = ref.watch(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - final limit = ref.watch(transactionPageSizeProvider); - final viewModel = TransactionPagingViewModel( - repository: ref.watch(transactionRepositoryProvider), - token: sessionUser.token, - serial: query.serial, - range: query.range, - limit: limit, - ); - viewModel.loadInitial(); - return viewModel; - }); - -class TransactionPagingViewModel extends StateNotifier { - TransactionPagingViewModel({ - required TransactionRepository repository, - required String token, - required String serial, - required String range, - required int limit, - }) : _repository = repository, - _token = token, - _serial = serial, - _range = range, - super(TransactionPagingState(limit: limit)); - - final TransactionRepository _repository; - final String _token; - final String _serial; - final String _range; - - Future loadInitial() async { - await refresh(); - } - - Future refresh() async { - state = state.copyWith( - isLoading: true, - isLoadingMore: false, - page: 1, - items: const [], - hasMore: true, - clearError: true, - ); - - try { - final results = await _repository.getTransactions( - token: _token, - serial: _serial, - range: _range, - page: 1, - limit: state.limit, - ); - state = state.copyWith( - isLoading: false, - items: results, - page: 1, - hasMore: results.length >= state.limit, - ); - } catch (error) { - state = state.copyWith( - isLoading: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } - - Future loadMore() async { - if (state.isLoading || state.isLoadingMore || !state.hasMore) { - return; - } - - state = state.copyWith(isLoadingMore: true, clearError: true); - final nextPage = state.page + 1; - - try { - final results = await _repository.getTransactions( - token: _token, - serial: _serial, - range: _range, - page: nextPage, - limit: state.limit, - ); - - final merged = _mergeUnique(state.items, results); - final addedCount = merged.length - state.items.length; - final hasMore = results.length >= state.limit && addedCount > 0; - - state = state.copyWith( - isLoadingMore: false, - items: merged, - page: nextPage, - hasMore: hasMore, - ); - } catch (error) { - state = state.copyWith( - isLoadingMore: false, - errorMessage: error.toString().replaceFirst('Exception: ', ''), - ); - } - } - - List _mergeUnique( - List existing, - List incoming, - ) { - if (existing.isEmpty) { - return incoming; - } - - final keys = existing.map(_keyFor).toSet(); - final merged = [...existing]; - for (final item in incoming) { - final key = _keyFor(item); - if (keys.add(key)) { - merged.add(item); - } - } - return merged; - } - - String _keyFor(TransactionRecord item) { - return [ - item.id, - item.rrn, - item.createdAt, - item.amount?.toString(), - item.status, - item.type, - ].join('|'); - } -} - +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; +import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_pagination_providers.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_paging_state.dart'; +import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final transactionPagingViewModelProvider = + StateNotifierProvider.autoDispose.family< + TransactionPagingViewModel, + TransactionPagingState, + TransactionQuery + >((ref, query) { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + final limit = ref.watch(transactionPageSizeProvider); + final viewModel = TransactionPagingViewModel( + repository: ref.watch(transactionRepositoryProvider), + token: sessionUser.token, + serial: query.serial, + range: query.range, + limit: limit, + ); + viewModel.loadInitial(); + return viewModel; + }); + +class TransactionPagingViewModel extends StateNotifier { + TransactionPagingViewModel({ + required TransactionRepository repository, + required String token, + required String serial, + required String range, + required int limit, + }) : _repository = repository, + _token = token, + _serial = serial, + _range = range, + super(TransactionPagingState(limit: limit)); + + final TransactionRepository _repository; + final String _token; + final String _serial; + final String _range; + + Future loadInitial() async { + await refresh(); + } + + Future refresh() async { + state = state.copyWith( + isLoading: true, + isLoadingMore: false, + page: 1, + items: const [], + hasMore: true, + clearError: true, + ); + + try { + final results = await _repository.getTransactions( + token: _token, + serial: _serial, + range: _range, + page: 1, + limit: state.limit, + ); + state = state.copyWith( + isLoading: false, + items: results, + page: 1, + hasMore: results.length >= state.limit, + ); + } catch (error) { + state = state.copyWith( + isLoading: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } + + Future loadMore() async { + if (state.isLoading || state.isLoadingMore || !state.hasMore) { + return; + } + + state = state.copyWith(isLoadingMore: true, clearError: true); + final nextPage = state.page + 1; + + try { + final results = await _repository.getTransactions( + token: _token, + serial: _serial, + range: _range, + page: nextPage, + limit: state.limit, + ); + + final merged = _mergeUnique(state.items, results); + final addedCount = merged.length - state.items.length; + final hasMore = results.length >= state.limit && addedCount > 0; + + state = state.copyWith( + isLoadingMore: false, + items: merged, + page: nextPage, + hasMore: hasMore, + ); + } catch (error) { + state = state.copyWith( + isLoadingMore: false, + errorMessage: error.toString().replaceFirst('Exception: ', ''), + ); + } + } + + List _mergeUnique( + List existing, + List incoming, + ) { + if (existing.isEmpty) { + return incoming; + } + + final keys = existing.map(_keyFor).toSet(); + final merged = [...existing]; + for (final item in incoming) { + final key = _keyFor(item); + if (keys.add(key)) { + merged.add(item); + } + } + return merged; + } + + String _keyFor(TransactionRecord item) { + return [ + item.id, + item.rrn, + item.createdAt, + item.amount?.toString(), + item.status, + item.type, + ].join('|'); + } +} + diff --git a/lib/presentation/terminal/transaction_receipt_screen.dart b/lib/presentation/terminal/transaction_receipt_screen.dart index d67fbd8..5da41b9 100644 --- a/lib/presentation/terminal/transaction_receipt_screen.dart +++ b/lib/presentation/terminal/transaction_receipt_screen.dart @@ -1,403 +1,403 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/terminal/receipt_view_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_pdfview/flutter_pdfview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:printing/printing.dart'; - -class TransactionReceiptScreen extends ConsumerWidget { - const TransactionReceiptScreen({ - required this.merchantName, - required this.terminal, - required this.transaction, - super.key, - }); - - final String merchantName; - final Terminal terminal; - final TransactionRecord transaction; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final transactionId = transaction.id?.trim(); - if (transactionId == null || transactionId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Receipt')), - body: const SafeArea( - child: Center(child: Text('Missing transaction id')), - ), - ); - } - - final pdfAsync = ref.watch( - transactionReceiptViewDataProvider( - TransactionReceiptQuery( - transactionId: transactionId, - copyFor: 'Merchant', - ), - ), - ); - - return Scaffold( - appBar: AppBar( - title: const Text('Receipt'), - actions: [ - IconButton( - tooltip: 'Reload', - onPressed: () => ref.invalidate( - transactionReceiptViewDataProvider( - TransactionReceiptQuery( - transactionId: transactionId, - copyFor: 'Merchant', - ), - ), - ), - icon: const Icon(Icons.refresh), - ), - ], - ), - body: SafeArea( - bottom: true, - child: pdfAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Padding( - padding: EdgeInsets.fromLTRB( - 24, - 24, - 24, - MediaQuery.viewPaddingOf(context).bottom + 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.picture_as_pdf_outlined, size: 56), - const SizedBox(height: 12), - Text( - 'Failed to load receipt PDF', - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Text( - Error.safeToString(error), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 14), - FilledButton.tonal( - onPressed: () => ref.invalidate( - transactionReceiptViewDataProvider( - TransactionReceiptQuery( - transactionId: transactionId, - copyFor: 'Merchant', - ), - ), - ), - child: const Text('Try again'), - ), - ], - ), - ), - ), - data: (viewData) => Column( - children: [ - Expanded( - child: switch (viewData) { - ReceiptPdfViewData() => _ReceiptViewport( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: PDFView( - filePath: viewData.file.path, - enableSwipe: true, - swipeHorizontal: false, - autoSpacing: false, - pageFling: true, - pageSnap: true, - fitEachPage: true, - fitPolicy: FitPolicy.WIDTH, - backgroundColor: Theme.of(context).colorScheme.primary, - onError: (error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'PDF error: ${Error.safeToString(error)}', - ), - ), - ); - }, - onPageError: (page, error) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'PDF page $page error: ${Error.safeToString(error)}', - ), - ), - ); - }, - ), - ), - }, - ), - SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Row( - children: [ - Expanded( - child: FilledButton.icon( - onPressed: () => _downloadReceipt( - context: context, - ref: ref, - transactionId: transactionId, - ), - icon: const Icon(Icons.download_outlined), - label: const Text('Download PDF'), - ), - ), - const SizedBox(width: 12), - Expanded( - child: FilledButton.tonal( - onPressed: () => _printReceipt( - context: context, - ref: ref, - transactionId: transactionId, - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.print_outlined), - SizedBox(width: 8), - Text('Print PDF'), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class _ReceiptViewport extends StatelessWidget { - const _ReceiptViewport({required this.child, this.padding}); - - final Widget child; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final viewPadding = MediaQuery.viewPaddingOf(context); - final size = MediaQuery.sizeOf(context); - final maxHeight = size.height * 0.60; - - return Padding( - padding: - padding ?? EdgeInsets.fromLTRB(16, 16, 16, viewPadding.bottom + 16), - child: LayoutBuilder( - builder: (context, constraints) { - final double targetHeight = maxHeight - .clamp(0, constraints.maxHeight) - .toDouble(); - - return Align( - alignment: Alignment.center, - child: SizedBox( - width: 230, - height: targetHeight, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.7), - border: Border.all(color: colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(16), - ), - child: SizedBox.expand(child: child), - ), - ), - ), - ); - }, - ), - ); - } -} - -Future _downloadReceipt({ - required BuildContext context, - required WidgetRef ref, - required String transactionId, -}) async { - try { - final sanitizedId = transactionId.replaceAll( - RegExp(r'[^A-Za-z0-9_-]'), - '_', - ); - final stamp = DateTime.now().toIso8601String().replaceAll(':', '-'); - - final Uint8List bytes = await _runWithProgress( - context, - message: 'Preparing receipt…', - task: () => _fetchReceiptPdfBytes( - ref: ref, - transactionId: transactionId, - copyFor: 'Merchant', - ), - ); - - final candidateBaseDirs = []; - if (!kIsWeb) { - if (Platform.isAndroid) { - final dir = await getExternalStorageDirectory(); - if (dir != null) { - candidateBaseDirs.add(dir); - } - } - final downloads = await getDownloadsDirectory(); - if (downloads != null) { - candidateBaseDirs.add(downloads); - } - } - candidateBaseDirs.add(await getApplicationDocumentsDirectory()); - - File? outFile; - Object? lastError; - for (final baseDir in candidateBaseDirs) { - try { - final receiptsDir = Directory( - '${baseDir.path}${Platform.pathSeparator}receipts', - ); - if (!await receiptsDir.exists()) { - await receiptsDir.create(recursive: true); - } - - final candidate = File( - '${receiptsDir.path}${Platform.pathSeparator}receipt_${sanitizedId}_merchant_$stamp.pdf', - ); - await candidate.writeAsBytes(bytes, flush: true); - outFile = candidate; - break; - } catch (e) { - lastError = e; - } - } - - if (outFile == null) { - throw lastError ?? Exception('Failed to save receipt PDF'); - } - - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Saved: ${outFile.path}'))); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Download failed: $e'))); - } - } -} - -Future _printReceipt({ - required BuildContext context, - required WidgetRef ref, - required String transactionId, -}) async { - try { - final Uint8List bytes = await _runWithProgress( - context, - message: 'Preparing print…', - task: () => _fetchReceiptPdfBytes( - ref: ref, - transactionId: transactionId, - copyFor: 'Merchant', - ), - ); - - await Printing.layoutPdf(onLayout: (_) async => bytes); - } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Print failed: $e'))); - } -} - -Future _fetchReceiptPdfBytes({ - required WidgetRef ref, - required String transactionId, - required String copyFor, -}) async { - final sessionUser = ref.read(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - return ref - .read(receiptRepositoryProvider) - .getTransactionReceiptPdfBytes( - token: sessionUser.token, - transactionId: transactionId, - copyFor: copyFor, - ); -} - -Future _runWithProgress( - BuildContext context, { - required String message, - required Future Function() task, -}) async { - if (!context.mounted) { - return task(); - } - - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return PopScope( - canPop: false, - child: AlertDialog( - content: Row( - children: [ - const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 12), - Expanded(child: Text(message)), - ], - ), - ), - ); - }, - ), - ); - - try { - return await task(); - } finally { - if (context.mounted) { - Navigator.of(context, rootNavigator: true).pop(); - } - } -} +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/terminal/receipt_view_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_pdfview/flutter_pdfview.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:printing/printing.dart'; + +class TransactionReceiptScreen extends ConsumerWidget { + const TransactionReceiptScreen({ + required this.merchantName, + required this.terminal, + required this.transaction, + super.key, + }); + + final String merchantName; + final Terminal terminal; + final TransactionRecord transaction; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final transactionId = transaction.id?.trim(); + if (transactionId == null || transactionId.isEmpty) { + return Scaffold( + appBar: AppBar(title: const Text('Receipt')), + body: const SafeArea( + child: Center(child: Text('Missing transaction id')), + ), + ); + } + + final pdfAsync = ref.watch( + transactionReceiptViewDataProvider( + TransactionReceiptQuery( + transactionId: transactionId, + copyFor: 'Merchant', + ), + ), + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Receipt'), + actions: [ + IconButton( + tooltip: 'Reload', + onPressed: () => ref.invalidate( + transactionReceiptViewDataProvider( + TransactionReceiptQuery( + transactionId: transactionId, + copyFor: 'Merchant', + ), + ), + ), + icon: const Icon(Icons.refresh), + ), + ], + ), + body: SafeArea( + bottom: true, + child: pdfAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: EdgeInsets.fromLTRB( + 24, + 24, + 24, + MediaQuery.viewPaddingOf(context).bottom + 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.picture_as_pdf_outlined, size: 56), + const SizedBox(height: 12), + Text( + 'Failed to load receipt PDF', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Text( + Error.safeToString(error), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 14), + FilledButton.tonal( + onPressed: () => ref.invalidate( + transactionReceiptViewDataProvider( + TransactionReceiptQuery( + transactionId: transactionId, + copyFor: 'Merchant', + ), + ), + ), + child: const Text('Try again'), + ), + ], + ), + ), + ), + data: (viewData) => Column( + children: [ + Expanded( + child: switch (viewData) { + ReceiptPdfViewData() => _ReceiptViewport( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: PDFView( + filePath: viewData.file.path, + enableSwipe: true, + swipeHorizontal: false, + autoSpacing: false, + pageFling: true, + pageSnap: true, + fitEachPage: true, + fitPolicy: FitPolicy.WIDTH, + backgroundColor: Theme.of(context).colorScheme.primary, + onError: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'PDF error: ${Error.safeToString(error)}', + ), + ), + ); + }, + onPageError: (page, error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'PDF page $page error: ${Error.safeToString(error)}', + ), + ), + ); + }, + ), + ), + }, + ), + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: () => _downloadReceipt( + context: context, + ref: ref, + transactionId: transactionId, + ), + icon: const Icon(Icons.download_outlined), + label: const Text('Download PDF'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.tonal( + onPressed: () => _printReceipt( + context: context, + ref: ref, + transactionId: transactionId, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.print_outlined), + SizedBox(width: 8), + Text('Print PDF'), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ReceiptViewport extends StatelessWidget { + const _ReceiptViewport({required this.child, this.padding}); + + final Widget child; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final viewPadding = MediaQuery.viewPaddingOf(context); + final size = MediaQuery.sizeOf(context); + final maxHeight = size.height * 0.60; + + return Padding( + padding: + padding ?? EdgeInsets.fromLTRB(16, 16, 16, viewPadding.bottom + 16), + child: LayoutBuilder( + builder: (context, constraints) { + final double targetHeight = maxHeight + .clamp(0, constraints.maxHeight) + .toDouble(); + + return Align( + alignment: Alignment.center, + child: SizedBox( + width: 230, + height: targetHeight, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.7), + border: Border.all(color: colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(16), + ), + child: SizedBox.expand(child: child), + ), + ), + ), + ); + }, + ), + ); + } +} + +Future _downloadReceipt({ + required BuildContext context, + required WidgetRef ref, + required String transactionId, +}) async { + try { + final sanitizedId = transactionId.replaceAll( + RegExp(r'[^A-Za-z0-9_-]'), + '_', + ); + final stamp = DateTime.now().toIso8601String().replaceAll(':', '-'); + + final Uint8List bytes = await _runWithProgress( + context, + message: 'Preparing receipt…', + task: () => _fetchReceiptPdfBytes( + ref: ref, + transactionId: transactionId, + copyFor: 'Merchant', + ), + ); + + final candidateBaseDirs = []; + if (!kIsWeb) { + if (Platform.isAndroid) { + final dir = await getExternalStorageDirectory(); + if (dir != null) { + candidateBaseDirs.add(dir); + } + } + final downloads = await getDownloadsDirectory(); + if (downloads != null) { + candidateBaseDirs.add(downloads); + } + } + candidateBaseDirs.add(await getApplicationDocumentsDirectory()); + + File? outFile; + Object? lastError; + for (final baseDir in candidateBaseDirs) { + try { + final receiptsDir = Directory( + '${baseDir.path}${Platform.pathSeparator}receipts', + ); + if (!await receiptsDir.exists()) { + await receiptsDir.create(recursive: true); + } + + final candidate = File( + '${receiptsDir.path}${Platform.pathSeparator}receipt_${sanitizedId}_merchant_$stamp.pdf', + ); + await candidate.writeAsBytes(bytes, flush: true); + outFile = candidate; + break; + } catch (e) { + lastError = e; + } + } + + if (outFile == null) { + throw lastError ?? Exception('Failed to save receipt PDF'); + } + + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Saved: ${outFile.path}'))); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Download failed: $e'))); + } + } +} + +Future _printReceipt({ + required BuildContext context, + required WidgetRef ref, + required String transactionId, +}) async { + try { + final Uint8List bytes = await _runWithProgress( + context, + message: 'Preparing print…', + task: () => _fetchReceiptPdfBytes( + ref: ref, + transactionId: transactionId, + copyFor: 'Merchant', + ), + ); + + await Printing.layoutPdf(onLayout: (_) async => bytes); + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Print failed: $e'))); + } +} + +Future _fetchReceiptPdfBytes({ + required WidgetRef ref, + required String transactionId, + required String copyFor, +}) async { + final sessionUser = ref.read(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + return ref + .read(receiptRepositoryProvider) + .getTransactionReceiptPdfBytes( + token: sessionUser.token, + transactionId: transactionId, + copyFor: copyFor, + ); +} + +Future _runWithProgress( + BuildContext context, { + required String message, + required Future Function() task, +}) async { + if (!context.mounted) { + return task(); + } + + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return PopScope( + canPop: false, + child: AlertDialog( + content: Row( + children: [ + const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + ), + ); + }, + ), + ); + + try { + return await task(); + } finally { + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } +} diff --git a/lib/presentation/terminal/transaction_view_model.dart b/lib/presentation/terminal/transaction_view_model.dart index 0b0bdb5..daad999 100644 --- a/lib/presentation/terminal/transaction_view_model.dart +++ b/lib/presentation/terminal/transaction_view_model.dart @@ -1,51 +1,51 @@ -import 'package:e_receipt_mobile/core/config/app_config.dart'; -import 'package:e_receipt_mobile/data/repositories/api_transaction_repository.dart'; -import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; -import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; -import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; -import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final transactionRepositoryProvider = Provider((ref) { - return ApiTransactionRepository( - baseUrl: AppConfig.apiBaseUrl, - apiSecret: AppConfig.apiSecret, - client: ref.watch(authenticatedHttpClientProvider), - ); -}); - -class TransactionQuery { - const TransactionQuery({required this.serial, required this.range}); - - final String serial; - final String range; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is TransactionQuery && - other.serial == serial && - other.range == range; - } - - @override - int get hashCode => Object.hash(serial, range); -} - -final transactionListProvider = - FutureProvider.family, TransactionQuery>( - (ref, query) async { - final sessionUser = ref.watch(sessionControllerProvider); - if (sessionUser == null) { - throw Exception('No active session'); - } - - return ref.watch(transactionRepositoryProvider).getTransactions( - token: sessionUser.token, - serial: query.serial, - range: query.range, - ); - }, - ); +import 'package:e_receipt_mobile/core/config/app_config.dart'; +import 'package:e_receipt_mobile/data/repositories/api_transaction_repository.dart'; +import 'package:e_receipt_mobile/domain/entities/transaction_record.dart'; +import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart'; +import 'package:e_receipt_mobile/presentation/auth/session_controller.dart'; +import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final transactionRepositoryProvider = Provider((ref) { + return ApiTransactionRepository( + baseUrl: AppConfig.apiBaseUrl, + apiSecret: AppConfig.apiSecret, + client: ref.watch(authenticatedHttpClientProvider), + ); +}); + +class TransactionQuery { + const TransactionQuery({required this.serial, required this.range}); + + final String serial; + final String range; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is TransactionQuery && + other.serial == serial && + other.range == range; + } + + @override + int get hashCode => Object.hash(serial, range); +} + +final transactionListProvider = + FutureProvider.family, TransactionQuery>( + (ref, query) async { + final sessionUser = ref.watch(sessionControllerProvider); + if (sessionUser == null) { + throw Exception('No active session'); + } + + return ref.watch(transactionRepositoryProvider).getTransactions( + token: sessionUser.token, + serial: query.serial, + range: query.range, + ); + }, + ); diff --git a/lib/presentation/terminal/widgets/terminal_header.dart b/lib/presentation/terminal/widgets/terminal_header.dart index 026a4d5..fdfb0f2 100644 --- a/lib/presentation/terminal/widgets/terminal_header.dart +++ b/lib/presentation/terminal/widgets/terminal_header.dart @@ -1,58 +1,58 @@ -import 'package:flutter/material.dart'; - -class TerminalHeader extends StatelessWidget { - const TerminalHeader({ - required this.merchantName, - required this.totalCount, - super.key, - }); - - final String merchantName; - final int totalCount; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Terminals', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 2), - Text( - merchantName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - const SizedBox(width: 12), - Chip( - label: Text('$totalCount'), - side: BorderSide(color: colorScheme.outlineVariant), - backgroundColor: colorScheme.surfaceContainerHigh, - ), - ], - ), - ], - ), - ); - } -} +import 'package:flutter/material.dart'; + +class TerminalHeader extends StatelessWidget { + const TerminalHeader({ + required this.merchantName, + required this.totalCount, + super.key, + }); + + final String merchantName; + final int totalCount; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Terminals', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + merchantName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Chip( + label: Text('$totalCount'), + side: BorderSide(color: colorScheme.outlineVariant), + backgroundColor: colorScheme.surfaceContainerHigh, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/terminal/widgets/terminal_list_view.dart b/lib/presentation/terminal/widgets/terminal_list_view.dart index b74fd55..19edb66 100644 --- a/lib/presentation/terminal/widgets/terminal_list_view.dart +++ b/lib/presentation/terminal/widgets/terminal_list_view.dart @@ -1,208 +1,208 @@ -import 'dart:convert'; -import 'dart:math'; -import 'dart:ui'; - -import 'package:e_receipt_mobile/domain/entities/terminal.dart'; -import 'package:flutter/material.dart'; - -class TerminalListView extends StatelessWidget { - const TerminalListView({ - required this.terminals, - required this.onRefresh, - required this.onTerminalTap, - required this.emptyMessage, - super.key, - }); - - final List terminals; - final Future Function() onRefresh; - final void Function(Terminal terminal) onTerminalTap; - final String emptyMessage; - - @override - Widget build(BuildContext context) { - final bottomInset = MediaQuery.viewPaddingOf(context).bottom; - final colorScheme = Theme.of(context).colorScheme; - - return RefreshIndicator.adaptive( - onRefresh: onRefresh, - child: terminals.isEmpty - ? ListView( - padding: EdgeInsets.fromLTRB(16, 96, 16, bottomInset + 24), - physics: const AlwaysScrollableScrollPhysics(), - children: [ - Icon( - Icons.point_of_sale_outlined, - size: 56, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 12), - Text( - emptyMessage, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 6), - Text( - 'Pull down to refresh.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ) - : ListView.separated( - padding: EdgeInsets.fromLTRB(16, 8, 16, bottomInset + 16), - physics: const AlwaysScrollableScrollPhysics(), - itemCount: terminals.length, - separatorBuilder: (_, __) => const SizedBox(height: 10), - itemBuilder: (context, index) { - final terminal = terminals[index]; - print('this is terminal ${terminal.status}'); - final enabled = terminal.tid != null || terminal.id != null; - final title = - (_sanitizeMultiline(terminal.name)?.isNotEmpty ?? false) - ? _sanitizeMultiline(terminal.name)! - : (terminal.tid?.trim().isNotEmpty ?? false) - ? terminal.tid!.trim() - : '-'; - final status = (terminal.status ?? '').trim(); - final serial = (_sanitizeMultiline(terminal.serial) ?? '') - .trim(); - - return Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide( - color: colorScheme.outlineVariant.withOpacity(0.55), - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(18), - onTap: enabled ? () => onTerminalTap(terminal) : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - child: Row( - children: [ - Container( - height: 44, - width: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: enabled - ? colorScheme.primary.withOpacity(0.14) - : colorScheme.surfaceContainerHigh, - ), - child: Icon( - Icons.point_of_sale_outlined, - color: enabled - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - ), - if (status.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 999, - ), - color: - colorScheme.surfaceContainerHigh, - border: Border.all( - color: colorScheme.outlineVariant, - ), - ), - child: Text( - status.toUpperCase(), - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith( - fontWeight: FontWeight.w700, - letterSpacing: 0.2, - ), - ), - ), - ], - ), - if (serial.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - 'Serial: $serial', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: colorScheme.onSurfaceVariant, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ], - ], - ), - ), - const SizedBox(width: 8), - Icon( - Icons.chevron_right, - color: enabled - ? colorScheme.onSurfaceVariant - : colorScheme.onSurfaceVariant.withOpacity(0.4), - ), - ], - ), - ), - ), - ); - }, - ), - ); - } -} - -String? _sanitizeMultiline(String? value) { - if (value == null) { - return null; - } - final cleaned = value - .replaceAll('\r', ' ') - .replaceAll('\n', ' ') - .replaceAll(r'\n', ' ') - .replaceAll(r'/n', ' ') - .replaceAll(RegExp(r'\s+'), ' ') - .trim(); - return cleaned.isEmpty ? null : cleaned; -} +import 'dart:convert'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:e_receipt_mobile/domain/entities/terminal.dart'; +import 'package:flutter/material.dart'; + +class TerminalListView extends StatelessWidget { + const TerminalListView({ + required this.terminals, + required this.onRefresh, + required this.onTerminalTap, + required this.emptyMessage, + super.key, + }); + + final List terminals; + final Future Function() onRefresh; + final void Function(Terminal terminal) onTerminalTap; + final String emptyMessage; + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + final colorScheme = Theme.of(context).colorScheme; + + return RefreshIndicator.adaptive( + onRefresh: onRefresh, + child: terminals.isEmpty + ? ListView( + padding: EdgeInsets.fromLTRB(16, 96, 16, bottomInset + 24), + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Icon( + Icons.point_of_sale_outlined, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 12), + Text( + emptyMessage, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 6), + Text( + 'Pull down to refresh.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ) + : ListView.separated( + padding: EdgeInsets.fromLTRB(16, 8, 16, bottomInset + 16), + physics: const AlwaysScrollableScrollPhysics(), + itemCount: terminals.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final terminal = terminals[index]; + print('this is terminal ${terminal.status}'); + final enabled = terminal.tid != null || terminal.id != null; + final title = + (_sanitizeMultiline(terminal.name)?.isNotEmpty ?? false) + ? _sanitizeMultiline(terminal.name)! + : (terminal.tid?.trim().isNotEmpty ?? false) + ? terminal.tid!.trim() + : '-'; + final status = (terminal.status ?? '').trim(); + final serial = (_sanitizeMultiline(terminal.serial) ?? '') + .trim(); + + return Card( + elevation: 0, + color: colorScheme.surfaceContainerLow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + color: colorScheme.outlineVariant.withOpacity(0.55), + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: enabled ? () => onTerminalTap(terminal) : null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + child: Row( + children: [ + Container( + height: 44, + width: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: enabled + ? colorScheme.primary.withOpacity(0.14) + : colorScheme.surfaceContainerHigh, + ), + child: Icon( + Icons.point_of_sale_outlined, + color: enabled + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + if (status.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 999, + ), + color: + colorScheme.surfaceContainerHigh, + border: Border.all( + color: colorScheme.outlineVariant, + ), + ), + child: Text( + status.toUpperCase(), + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + ), + ), + ), + ], + ), + if (serial.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + 'Serial: $serial', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: colorScheme.onSurfaceVariant, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ], + ], + ), + ), + const SizedBox(width: 8), + Icon( + Icons.chevron_right, + color: enabled + ? colorScheme.onSurfaceVariant + : colorScheme.onSurfaceVariant.withOpacity(0.4), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} + +String? _sanitizeMultiline(String? value) { + if (value == null) { + return null; + } + final cleaned = value + .replaceAll('\r', ' ') + .replaceAll('\n', ' ') + .replaceAll(r'\n', ' ') + .replaceAll(r'/n', ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + return cleaned.isEmpty ? null : cleaned; +} diff --git a/pubspec.lock b/pubspec.lock index da875ed..aad91f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,570 +1,570 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" - source: hosted - version: "4.0.9" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - barcode: - dependency: transitive - description: - name: barcode - sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" - url: "https://pub.dev" - source: hosted - version: "2.2.9" - bidi: - dependency: transitive - description: - name: bidi - sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" - url: "https://pub.dev" - source: hosted - version: "2.0.13" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_pdfview: - dependency: "direct main" - description: - name: flutter_pdfview - sha256: "5b80e89f3ba6e478d1e897543c9634508284ad73476807febc188378986b69ee" - url: "https://pub.dev" - source: hosted - version: "1.4.4" - flutter_riverpod: - dependency: "direct main" - description: - name: flutter_riverpod - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" - url: "https://pub.dev" - source: hosted - version: "2.6.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - hooks: - dependency: transitive - description: - name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" - source: hosted - version: "4.8.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" - url: "https://pub.dev" - source: hosted - version: "2.2.23" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - pdf: - dependency: transitive - description: - name: pdf - sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b - url: "https://pub.dev" - source: hosted - version: "3.12.0" - pdf_widget_wrapper: - dependency: transitive - description: - name: pdf_widget_wrapper - sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 - url: "https://pub.dev" - source: hosted - version: "1.0.4" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - printing: - dependency: "direct main" - description: - name: printing - sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" - url: "https://pub.dev" - source: hosted - version: "5.14.3" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" - url: "https://pub.dev" - source: hosted - version: "2.6.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - state_notifier: - dependency: transitive - description: - name: state_notifier - sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" - source: hosted - version: "1.0.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 - url: "https://pub.dev" - source: hosted - version: "4.13.1" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "0f7fcd2c86bf36bdcf94881f7941ce0cbc4f8d104b9fdcd5fcbef90e2199db76" - url: "https://pub.dev" - source: hosted - version: "4.10.15" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" - source: hosted - version: "2.15.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83 - url: "https://pub.dev" - source: hosted - version: "3.24.2" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_pdfview: + dependency: "direct main" + description: + name: flutter_pdfview + sha256: "5b80e89f3ba6e478d1e897543c9634508284ad73476807febc188378986b69ee" + url: "https://pub.dev" + source: hosted + version: "1.4.4" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pdf: + dependency: transitive + description: + name: pdf + sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b + url: "https://pub.dev" + source: hosted + version: "3.12.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + printing: + dependency: "direct main" + description: + name: printing + sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" + url: "https://pub.dev" + source: hosted + version: "5.14.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "0f7fcd2c86bf36bdcf94881f7941ce0cbc4f8d104b9fdcd5fcbef90e2199db76" + url: "https://pub.dev" + source: hosted + version: "4.10.15" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83 + url: "https://pub.dev" + source: hosted + version: "3.24.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index c7cdf68..f4856ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,95 +1,95 @@ -name: e_receipt_mobile -description: "E-Receipt portal moble version" -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.10.7 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - flutter_riverpod: ^2.6.1 - http: ^1.2.2 - flutter_pdfview: ^1.4.0 - path_provider: ^2.1.5 - webview_flutter: ^4.0.0 - printing: ^5.12.0 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package +name: e_receipt_mobile +description: "E-Receipt portal moble version" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is hused as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 2.0.0+1 + +environment: + sdk: ^3.10.7 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + flutter_riverpod: ^2.6.1 + http: ^1.2.2 + flutter_pdfview: ^1.4.0 + path_provider: ^2.1.5 + webview_flutter: ^4.0.0 + printing: ^5.12.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/widget_test.dart b/test/widget_test.dart index fb6f436..06a1ac5 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:e_receipt_mobile/main.dart'; - -void main() { - testWidgets('Login page smoke test', (WidgetTester tester) async { - await tester.pumpWidget(const ProviderScope(child: EReceiptApp())); - - expect(find.text('Login'), findsOneWidget); - expect(find.text('Sign In'), findsOneWidget); - expect(find.text('Username'), findsOneWidget); - expect(find.text('Password'), findsOneWidget); - }); -} +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:e_receipt_mobile/main.dart'; + +void main() { + testWidgets('Login page smoke test', (WidgetTester tester) async { + await tester.pumpWidget(const ProviderScope(child: EReceiptApp())); + + expect(find.text('Login'), findsOneWidget); + expect(find.text('Sign In'), findsOneWidget); + expect(find.text('Username'), findsOneWidget); + expect(find.text('Password'), findsOneWidget); + }); +}