commit f9923d9d26dca78e50c827066170c1d981bc4b03 Author: kyawkhantwin Date: Thu Apr 9 13:17:03 2026 +0630 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f0d006 --- /dev/null +++ b/.gitignore @@ -0,0 +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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..e4b4e8b --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# 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: "2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: android + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: ios + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: linux + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: macos + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: web + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: windows + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + + # 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 new file mode 100644 index 0000000..af34ed4 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# cb_prestige_qr +A new Flutter project. +## 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: + + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +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. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..b5fe809 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,32 @@ +# 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 + +analyzer: + plugins: + - custom_lint + +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 new file mode 100644 index 0000000..c908258 --- /dev/null +++ b/android/.gitignore @@ -0,0 +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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..5c12ea4 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +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.cb_prestige_qr" + 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.cb_prestige_qr" + // 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 new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..150f200 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt b/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt new file mode 100644 index 0000000..cca031c --- /dev/null +++ b/android/app/src/main/kotlin/com/example/cb_prestige_qr/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.cb_prestige_qr + +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 new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +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 new file mode 100644 index 0000000..2b02b7b Binary files /dev/null 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 new file mode 100644 index 0000000..471355b Binary files /dev/null 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 new file mode 100644 index 0000000..6849b9e Binary files /dev/null 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 new file mode 100644 index 0000000..2fec2ae Binary files /dev/null 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 new file mode 100644 index 0000000..036fcb9 Binary files /dev/null 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 new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..fc42c61 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..1f88145 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +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) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..21dbfa5 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..db3f453 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +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 diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..4dcef4b --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +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") diff --git a/assets/images/caro_1.jpg b/assets/images/caro_1.jpg new file mode 100644 index 0000000..0998b8d Binary files /dev/null and b/assets/images/caro_1.jpg differ diff --git a/assets/images/caro_2.jpg b/assets/images/caro_2.jpg new file mode 100644 index 0000000..b5014fd Binary files /dev/null and b/assets/images/caro_2.jpg differ diff --git a/assets/images/caro_3.jpg b/assets/images/caro_3.jpg new file mode 100644 index 0000000..ddd61cb Binary files /dev/null and b/assets/images/caro_3.jpg differ diff --git a/assets/images/cb_logo_white.png b/assets/images/cb_logo_white.png new file mode 100644 index 0000000..c7a7548 Binary files /dev/null and b/assets/images/cb_logo_white.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..4d57c32 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/logo_white.png b/assets/images/logo_white.png new file mode 100644 index 0000000..61f5aee Binary files /dev/null and b/assets/images/logo_white.png differ diff --git a/assets/images/splash.png b/assets/images/splash.png new file mode 100644 index 0000000..3ec4c50 Binary files /dev/null and b/assets/images/splash.png differ diff --git a/assets/images/splash_1.png b/assets/images/splash_1.png new file mode 100644 index 0000000..93bbb6c Binary files /dev/null and b/assets/images/splash_1.png differ diff --git a/assets/mp3/splash.mp4 b/assets/mp3/splash.mp4 new file mode 100644 index 0000000..281e090 Binary files /dev/null and b/assets/mp3/splash.mp4 differ diff --git a/assets/svg/coin_receive.svg b/assets/svg/coin_receive.svg new file mode 100644 index 0000000..f6c75fb --- /dev/null +++ b/assets/svg/coin_receive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/coin_send.svg b/assets/svg/coin_send.svg new file mode 100644 index 0000000..ca2aa63 --- /dev/null +++ b/assets/svg/coin_send.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..ad322bc --- /dev/null +++ b/ios/.gitignore @@ -0,0 +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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..256cf28 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..0b2d479 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..0b2d479 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8d94f2d --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,620 @@ +// !$*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 */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.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 */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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.cbPrestigeQr; + 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.cbPrestigeQr.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.cbPrestigeQr.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.cbPrestigeQr.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.cbPrestigeQr; + 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.cbPrestigeQr; + 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 new file mode 100644 index 0000000..c4b79bd --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..bbabc4e --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..59c6d39 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..ed1c097 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1950fd8 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +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" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..d08a4de --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +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" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..65a94b5 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# 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 new file mode 100644 index 0000000..497371e --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..bbb83ca --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..63e80de --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Cb Prestige Qr + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + cb_prestige_qr + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..fae207f --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b79be9b --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..4d206de --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +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. + } + +} diff --git a/lib/core/presentation/widgets/global_loading_overlay.dart b/lib/core/presentation/widgets/global_loading_overlay.dart new file mode 100644 index 0000000..e4f1385 --- /dev/null +++ b/lib/core/presentation/widgets/global_loading_overlay.dart @@ -0,0 +1,127 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; + +class GlobalLoadingOverlay extends StatelessWidget { + const GlobalLoadingOverlay({ + super.key, + required this.isLoading, + }); + + final bool isLoading; + + @override + Widget build(BuildContext context) { + if (!isLoading) return const SizedBox.shrink(); + + return SizedBox.expand( + child: AbsorbPointer( + absorbing: true, + child: Stack( + children: [ + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: const SizedBox.expand(), + ), + ColoredBox( + color: Colors.black.withOpacity(0.55), + ), + const Center( + child: _AnimatedLoader(), + ), + ], + ), + ), + ); + } +} + +class _AnimatedLoader extends StatefulWidget { + const _AnimatedLoader(); + + @override + State<_AnimatedLoader> createState() => _AnimatedLoaderState(); +} + +class _AnimatedLoaderState extends State<_AnimatedLoader> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _mainOpacity; + late final Animation _glowOpacity; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2200), + )..repeat(reverse: true); + + /// Main fade (smooth breathing) + _mainOpacity = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0), + weight: 50, + ), + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 0.0), + weight: 50, + ), + ]).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + /// Glow fade (slightly offset feel) + _glowOpacity = Tween( + begin: 0.2, + end: 0.5, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return Opacity( + opacity: _mainOpacity.value, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(_glowOpacity.value), + blurRadius: 50, + spreadRadius: 12, + ), + ], + ), + child: Image.asset( + 'assets/images/logo_white.png', + width: 100, + height: 100, + colorBlendMode: BlendMode.srcIn, + ), + ), + ); + }, + ); + } +} diff --git a/lib/core/presentation/widgets/start_loading_overlay.dart b/lib/core/presentation/widgets/start_loading_overlay.dart new file mode 100644 index 0000000..ed22a36 --- /dev/null +++ b/lib/core/presentation/widgets/start_loading_overlay.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart'; +import 'package:flutter/material.dart'; + +class StartLoadingOverlay extends StatefulWidget { + const StartLoadingOverlay({ + super.key, + required this.child, + this.enabled = true, + this.duration = const Duration(seconds: 2), + }); + + final Widget child; + final bool enabled; + final Duration duration; + + @override + State createState() => _StartLoadingOverlayState(); +} + +class _StartLoadingOverlayState extends State { + Timer? _timer; + var _isLoading = false; + + @override + void initState() { + super.initState(); + if (!widget.enabled) return; + + _isLoading = true; + _timer = Timer(widget.duration, () { + if (!mounted) return; + setState(() => _isLoading = false); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + GlobalLoadingOverlay(isLoading: _isLoading), + ], + ); + } +} diff --git a/lib/core/utils/MainShell.dart b/lib/core/utils/MainShell.dart new file mode 100644 index 0000000..8927e6a --- /dev/null +++ b/lib/core/utils/MainShell.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:cb_prestige_qr/core/utils/ScanShell.dart'; +import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart'; +import 'package:cb_prestige_qr/core/widgets/CenterNavButton.dart'; +import 'package:cb_prestige_qr/core/widgets/NavItem.dart'; +import 'package:cb_prestige_qr/features/analysis/presentation/pages/analysis_page.dart'; +import 'package:cb_prestige_qr/features/history/presentation/pages/history_page.dart'; +import 'package:cb_prestige_qr/features/home/presentation/pages/home.dart'; +import 'package:cb_prestige_qr/features/settings/presentation/pages/settings_page.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final navIndexNotifierProvider = NotifierProvider( + NavIndexNotifier.new, +); + +class NavIndexNotifier extends Notifier { + @override + int build() => 0; + + void setIndex(int index) { + if (state == index) return; + state = index; + } +} + +class MainShell extends ConsumerStatefulWidget { + const MainShell({super.key}); + + @override + ConsumerState createState() => _MainShellState(); +} + +class _MainShellState extends ConsumerState { + Timer? _timer; + var _isLoading = false; + + void _startLoading([Duration duration = const Duration(seconds: 1)]) { + _timer?.cancel(); + if (!_isLoading) setState(() => _isLoading = true); + _timer = Timer(duration, () { + if (!mounted) return; + setState(() => _isLoading = false); + }); + } + + @override + void initState() { + super.initState(); + final initialIndex = ref.read(navIndexNotifierProvider); + if (initialIndex != 0) _startLoading(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final selectedIndex = ref.watch(navIndexNotifierProvider); + final theme = Theme.of(context); + + ref.listen(navIndexNotifierProvider, (previous, next) { + if (previous == null || previous == next) return; + if (next == 0) return; + _startLoading(); + }); + + final pages = [ + const HomeView(), + const AnalysisPage(), + const SizedBox(), + const HistoryPage(), + const SettingsPage(), + ]; + + return Stack( + children: [ + Scaffold( + extendBody: true, + body: IndexedStack(index: selectedIndex, children: pages), + bottomNavigationBar: Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + height: 70 + MediaQuery.of(context).padding.bottom, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withAlpha(31)), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(31), + blurRadius: 25, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + ), + child: Row( + children: [ + Expanded( + child: NavItem( + icon: Icons.home_rounded, + isActive: selectedIndex == 0, + onTap: () => ref + .read(navIndexNotifierProvider.notifier) + .setIndex(0), + ), + ), + Expanded( + child: NavItem( + icon: Icons.analytics, + isActive: selectedIndex == 1, + onTap: () => ref + .read(navIndexNotifierProvider.notifier) + .setIndex(1), + ), + ), + Expanded( + child: CenterNavButton( + isActive: false, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const ScanFlow(), + ), + ); + }, + ), + ), + Expanded( + child: NavItem( + icon: Icons.history_rounded, + isActive: selectedIndex == 3, + onTap: () => ref + .read(navIndexNotifierProvider.notifier) + .setIndex(3), + ), + ), + Expanded( + child: NavItem( + icon: Icons.person_rounded, + isActive: selectedIndex == 4, + onTap: () => ref + .read(navIndexNotifierProvider.notifier) + .setIndex(4), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + GlobalLoadingOverlay(isLoading: _isLoading), + ], + ); + } +} diff --git a/lib/core/utils/ScanShell.dart b/lib/core/utils/ScanShell.dart new file mode 100644 index 0000000..05f9a97 --- /dev/null +++ b/lib/core/utils/ScanShell.dart @@ -0,0 +1,11 @@ +import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_page.dart'; +import 'package:flutter/material.dart'; + +class ScanFlow extends StatelessWidget { + const ScanFlow({super.key}); + + @override + Widget build(BuildContext context) { + return const ScanPage(); + } +} diff --git a/lib/core/widgets/CenterNavButton.dart b/lib/core/widgets/CenterNavButton.dart new file mode 100644 index 0000000..7ccdd47 --- /dev/null +++ b/lib/core/widgets/CenterNavButton.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class CenterNavButton extends StatelessWidget { + final bool isActive; + final VoidCallback onTap; + + const CenterNavButton({ + super.key, + required this.isActive, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + splashColor: theme.colorScheme.primary.withAlpha(26), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 10), + decoration: BoxDecoration( + color: isActive + ? theme.colorScheme.primary.withAlpha(31) + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: isActive ? 1.15 : 1.0, + child: Container( + height: 48, + width: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + theme.colorScheme.primary, + theme.colorScheme.primary.withAlpha(179), + ], + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withAlpha(102), + blurRadius: 16, + offset: const Offset(0, 6), + ), + ], + ), + child: const Icon( + Icons.qr_code_scanner, + color: Colors.white, + size: 24, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/NavItem.dart b/lib/core/widgets/NavItem.dart new file mode 100644 index 0000000..11378ca --- /dev/null +++ b/lib/core/widgets/NavItem.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class NavItem extends StatelessWidget { + final IconData icon; + final bool isActive; + final VoidCallback onTap; + + const NavItem({ + super.key, + required this.icon, + required this.isActive, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + splashColor: theme.colorScheme.primary.withAlpha(26), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 10), + decoration: BoxDecoration( + color: isActive + ? theme.colorScheme.primary.withAlpha(31) + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: isActive ? 1.2 : 1.0, + child: Icon( + icon, + size: 26, + color: isActive + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/analysis/data/data_sources/analysis_local_data_source.dart b/lib/features/analysis/data/data_sources/analysis_local_data_source.dart new file mode 100644 index 0000000..8b5a6f9 --- /dev/null +++ b/lib/features/analysis/data/data_sources/analysis_local_data_source.dart @@ -0,0 +1,64 @@ +import 'dart:math'; + +import '../../domain/entities/analysis_content.dart'; +import '../../domain/entities/analysis_series_point.dart'; +import '../../domain/entities/analysis_summary.dart'; + +abstract class AnalysisLocalDataSource { + AnalysisContent getContent({required int rangeDays, DateTime? now}); +} + +class AnalysisLocalDataSourceImpl implements AnalysisLocalDataSource { + const AnalysisLocalDataSourceImpl(); + + @override + AnalysisContent getContent({required int rangeDays, DateTime? now}) { + final effectiveNow = now ?? DateTime.now(); + final normalizedNow = DateTime( + effectiveNow.year, + effectiveNow.month, + effectiveNow.day, + ); + + final clampedDays = rangeDays.clamp(1, 365); + final startDate = normalizedNow.subtract(Duration(days: clampedDays - 1)); + + final rand = Random(clampedDays * 7919 + normalizedNow.day); + final series = []; + var totalScans = 0; + var totalPointsUsed = 0; + + for (var i = 0; i < clampedDays; i++) { + final date = startDate.add(Duration(days: i)); + final base = 30 + (15 * sin((i / max(1, clampedDays - 1)) * pi)).round(); + final noise = rand.nextInt(14) - 7; + final scans = max(0, base + noise); + final pointsUsed = max(0, (scans * (2 + rand.nextInt(3))) ~/ 3); + + totalScans += scans; + totalPointsUsed += pointsUsed; + series.add( + AnalysisSeriesPoint( + date: date, + scanCount: scans, + pointsUsed: pointsUsed, + ), + ); + } + + final users = 1240 + rand.nextInt(100); + final activeUsers = (users * (0.22 + rand.nextDouble() * 0.08)).round(); + final successRate = 0.965 + rand.nextDouble() * 0.03; + + return AnalysisContent( + summary: AnalysisSummary( + users: users, + activeUsers: activeUsers, + scans: totalScans, + successRate: successRate, + pointsUsed: totalPointsUsed, + ), + series: series, + ); + } +} diff --git a/lib/features/analysis/data/repositories/analysis_repository_impl.dart b/lib/features/analysis/data/repositories/analysis_repository_impl.dart new file mode 100644 index 0000000..a860feb --- /dev/null +++ b/lib/features/analysis/data/repositories/analysis_repository_impl.dart @@ -0,0 +1,14 @@ +import '../../domain/entities/analysis_content.dart'; +import '../../domain/repositories/analysis_repository.dart'; +import '../data_sources/analysis_local_data_source.dart'; + +class AnalysisRepositoryImpl implements AnalysisRepository { + const AnalysisRepositoryImpl(this._localDataSource); + + final AnalysisLocalDataSource _localDataSource; + + @override + Future getAnalysisContent({required int rangeDays}) async { + return _localDataSource.getContent(rangeDays: rangeDays); + } +} diff --git a/lib/features/analysis/domain/entities/analysis_content.dart b/lib/features/analysis/domain/entities/analysis_content.dart new file mode 100644 index 0000000..f4333ac --- /dev/null +++ b/lib/features/analysis/domain/entities/analysis_content.dart @@ -0,0 +1,9 @@ +import 'analysis_series_point.dart'; +import 'analysis_summary.dart'; + +class AnalysisContent { + const AnalysisContent({required this.summary, required this.series}); + + final AnalysisSummary summary; + final List series; +} diff --git a/lib/features/analysis/domain/entities/analysis_series_point.dart b/lib/features/analysis/domain/entities/analysis_series_point.dart new file mode 100644 index 0000000..2024cc4 --- /dev/null +++ b/lib/features/analysis/domain/entities/analysis_series_point.dart @@ -0,0 +1,11 @@ +class AnalysisSeriesPoint { + const AnalysisSeriesPoint({ + required this.date, + required this.scanCount, + required this.pointsUsed, + }); + + final DateTime date; + final int scanCount; + final int pointsUsed; +} diff --git a/lib/features/analysis/domain/entities/analysis_summary.dart b/lib/features/analysis/domain/entities/analysis_summary.dart new file mode 100644 index 0000000..40ad448 --- /dev/null +++ b/lib/features/analysis/domain/entities/analysis_summary.dart @@ -0,0 +1,23 @@ +class AnalysisSummary { + const AnalysisSummary({ + required this.users, + required this.activeUsers, + required this.scans, + required this.successRate, + required this.pointsUsed, + }); + + /// Number of users for the selected range (not all-time). + final int users; + + /// Active users for the selected range (not all-time). + final int activeUsers; + + /// Scans for the selected range (not all-time). + final int scans; + + final double successRate; + + /// Points used for the selected range (not all-time). + final int pointsUsed; +} diff --git a/lib/features/analysis/domain/repositories/analysis_repository.dart b/lib/features/analysis/domain/repositories/analysis_repository.dart new file mode 100644 index 0000000..58b3cdf --- /dev/null +++ b/lib/features/analysis/domain/repositories/analysis_repository.dart @@ -0,0 +1,5 @@ +import '../entities/analysis_content.dart'; + +abstract class AnalysisRepository { + Future getAnalysisContent({required int rangeDays}); +} diff --git a/lib/features/analysis/domain/use_cases/get_analysis_content.dart b/lib/features/analysis/domain/use_cases/get_analysis_content.dart new file mode 100644 index 0000000..ceb6928 --- /dev/null +++ b/lib/features/analysis/domain/use_cases/get_analysis_content.dart @@ -0,0 +1,12 @@ +import '../entities/analysis_content.dart'; +import '../repositories/analysis_repository.dart'; + +class GetAnalysisContent { + const GetAnalysisContent(this._repository); + + final AnalysisRepository _repository; + + Future call({required int rangeDays}) { + return _repository.getAnalysisContent(rangeDays: rangeDays); + } +} diff --git a/lib/features/analysis/presentation/manager/analysis_chart_metric.dart b/lib/features/analysis/presentation/manager/analysis_chart_metric.dart new file mode 100644 index 0000000..9d122e4 --- /dev/null +++ b/lib/features/analysis/presentation/manager/analysis_chart_metric.dart @@ -0,0 +1,8 @@ +enum AnalysisChartMetric { scans, pointsUsed } + +extension AnalysisChartMetricX on AnalysisChartMetric { + String get label => switch (this) { + AnalysisChartMetric.scans => 'Scans', + AnalysisChartMetric.pointsUsed => 'Points Used', + }; +} diff --git a/lib/features/analysis/presentation/manager/analysis_range.dart b/lib/features/analysis/presentation/manager/analysis_range.dart new file mode 100644 index 0000000..bf27f35 --- /dev/null +++ b/lib/features/analysis/presentation/manager/analysis_range.dart @@ -0,0 +1,15 @@ +enum AnalysisRangePreset { today, last7Days, last30Days } + +extension AnalysisRangePresetX on AnalysisRangePreset { + int get days => switch (this) { + AnalysisRangePreset.today => 1, + AnalysisRangePreset.last7Days => 7, + AnalysisRangePreset.last30Days => 30, + }; + + String get label => switch (this) { + AnalysisRangePreset.today => 'Today', + AnalysisRangePreset.last7Days => '7 days', + AnalysisRangePreset.last30Days => '30 days', + }; +} diff --git a/lib/features/analysis/presentation/manager/analysis_ui_state.dart b/lib/features/analysis/presentation/manager/analysis_ui_state.dart new file mode 100644 index 0000000..bed4e8e --- /dev/null +++ b/lib/features/analysis/presentation/manager/analysis_ui_state.dart @@ -0,0 +1,70 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class AnalysisSeriesPointUiModel { + const AnalysisSeriesPointUiModel({ + required this.label, + required this.scanCount, + required this.pointsUsed, + }); + + final String label; + final int scanCount; + final int pointsUsed; +} + +@immutable +class AnalysisSummaryUiModel { + const AnalysisSummaryUiModel({ + required this.users, + required this.activeUsers, + required this.scans, + required this.successRatePercent, + required this.pointsUsed, + }); + + static const empty = AnalysisSummaryUiModel( + users: 0, + activeUsers: 0, + scans: 0, + successRatePercent: 0, + pointsUsed: 0, + ); + + final int users; + final int activeUsers; + final int scans; + final int successRatePercent; + final int pointsUsed; +} + +@immutable +class AnalysisUiState { + AnalysisUiState({ + int? rangeDays, + String? rangeLabel, + AnalysisSummaryUiModel? summary, + List? series, + double? averageScansPerDay, + double? averagePointsPerDay, + double? pointsPerScan, + int? activeUserRatePercent, + }) : rangeDays = rangeDays ?? 7, + rangeLabel = rangeLabel ?? '7 days', + summary = summary ?? AnalysisSummaryUiModel.empty, + series = series ?? const [], + averageScansPerDay = averageScansPerDay ?? 0, + averagePointsPerDay = averagePointsPerDay ?? 0, + pointsPerScan = pointsPerScan ?? 0, + activeUserRatePercent = activeUserRatePercent ?? 0; + + final int? rangeDays; + final String? rangeLabel; + final AnalysisSummaryUiModel? summary; + final List? series; + + final double? averageScansPerDay; + final double? averagePointsPerDay; + final double? pointsPerScan; + final int? activeUserRatePercent; +} diff --git a/lib/features/analysis/presentation/manager/analysis_view_model.dart b/lib/features/analysis/presentation/manager/analysis_view_model.dart new file mode 100644 index 0000000..6035b39 --- /dev/null +++ b/lib/features/analysis/presentation/manager/analysis_view_model.dart @@ -0,0 +1,128 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../data/data_sources/analysis_local_data_source.dart'; +import '../../data/repositories/analysis_repository_impl.dart'; +import '../../domain/entities/analysis_content.dart'; +import '../../domain/repositories/analysis_repository.dart'; +import '../../domain/use_cases/get_analysis_content.dart'; +import 'analysis_chart_metric.dart'; +import 'analysis_range.dart'; +import 'analysis_ui_state.dart'; + +final analysisRangeNotifierProvider = + NotifierProvider( + AnalysisRangeNotifier.new, + ); + +final analysisChartMetricNotifierProvider = + NotifierProvider( + AnalysisChartMetricNotifier.new, + ); + +class AnalysisRangeNotifier extends Notifier { + @override + AnalysisRangePreset build() => AnalysisRangePreset.last7Days; + + void setRange(AnalysisRangePreset preset) => state = preset; +} + +class AnalysisChartMetricNotifier extends Notifier { + @override + AnalysisChartMetric build() => AnalysisChartMetric.scans; + + void setMetric(AnalysisChartMetric metric) => state = metric; +} + +final _analysisLocalDataSourceProvider = Provider( + (ref) => const AnalysisLocalDataSourceImpl(), +); + +final _analysisRepositoryProvider = Provider( + (ref) => AnalysisRepositoryImpl(ref.watch(_analysisLocalDataSourceProvider)), +); + +final _getAnalysisContentProvider = Provider( + (ref) => GetAnalysisContent(ref.watch(_analysisRepositoryProvider)), +); + +final analysisViewModelProvider = + AsyncNotifierProvider( + AnalysisViewModel.new, + ); + +class AnalysisViewModel extends AsyncNotifier { + @override + Future build() async { + final rangePreset = ref.watch(analysisRangeNotifierProvider); + final content = await ref.watch(_getAnalysisContentProvider)( + rangeDays: rangePreset.days, + ); + return _mapContentToUiState( + content: content, + rangeDays: rangePreset.days, + rangeLabel: rangePreset.label, + ); + } + + AnalysisUiState _mapContentToUiState({ + required AnalysisContent content, + required int rangeDays, + required String rangeLabel, + }) { + final scans = content.summary.scans; + final pointsUsed = content.summary.pointsUsed; + final users = content.summary.users; + final activeUsers = content.summary.activeUsers; + + final averageScansPerDay = scans / (rangeDays == 0 ? 1 : rangeDays); + final averagePointsPerDay = pointsUsed / (rangeDays == 0 ? 1 : rangeDays); + final double pointsPerScan = scans == 0 + ? 0 + : (pointsUsed.toDouble() / scans.toDouble()); + final activeUserRatePercent = users == 0 + ? 0 + : ((activeUsers / users) * 100).round(); + + return AnalysisUiState( + rangeDays: rangeDays, + rangeLabel: rangeLabel, + summary: AnalysisSummaryUiModel( + users: content.summary.users, + activeUsers: content.summary.activeUsers, + scans: content.summary.scans, + successRatePercent: (content.summary.successRate * 100).round(), + pointsUsed: content.summary.pointsUsed, + ), + series: content.series + .map( + (p) => AnalysisSeriesPointUiModel( + label: _labelForDate(p.date, rangeDays: rangeDays), + scanCount: p.scanCount, + pointsUsed: p.pointsUsed, + ), + ) + .toList(growable: false), + averageScansPerDay: averageScansPerDay, + averagePointsPerDay: averagePointsPerDay, + pointsPerScan: pointsPerScan, + activeUserRatePercent: activeUserRatePercent, + ); + } + + String _labelForDate(DateTime date, {required int rangeDays}) { + if (rangeDays <= 1) return 'Today'; + if (rangeDays <= 7) { + return switch (date.weekday) { + DateTime.monday => 'Mon', + DateTime.tuesday => 'Tue', + DateTime.wednesday => 'Wed', + DateTime.thursday => 'Thu', + DateTime.friday => 'Fri', + DateTime.saturday => 'Sat', + DateTime.sunday => 'Sun', + _ => '', + }; + } + return '${date.month}/${date.day}'; + } +} diff --git a/lib/features/analysis/presentation/pages/analysis_page.dart b/lib/features/analysis/presentation/pages/analysis_page.dart new file mode 100644 index 0000000..3fbb8b1 --- /dev/null +++ b/lib/features/analysis/presentation/pages/analysis_page.dart @@ -0,0 +1,578 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../manager/analysis_range.dart'; +import '../manager/analysis_chart_metric.dart'; +import '../manager/analysis_ui_state.dart'; +import '../manager/analysis_view_model.dart'; +import '../widgets/analysis_line_chart.dart'; + +class AnalysisPage extends ConsumerWidget { + const AnalysisPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final rangePreset = ref.watch(analysisRangeNotifierProvider); + final chartMetric = ref.watch(analysisChartMetricNotifierProvider); + final stateAsync = ref.watch(analysisViewModelProvider); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Analysis'), + backgroundColor: theme.scaffoldBackgroundColor, + surfaceTintColor: theme.scaffoldBackgroundColor, + elevation: 0, + scrolledUnderElevation: 0, + ), + body: RefreshIndicator( + onRefresh: () async => ref.invalidate(analysisViewModelProvider), + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + _RangeSelector( + selected: rangePreset, + onSelected: (preset) => ref + .read(analysisRangeNotifierProvider.notifier) + .setRange(preset), + ), + const SizedBox(height: 10), + _ChartMetricSelector( + selected: chartMetric, + onSelected: (metric) => ref + .read(analysisChartMetricNotifierProvider.notifier) + .setMetric(metric), + ), + const SizedBox(height: 12), + stateAsync.when( + data: (state) => + _AnalysisBody(state: state, chartMetric: chartMetric), + loading: () => const _LoadingBody(), + error: (error, stackTrace) => _ErrorBody( + onRetry: () => ref.invalidate(analysisViewModelProvider), + ), + ), + ], + ), + ), + ); + } +} + +class _RangeSelector extends StatelessWidget { + const _RangeSelector({required this.selected, required this.onSelected}); + + final AnalysisRangePreset selected; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: AnalysisRangePreset.values + .map( + (preset) => ButtonSegment(value: preset, label: Text(preset.label)), + ) + .toList(growable: false), + selected: {selected}, + onSelectionChanged: (selection) { + if (selection.isEmpty) return; + onSelected(selection.first); + }, + ); + } +} + +class _AnalysisBody extends StatelessWidget { + const _AnalysisBody({required this.state, required this.chartMetric}); + + final AnalysisUiState state; + final AnalysisChartMetric chartMetric; + + @override + Widget build(BuildContext context) { + final summary = state.summary ?? AnalysisSummaryUiModel.empty; + final series = state.series ?? const []; + final rangeLabel = state.rangeLabel ?? ''; + + final values = series + .map( + (p) => switch (chartMetric) { + AnalysisChartMetric.scans => p.scanCount.toDouble(), + AnalysisChartMetric.pointsUsed => p.pointsUsed.toDouble(), + }, + ) + .toList(growable: false); + + final totalValue = switch (chartMetric) { + AnalysisChartMetric.scans => summary.scans, + AnalysisChartMetric.pointsUsed => summary.pointsUsed, + }; + final xLabels = series.map((p) => p.label).toList(growable: false); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ChartCard( + title: chartMetric.label, + subtitle: rangeLabel, + totalValue: totalValue, + chart: AnalysisLineChart(values: values), + xLabels: xLabels, + ), + const SizedBox(height: 12), + _InsightRow(state: state), + const SizedBox(height: 12), + _KpiGrid(summary: summary), + ], + ); + } +} + +class _ChartCard extends StatelessWidget { + const _ChartCard({ + required this.title, + required this.subtitle, + required this.totalValue, + required this.chart, + required this.xLabels, + }); + + final String title; + final String subtitle; + final int totalValue; + final Widget chart; + final List xLabels; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + totalValue.toString(), + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 4), + const SizedBox.shrink(), + ], + ), + ], + ), + const SizedBox(height: 12), + chart, + if (xLabels.isNotEmpty) ...[ + const SizedBox(height: 10), + _XAxisLabels(labels: xLabels), + ], + ], + ), + ), + ); + } +} + +class _KpiGrid extends StatelessWidget { + const _KpiGrid({required this.summary}); + + final AnalysisSummaryUiModel summary; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 1.5, + children: [ + _KpiCard( + title: 'Scans', + value: summary.scans.toString(), + icon: Icons.qr_code_scanner_rounded, + ), + _KpiCard( + title: 'Users', + value: summary.users.toString(), + icon: Icons.people_alt_rounded, + ), + _KpiCard( + title: 'Active Users', + value: summary.activeUsers.toString(), + icon: Icons.person_pin_circle_rounded, + ), + _KpiCard( + title: 'Points Used', + value: summary.pointsUsed.toString(), + icon: Icons.stars_rounded, + ), + _KpiCard( + title: 'Points/User', + value: summary.users == 0 + ? '0' + : (summary.pointsUsed / summary.users).toStringAsFixed(2), + icon: Icons.functions_rounded, + ), + _KpiCard( + title: 'Success Rate', + value: '${summary.successRatePercent}%', + icon: Icons.verified_rounded, + ), + ], + ); + } +} + +class _ChartMetricSelector extends StatelessWidget { + const _ChartMetricSelector({ + required this.selected, + required this.onSelected, + }); + + final AnalysisChartMetric selected; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: AnalysisChartMetric.values + .map( + (metric) => ButtonSegment(value: metric, label: Text(metric.label)), + ) + .toList(growable: false), + selected: {selected}, + onSelectionChanged: (selection) { + if (selection.isEmpty) return; + onSelected(selection.first); + }, + ); + } +} + +class _XAxisLabels extends StatelessWidget { + const _XAxisLabels({required this.labels}); + + final List labels; + + @override + Widget build(BuildContext context) { + final textStyle = Theme.of(context).textTheme.labelSmall; + final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant; + + final count = labels.length; + final desiredTicks = 4; + final step = count <= desiredTicks ? 1 : (count - 1) ~/ (desiredTicks - 1); + + final ticks = {0, count - 1}; + for (var i = 0; i < count; i += step) { + ticks.add(i); + } + + return Row( + children: List.generate(count, (i) { + final show = ticks.contains(i); + return Expanded( + child: Align( + alignment: i == 0 + ? Alignment.centerLeft + : i == count - 1 + ? Alignment.centerRight + : Alignment.center, + child: Text( + show ? labels[i] : '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle?.copyWith(color: onSurfaceVariant), + ), + ), + ); + }), + ); + } +} + +class _InsightRow extends StatelessWidget { + const _InsightRow({required this.state}); + + final AnalysisUiState state; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final averageScansPerDay = state.averageScansPerDay ?? 0; + final averagePointsPerDay = state.averagePointsPerDay ?? 0; + final pointsPerScan = state.pointsPerScan ?? 0; + final activeUserRatePercent = state.activeUserRatePercent ?? 0; + + return Wrap( + spacing: 12, + runSpacing: 12, + children: + [ + _InsightCard( + title: 'Avg scans/day', + value: averageScansPerDay.toStringAsFixed(1), + icon: Icons.stacked_line_chart_rounded, + ), + _InsightCard( + title: 'Avg points/day', + value: averagePointsPerDay.toStringAsFixed(1), + icon: Icons.trending_up_rounded, + ), + _InsightCard( + title: 'Points/scan', + value: pointsPerScan.toStringAsFixed(2), + icon: Icons.bolt_rounded, + ), + _InsightCard( + title: 'Active rate', + value: '$activeUserRatePercent%', + icon: Icons.percent_rounded, + ), + ] + .map((w) { + return SizedBox( + width: (MediaQuery.of(context).size.width - 16 * 2 - 12) / 2, + child: w, + ); + }) + .toList(growable: false), + ); + } +} + +class _InsightCard extends StatelessWidget { + const _InsightCard({ + required this.title, + required this.value, + required this.icon, + }); + + final String title; + final String value; + final IconData icon; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Icon( + icon, + color: colorScheme.onPrimaryContainer, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + title, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _KpiCard extends StatelessWidget { + const _KpiCard({ + required this.title, + required this.value, + required this.icon, + }); + + final String title; + final String value; + final IconData icon; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Icon( + icon, + color: colorScheme.onPrimaryContainer, + size: 20, + ), + ), + const Spacer(), + Text( + value, + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + title, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } +} + +class _LoadingBody extends StatelessWidget { + const _LoadingBody(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 40), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text('Loading analytics...'), + ], + ), + ), + ); + } +} + +class _ErrorBody extends StatelessWidget { + const _ErrorBody({required this.onRetry}); + + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 40), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Unable to load analytics'), + const SizedBox(height: 10), + TextButton(onPressed: onRetry, child: const Text('Retry')), + ], + ), + ), + ); + } +} diff --git a/lib/features/analysis/presentation/widgets/analysis_line_chart.dart b/lib/features/analysis/presentation/widgets/analysis_line_chart.dart new file mode 100644 index 0000000..574f7c1 --- /dev/null +++ b/lib/features/analysis/presentation/widgets/analysis_line_chart.dart @@ -0,0 +1,115 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class AnalysisLineChart extends StatelessWidget { + const AnalysisLineChart({super.key, required this.values, this.height = 140}); + + final List values; + final double height; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return SizedBox( + height: height, + width: double.infinity, + child: CustomPaint( + painter: _LineChartPainter( + values: values, + lineColor: colorScheme.primary, + fillColor: colorScheme.primary.withOpacity(0.16), + gridColor: colorScheme.outlineVariant.withOpacity(0.35), + ), + ), + ); + } +} + +class _LineChartPainter extends CustomPainter { + _LineChartPainter({ + required this.values, + required this.lineColor, + required this.fillColor, + required this.gridColor, + }); + + final List values; + final Color lineColor; + final Color fillColor; + final Color gridColor; + + @override + void paint(Canvas canvas, Size size) { + if (values.isEmpty) return; + + final padding = const EdgeInsets.fromLTRB(4, 6, 4, 6); + final chartRect = Rect.fromLTWH( + padding.left, + padding.top, + max(0, size.width - padding.horizontal), + max(0, size.height - padding.vertical), + ); + + _paintGrid(canvas, chartRect); + + final minValue = values.reduce(min); + final maxValue = values.reduce(max); + final span = max(1e-6, maxValue - minValue); + + final dx = values.length == 1 ? 0.0 : chartRect.width / (values.length - 1); + + Offset pointAt(int i) { + final x = chartRect.left + dx * i; + final normalized = (values[i] - minValue) / span; + final y = chartRect.bottom - normalized * chartRect.height; + return Offset(x, y); + } + + final linePath = Path()..moveTo(pointAt(0).dx, pointAt(0).dy); + for (var i = 1; i < values.length; i++) { + final p = pointAt(i); + linePath.lineTo(p.dx, p.dy); + } + + final fillPath = Path.from(linePath) + ..lineTo(chartRect.right, chartRect.bottom) + ..lineTo(chartRect.left, chartRect.bottom) + ..close(); + + final fillPaint = Paint()..color = fillColor; + canvas.drawPath(fillPath, fillPaint); + + final linePaint = Paint() + ..color = lineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5 + ..strokeCap = StrokeCap.round; + canvas.drawPath(linePath, linePaint); + + final last = pointAt(values.length - 1); + final dotPaint = Paint()..color = lineColor; + canvas.drawCircle(last, 4, dotPaint); + canvas.drawCircle(last, 8, Paint()..color = lineColor.withOpacity(0.16)); + } + + void _paintGrid(Canvas canvas, Rect rect) { + final paint = Paint() + ..color = gridColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + for (var i = 1; i <= 3; i++) { + final y = rect.top + rect.height * (i / 4); + canvas.drawLine(Offset(rect.left, y), Offset(rect.right, y), paint); + } + } + + @override + bool shouldRepaint(covariant _LineChartPainter oldDelegate) { + return oldDelegate.values != values || + oldDelegate.lineColor != lineColor || + oldDelegate.fillColor != fillColor || + oldDelegate.gridColor != gridColor; + } +} diff --git a/lib/features/history/data/data_sources/history_local_data_source.dart b/lib/features/history/data/data_sources/history_local_data_source.dart new file mode 100644 index 0000000..5f4932e --- /dev/null +++ b/lib/features/history/data/data_sources/history_local_data_source.dart @@ -0,0 +1,111 @@ +import 'dart:math'; + +import '../../domain/entities/history_item.dart'; + +abstract class HistoryLocalDataSource { + List getHistory({int limit = 20}); +} + +class HistoryLocalDataSourceImpl implements HistoryLocalDataSource { + const HistoryLocalDataSourceImpl(); + + @override + List getHistory({int limit = 20}) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final rand = Random(99173 + today.millisecondsSinceEpoch); + + const merchants = [ + 'CB Prestige', + 'Prestige Rewards', + 'City Center', + 'Junction Mall', + 'Market Place', + 'Downtown Branch', + 'North Branch', + ]; + + const buyItems = [ + 'Coffee', + 'Lunch', + 'Movie Ticket', + 'Grocery', + 'Mobile Top-up', + 'Taxi', + 'Shopping', + 'Snacks', + ]; + + const promoItems = [ + 'Promo Bonus', + 'Referral Reward', + 'Campaign Gift', + 'Welcome Gift', + 'Cashback Bonus', + ]; + + // Build daily buckets so grouping looks realistic (multiple transactions/day). + final items = []; + var dayOffset = 0; + while (items.length < limit && dayOffset < 60) { + final date = today.subtract(Duration(days: dayOffset)); + + // More activity on recent days, less on older days. + final maxPerDay = dayOffset <= 1 + ? 6 + : dayOffset <= 6 + ? 4 + : 2; + final countForDay = 1 + rand.nextInt(maxPerDay); + + for (var i = 0; i < countForDay && items.length < limit; i++) { + // Business-hour-ish distribution with some night activity. + final hour = rand.nextInt(100) < 82 + ? 8 + rand.nextInt(13) + : rand.nextInt(24); + final minute = rand.nextInt(60); + final occurredAt = date.add(Duration(hours: hour, minutes: minute)); + + final merchant = merchants[rand.nextInt(merchants.length)]; + final isRedeem = rand.nextInt(100) < 65; // spending more common + final title = isRedeem + ? buyItems[rand.nextInt(buyItems.length)] + : promoItems[rand.nextInt(promoItems.length)]; + + final amountAbs = isRedeem + ? (500 + rand.nextInt(15000)).toDouble() + : (200 + rand.nextInt(6000)).toDouble(); + + final signedAmount = isRedeem ? -amountAbs : amountAbs; + + items.add( + HistoryItem( + title: title, + subtitle: merchant, + occurredAt: occurredAt, + amount: signedAmount, + ), + ); + } + + dayOffset++; + } + + // Guarantee we have at least one earn and one redeem entry so both states show. + final hasRedeem = items.any((e) => e.amount < 0); + final hasEarn = items.any((e) => e.amount > 0); + if (items.isNotEmpty && (!hasRedeem || !hasEarn)) { + final first = items.first; + final flipToRedeem = !hasRedeem; + items[0] = HistoryItem( + title: first.title, + subtitle: first.subtitle, + occurredAt: first.occurredAt, + amount: flipToRedeem ? -first.amount.abs() : first.amount.abs(), + ); + } + + items.sort((a, b) => b.occurredAt.compareTo(a.occurredAt)); + return items; + } +} diff --git a/lib/features/history/data/repositories/history_repository_impl.dart b/lib/features/history/data/repositories/history_repository_impl.dart new file mode 100644 index 0000000..e4d5180 --- /dev/null +++ b/lib/features/history/data/repositories/history_repository_impl.dart @@ -0,0 +1,15 @@ +import '../../domain/entities/history_content.dart'; +import '../../domain/repositories/history_repository.dart'; +import '../data_sources/history_local_data_source.dart'; + +class HistoryRepositoryImpl implements HistoryRepository { + const HistoryRepositoryImpl(this._localDataSource); + + final HistoryLocalDataSource _localDataSource; + + @override + Future getHistory({int limit = 20}) async { + final items = _localDataSource.getHistory(limit: limit); + return HistoryContent(items: items); + } +} diff --git a/lib/features/history/domain/entities/history_content.dart b/lib/features/history/domain/entities/history_content.dart new file mode 100644 index 0000000..a0e05da --- /dev/null +++ b/lib/features/history/domain/entities/history_content.dart @@ -0,0 +1,7 @@ +import 'history_item.dart'; + +class HistoryContent { + const HistoryContent({required this.items}); + + final List items; +} diff --git a/lib/features/history/domain/entities/history_item.dart b/lib/features/history/domain/entities/history_item.dart new file mode 100644 index 0000000..b08e07b --- /dev/null +++ b/lib/features/history/domain/entities/history_item.dart @@ -0,0 +1,13 @@ +class HistoryItem { + const HistoryItem({ + required this.title, + required this.subtitle, + required this.occurredAt, + required this.amount, + }); + + final String title; + final String subtitle; + final DateTime occurredAt; + final double amount; +} diff --git a/lib/features/history/domain/repositories/history_repository.dart b/lib/features/history/domain/repositories/history_repository.dart new file mode 100644 index 0000000..f720661 --- /dev/null +++ b/lib/features/history/domain/repositories/history_repository.dart @@ -0,0 +1,5 @@ +import '../entities/history_content.dart'; + +abstract class HistoryRepository { + Future getHistory({int limit = 20}); +} diff --git a/lib/features/history/domain/use_cases/get_history.dart b/lib/features/history/domain/use_cases/get_history.dart new file mode 100644 index 0000000..7d9a913 --- /dev/null +++ b/lib/features/history/domain/use_cases/get_history.dart @@ -0,0 +1,12 @@ +import '../entities/history_content.dart'; +import '../repositories/history_repository.dart'; + +class GetHistory { + const GetHistory(this._repository); + + final HistoryRepository _repository; + + Future call({int limit = 20}) { + return _repository.getHistory(limit: limit); + } +} diff --git a/lib/features/history/presentation/manager/history_ui_state.dart b/lib/features/history/presentation/manager/history_ui_state.dart new file mode 100644 index 0000000..90e1ff3 --- /dev/null +++ b/lib/features/history/presentation/manager/history_ui_state.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class HistoryItemUiModel { + const HistoryItemUiModel({ + required this.title, + required this.subtitle, + required this.timeLabel, + required this.amountLabel, + this.isRedeem, + }); + + final String title; + final String subtitle; + final String timeLabel; + final String amountLabel; + final bool? isRedeem; +} + +@immutable +class HistorySectionUiModel { + const HistorySectionUiModel({ + required this.dateLabel, + required this.totalAmountLabel, + required this.totalTransactionsLabel, + required this.items, + }); + + final String dateLabel; + final String totalAmountLabel; + final String totalTransactionsLabel; + final List items; +} + +@immutable +class HistoryUiState { + const HistoryUiState({List? sections}) + : sections = sections ?? const []; + + final List? sections; +} diff --git a/lib/features/history/presentation/manager/history_view_model.dart b/lib/features/history/presentation/manager/history_view_model.dart new file mode 100644 index 0000000..bbf5f88 --- /dev/null +++ b/lib/features/history/presentation/manager/history_view_model.dart @@ -0,0 +1,133 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../data/data_sources/history_local_data_source.dart'; +import '../../data/repositories/history_repository_impl.dart'; +import '../../domain/entities/history_content.dart'; +import '../../domain/repositories/history_repository.dart'; +import '../../domain/use_cases/get_history.dart'; +import 'history_ui_state.dart'; + +final _historyLocalDataSourceProvider = Provider( + (ref) => const HistoryLocalDataSourceImpl(), +); + +final _historyRepositoryProvider = Provider( + (ref) => HistoryRepositoryImpl(ref.watch(_historyLocalDataSourceProvider)), +); + +final _getHistoryProvider = Provider( + (ref) => GetHistory(ref.watch(_historyRepositoryProvider)), +); + +final historyViewModelProvider = + AsyncNotifierProvider( + HistoryViewModel.new, + ); + +class HistoryViewModel extends AsyncNotifier { + @override + Future build() async { + final content = await ref.watch(_getHistoryProvider)(); + return _mapContentToUiState(content); + } + + HistoryUiState _mapContentToUiState(HistoryContent content) { + final grouped = >{}; + final totals = {}; + + for (final item in content.items) { + final dayKey = DateTime( + item.occurredAt.year, + item.occurredAt.month, + item.occurredAt.day, + ); + + final uiItem = HistoryItemUiModel( + title: item.title, + subtitle: item.subtitle, + timeLabel: _formatTime(item.occurredAt), + amountLabel: _formatAmount(item.amount), + isRedeem: item.amount < 0, + ); + + grouped.putIfAbsent(dayKey, () => []).add(uiItem); + totals[dayKey] = (totals[dayKey] ?? 0) + item.amount; + } + + final sortedKeys = grouped.keys.toList()..sort((a, b) => b.compareTo(a)); + + final sections = sortedKeys + .map((dayKey) { + final items = grouped[dayKey] ?? const []; + final totalAmount = totals[dayKey] ?? 0; + + return HistorySectionUiModel( + dateLabel: _formatDateHeader(dayKey), + totalAmountLabel: _formatAmount(totalAmount), + totalTransactionsLabel: '${items.length} txn', + items: items, + ); + }) + .toList(growable: false); + + return HistoryUiState(sections: sections); + } + + String _formatDateHeader(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + + final day = DateTime(date.year, date.month, date.day); + if (day == today) return 'Today'; + if (day == yesterday) return 'Yesterday'; + + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + return '${months[date.month - 1]} ${date.day}, ${date.year}'; + } + + String _formatTime(DateTime dateTime) { + var hour = dateTime.hour; + final minute = dateTime.minute.toString().padLeft(2, '0'); + final suffix = hour >= 12 ? 'PM' : 'AM'; + hour = hour % 12; + if (hour == 0) hour = 12; + return '$hour:$minute $suffix'; + } + + String _formatAmount(double amount) { + final isNegative = amount < 0; + final abs = amount.abs(); + final intPart = abs.floor(); + final formattedInt = _formatIntWithCommas(intPart); + final sign = isNegative ? '-' : '+'; + final unit = intPart == 1 ? 'coin' : 'coins'; + return '$sign$formattedInt $unit'; + } + + String _formatIntWithCommas(int value) { + final s = value.toString(); + final buffer = StringBuffer(); + for (var i = 0; i < s.length; i++) { + final indexFromEnd = s.length - i; + buffer.write(s[i]); + if (indexFromEnd > 1 && indexFromEnd % 3 == 1) { + buffer.write(','); + } + } + return buffer.toString(); + } +} diff --git a/lib/features/history/presentation/pages/history_page.dart b/lib/features/history/presentation/pages/history_page.dart new file mode 100644 index 0000000..975f10b --- /dev/null +++ b/lib/features/history/presentation/pages/history_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../manager/history_ui_state.dart'; +import '../manager/history_view_model.dart'; +import '../widgets/history_section_card.dart'; + +class HistoryPage extends ConsumerWidget { + const HistoryPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final stateAsync = ref.watch(historyViewModelProvider); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('History'), + backgroundColor: theme.scaffoldBackgroundColor, + surfaceTintColor: theme.scaffoldBackgroundColor, + elevation: 0, + scrolledUnderElevation: 0, + ), + body: RefreshIndicator( + onRefresh: () async => ref.invalidate(historyViewModelProvider), + child: SafeArea( + bottom: true, + child: stateAsync.when( + data: (state) => _HistoryBody( + state: state, + onReload: () => ref.invalidate(historyViewModelProvider), + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( + child: TextButton( + onPressed: () => ref.invalidate(historyViewModelProvider), + child: const Text('Retry'), + ), + ), + ), + ), + ), + ); + } +} + +class _HistoryBody extends StatelessWidget { + const _HistoryBody({required this.state, required this.onReload}); + + final HistoryUiState state; + final VoidCallback onReload; + + @override + Widget build(BuildContext context) { + final sections = state.sections ?? const []; + + if (sections.isEmpty) { + return ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + const SizedBox(height: 80), + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('No history yet'), + const SizedBox(height: 10), + TextButton(onPressed: onReload, child: const Text('Reload')), + ], + ), + ), + ], + ); + } + + return ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + for (final section in sections) HistorySectionCard(section: section), + ], + ); + } +} diff --git a/lib/features/history/presentation/widgets/history_item_tile.dart b/lib/features/history/presentation/widgets/history_item_tile.dart new file mode 100644 index 0000000..ef4a6ef --- /dev/null +++ b/lib/features/history/presentation/widgets/history_item_tile.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class HistoryItemTile extends StatelessWidget { + const HistoryItemTile({ + super.key, + required this.title, + required this.subtitle, + required this.timeLabel, + this.amountLabel, + this.isRedeem, + this.onTap, + }); + + final String title; + final String subtitle; + final String timeLabel; + final String? amountLabel; + final bool? isRedeem; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final redeem = isRedeem ?? false; + + final accentColor = redeem ? colorScheme.error : colorScheme.tertiary; + final accentContainer = redeem + ? colorScheme.errorContainer + : colorScheme.tertiaryContainer; + final onAccentContainer = redeem + ? colorScheme.onErrorContainer + : colorScheme.onTertiaryContainer; + final leadingAssetPath = redeem + ? 'assets/svg/coin_send.svg' + : 'assets/svg/coin_receive.svg'; + + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: accentContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: SvgPicture.asset( + leadingAssetPath, + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + onAccentContainer, + BlendMode.srcIn, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + '$subtitle • $timeLabel', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + if (amountLabel != null && amountLabel!.isNotEmpty) + Text( + amountLabel!, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + color: accentColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/history/presentation/widgets/history_section_card.dart b/lib/features/history/presentation/widgets/history_section_card.dart new file mode 100644 index 0000000..02a29f2 --- /dev/null +++ b/lib/features/history/presentation/widgets/history_section_card.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import '../manager/history_ui_state.dart'; +import 'history_item_tile.dart'; + +class HistorySectionCard extends StatelessWidget { + const HistorySectionCard({super.key, required this.section, this.onItemTap}); + + final HistorySectionUiModel section; + final void Function(int index)? onItemTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.5), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + color: colorScheme.primary, + child: Row( + children: [ + Expanded( + child: Text( + section.dateLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + color: colorScheme.onPrimary, + ), + ), + ), + const SizedBox(width: 10), + Text( + '${section.totalTransactionsLabel} • ${section.totalAmountLabel}', + style: textTheme.labelMedium?.copyWith( + color: colorScheme.onPrimary.withOpacity(0.9), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + for (var i = 0; i < section.items.length; i++) ...[ + if (i != 0) + Divider( + height: 1, + thickness: 1, + color: colorScheme.outlineVariant.withOpacity(0.35), + ), + HistoryItemTile( + title: section.items[i].title, + subtitle: section.items[i].subtitle, + timeLabel: section.items[i].timeLabel, + amountLabel: section.items[i].amountLabel, + isRedeem: section.items[i].isRedeem ?? false, + onTap: onItemTap == null ? null : () => onItemTap!(i), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/home/data/data_sources/home_local_data_source.dart b/lib/features/home/data/data_sources/home_local_data_source.dart new file mode 100644 index 0000000..4b49c70 --- /dev/null +++ b/lib/features/home/data/data_sources/home_local_data_source.dart @@ -0,0 +1,31 @@ +import '../models/recent_scan_model.dart'; + +abstract class HomeLocalDataSource { + List getCarouselAssets(); + List getRecentScans({int limit = 5}); +} + +class HomeLocalDataSourceImpl implements HomeLocalDataSource { + const HomeLocalDataSourceImpl(); + + @override + List getCarouselAssets() { + return const [ + 'assets/images/caro_1.jpg', + 'assets/images/caro_2.jpg', + 'assets/images/caro_3.jpg', + ]; + } + + @override + List getRecentScans({int limit = 5}) { + return List.generate( + limit, + (index) => RecentScanModel( + title: 'Scan Item ${index + 1}', + subtitle: 'Scanned recently', + ), + ); + } +} + diff --git a/lib/features/home/data/models/recent_scan_model.dart b/lib/features/home/data/models/recent_scan_model.dart new file mode 100644 index 0000000..0e1f902 --- /dev/null +++ b/lib/features/home/data/models/recent_scan_model.dart @@ -0,0 +1,9 @@ +import '../../domain/entities/recent_scan.dart'; + +class RecentScanModel extends RecentScan { + const RecentScanModel({ + required super.title, + required super.subtitle, + }); +} + diff --git a/lib/features/home/data/repositories/home_repository_impl.dart b/lib/features/home/data/repositories/home_repository_impl.dart new file mode 100644 index 0000000..9d8d094 --- /dev/null +++ b/lib/features/home/data/repositories/home_repository_impl.dart @@ -0,0 +1,21 @@ +import '../../domain/entities/home_content.dart'; +import '../../domain/repositories/home_repository.dart'; +import '../data_sources/home_local_data_source.dart'; + +class HomeRepositoryImpl implements HomeRepository { + const HomeRepositoryImpl(this._localDataSource); + + final HomeLocalDataSource _localDataSource; + + @override + Future getHomeContent({int recentLimit = 5}) async { + final carouselAssets = _localDataSource.getCarouselAssets(); + final recentScans = _localDataSource.getRecentScans(limit: recentLimit); + + return HomeContent( + carouselAssets: carouselAssets, + recentScans: recentScans, + ); + } +} + diff --git a/lib/features/home/domain/entities/home_content.dart b/lib/features/home/domain/entities/home_content.dart new file mode 100644 index 0000000..471cf20 --- /dev/null +++ b/lib/features/home/domain/entities/home_content.dart @@ -0,0 +1,12 @@ +import 'recent_scan.dart'; + +class HomeContent { + const HomeContent({ + required this.carouselAssets, + required this.recentScans, + }); + + final List carouselAssets; + final List recentScans; +} + diff --git a/lib/features/home/domain/entities/recent_scan.dart b/lib/features/home/domain/entities/recent_scan.dart new file mode 100644 index 0000000..a6721bb --- /dev/null +++ b/lib/features/home/domain/entities/recent_scan.dart @@ -0,0 +1,10 @@ +class RecentScan { + const RecentScan({ + required this.title, + required this.subtitle, + }); + + final String title; + final String subtitle; +} + diff --git a/lib/features/home/domain/repositories/home_repository.dart b/lib/features/home/domain/repositories/home_repository.dart new file mode 100644 index 0000000..a9a08c4 --- /dev/null +++ b/lib/features/home/domain/repositories/home_repository.dart @@ -0,0 +1,6 @@ +import '../entities/home_content.dart'; + +abstract class HomeRepository { + Future getHomeContent({int recentLimit = 5}); +} + diff --git a/lib/features/home/domain/use_cases/get_home_content.dart b/lib/features/home/domain/use_cases/get_home_content.dart new file mode 100644 index 0000000..07cb99e --- /dev/null +++ b/lib/features/home/domain/use_cases/get_home_content.dart @@ -0,0 +1,13 @@ +import '../entities/home_content.dart'; +import '../repositories/home_repository.dart'; + +class GetHomeContent { + const GetHomeContent(this._repository); + + final HomeRepository _repository; + + Future call({int recentLimit = 5}) { + return _repository.getHomeContent(recentLimit: recentLimit); + } +} + diff --git a/lib/features/home/presentation/manager/home_ui_state.dart b/lib/features/home/presentation/manager/home_ui_state.dart new file mode 100644 index 0000000..f51a6a6 --- /dev/null +++ b/lib/features/home/presentation/manager/home_ui_state.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class RecentScanUiModel { + const RecentScanUiModel({required this.title, required this.subtitle}); + + final String title; + final String subtitle; +} + +@immutable +class HomeUiState { + const HomeUiState({required this.carouselAssets, required this.recentScans}); + + final List carouselAssets; + final List recentScans; +} diff --git a/lib/features/home/presentation/manager/home_view_model.dart b/lib/features/home/presentation/manager/home_view_model.dart new file mode 100644 index 0000000..9aa36aa --- /dev/null +++ b/lib/features/home/presentation/manager/home_view_model.dart @@ -0,0 +1,52 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../data/data_sources/home_local_data_source.dart'; +import '../../data/repositories/home_repository_impl.dart'; +import '../../domain/entities/home_content.dart'; +import '../../domain/repositories/home_repository.dart'; +import '../../domain/use_cases/get_home_content.dart'; +import 'home_ui_state.dart'; + +final _homeLocalDataSourceProvider = Provider( + (ref) => const HomeLocalDataSourceImpl(), +); + +final _homeRepositoryProvider = Provider( + (ref) => HomeRepositoryImpl(ref.watch(_homeLocalDataSourceProvider)), +); + +final _getHomeContentProvider = Provider( + (ref) => GetHomeContent(ref.watch(_homeRepositoryProvider)), +); + +final homeViewModelProvider = AsyncNotifierProvider( + HomeViewModel.new, +); + +class HomeViewModel extends AsyncNotifier { + @override + Future build() async { + final content = await ref.watch(_getHomeContentProvider)(); + return _mapContentToUiState(content); + } + + HomeUiState _mapContentToUiState(HomeContent content) { + return HomeUiState( + carouselAssets: content.carouselAssets, + recentScans: content.recentScans + .map( + (scan) => + RecentScanUiModel(title: scan.title, subtitle: scan.subtitle), + ) + .toList(growable: false), + ); + } + + void onSeeAllTapped() { + // TODO: navigate to full list + } + + void onRecentScanTapped(int index) { + // TODO: navigate to details + } +} diff --git a/lib/features/home/presentation/pages/home.dart b/lib/features/home/presentation/pages/home.dart new file mode 100644 index 0000000..d3ca565 --- /dev/null +++ b/lib/features/home/presentation/pages/home.dart @@ -0,0 +1,210 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:cb_prestige_qr/features/history/presentation/widgets/history_item_tile.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../../core/presentation/widgets/global_loading_overlay.dart'; +import '../manager/home_ui_state.dart'; +import '../manager/home_view_model.dart'; +import '../widgets/home_today_summary_section.dart'; + +class HomeView extends ConsumerWidget { + const HomeView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final viewModel = ref.read(homeViewModelProvider.notifier); + final stateAsync = ref.watch(homeViewModelProvider); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + elevation: 0, + scrolledUnderElevation: 0, + titleSpacing: 16, + title: Row( + children: [ + Image.asset( + 'assets/images/logo_white.png', + height: 60, + fit: BoxFit.contain, + ), + const Spacer(), + Image.asset( + 'assets/images/cb_logo_white.png', + height: 50, + fit: BoxFit.contain, + ), + ], + ), + ), + body: stateAsync.when( + data: (state) => _HomeBody( + state: state, + onSeeAll: viewModel.onSeeAllTapped, + onRecentScanTap: viewModel.onRecentScanTapped, + ), + loading: () => const GlobalLoadingOverlay(isLoading: true), + error: (error, stackTrace) => Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Failed to load Home'), + const SizedBox(height: 10), + TextButton( + onPressed: () => ref.invalidate(homeViewModelProvider), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _HomeBody extends StatelessWidget { + const _HomeBody({ + required this.state, + required this.onSeeAll, + required this.onRecentScanTap, + }); + + final HomeUiState state; + final VoidCallback onSeeAll; + final void Function(int index) onRecentScanTap; + + @override + Widget build(BuildContext context) { + final shellBottomInset = + 70 + MediaQuery.of(context).padding.bottom; + final contentBottomPadding = shellBottomInset + 8; + + return CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), + sliver: SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: CarouselSlider( + options: CarouselOptions( + height: MediaQuery.of(context).size.height * 0.3, + viewportFraction: 1.0, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 3), + autoPlayAnimationDuration: const Duration( + milliseconds: 800, + ), + autoPlayCurve: Curves.fastOutSlowIn, + ), + items: state.carouselAssets.map((assetPath) { + return Builder( + builder: (BuildContext context) { + return SizedBox( + width: double.infinity, + child: Image.asset(assetPath, fit: BoxFit.cover), + ); + }, + ); + }).toList(), + ), + ), + const SizedBox(height: 20), + HomeTodaySummarySection( + customersLabel: '18', + earnedCoinsLabel: '+3,240', + redeemedCoinsLabel: '-1,120', + onViewDetailsTap: onSeeAll, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Recent History', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + TextButton( + onPressed: onSeeAll, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + foregroundColor: Colors.blue, + ), + child: const Text('See All'), + ), + ], + ), + const SizedBox(height: 10), + ], + ), + ), + ), + SliverPadding( + padding: EdgeInsets.fromLTRB(16, 0, 16, contentBottomPadding), + sliver: state.recentScans.isEmpty + ? const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 40), + child: Center(child: Text('No recent scans')), + ), + ) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index.isOdd) return const SizedBox(height: 10); + + final itemIndex = index ~/ 2; + final item = state.recentScans[itemIndex]; + final colorScheme = Theme.of(context).colorScheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.5), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: HistoryItemTile( + title: item.title, + subtitle: item.subtitle, + timeLabel: 'Just now', + amountLabel: null, + isRedeem: null, + onTap: () => onRecentScanTap(itemIndex), + ), + ), + ); + }, + childCount: state.recentScans.isEmpty + ? 0 + : state.recentScans.length * 2 - 1, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/home/presentation/widgets/home_context_section.dart b/lib/features/home/presentation/widgets/home_context_section.dart new file mode 100644 index 0000000..5417418 --- /dev/null +++ b/lib/features/home/presentation/widgets/home_context_section.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class HomeContextSection extends StatelessWidget { + const HomeContextSection({ + super.key, + this.customerName, + this.customerIdLabel, + this.coinBalanceLabel, + this.secondaryInfoLabel, + this.onScanCustomerTap, + this.onEarnCoinsTap, + this.onRedeemCoinsTap, + this.onApplyPromoTap, + }); + + /// For cashier flow: e.g. "Cashier Mode" + + /// If null, the card renders the "scan customer" empty state. + final String? customerName; + final String? customerIdLabel; + + /// Customer coin balance label, e.g. "2,450 coins" + final String? coinBalanceLabel; + + /// Optional small chip on the right, e.g. "Promo ready" + final String? secondaryInfoLabel; + + final VoidCallback? onScanCustomerTap; + final VoidCallback? onEarnCoinsTap; + final VoidCallback? onRedeemCoinsTap; + final VoidCallback? onApplyPromoTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final hasCustomer = customerName != null && customerName!.trim().isNotEmpty; + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [colorScheme.primary.withOpacity(0.35), colorScheme.surface], + ), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.10), + blurRadius: 18, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: SvgPicture.asset( + 'assets/svg/coin_receive.svg', + width: 22, + height: 22, + colorFilter: ColorFilter.mode( + colorScheme.onPrimaryContainer, + BlendMode.srcIn, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + const SizedBox(height: 2), + Text( + hasCustomer + ? (customerName ?? '') + : 'Scan customer QR to start', + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + ), + ), + if (hasCustomer && customerIdLabel != null) ...[ + const SizedBox(height: 2), + Text( + customerIdLabel!, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + if ((secondaryInfoLabel ?? '').trim().isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.6), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.4), + ), + ), + child: Text( + secondaryInfoLabel!, + style: textTheme.labelMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + if (hasCustomer && (coinBalanceLabel ?? '').trim().isNotEmpty) ...[ + const SizedBox(height: 10), + Text( + 'Customer coins: ${coinBalanceLabel!}', + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ], + const SizedBox(height: 12), + if (!hasCustomer) + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: onScanCustomerTap, + child: const Text('Scan Customer QR'), + ), + ) + else + Column( + children: [ + Row( + children: [ + Expanded( + child: FilledButton( + onPressed: onEarnCoinsTap, + child: const Text('Add Coins'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: FilledButton.tonal( + onPressed: onRedeemCoinsTap, + child: const Text('Redeem Coins'), + ), + ), + ], + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: onApplyPromoTap, + child: const Text('Apply Promo'), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/home_today_summary_section.dart b/lib/features/home/presentation/widgets/home_today_summary_section.dart new file mode 100644 index 0000000..e4a2327 --- /dev/null +++ b/lib/features/home/presentation/widgets/home_today_summary_section.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; + +class HomeTodaySummarySection extends StatelessWidget { + const HomeTodaySummarySection({ + super.key, + required this.customersLabel, + required this.earnedCoinsLabel, + required this.redeemedCoinsLabel, + this.onViewDetailsTap, + }); + + final String customersLabel; + final String earnedCoinsLabel; + final String redeemedCoinsLabel; + final VoidCallback? onViewDetailsTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Today Summary', + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + ), + if (onViewDetailsTap != null) + TextButton( + onPressed: onViewDetailsTap, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text('Details'), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _MiniStat( + label: 'Customers', + value: customersLabel, + icon: Icons.person_rounded, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _MiniStat( + label: 'Earned', + value: earnedCoinsLabel, + icon: Icons.south_west_rounded, + iconColor: colorScheme.tertiary, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _MiniStat( + label: 'Redeemed', + value: redeemedCoinsLabel, + icon: Icons.north_east_rounded, + iconColor: colorScheme.error, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _MiniStat extends StatelessWidget { + const _MiniStat({ + required this.label, + required this.value, + required this.icon, + this.iconColor, + }); + + final String label; + final String value; + final IconData icon; + final Color? iconColor; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.25), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.25)), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Icon( + icon, + size: 18, + color: iconColor ?? colorScheme.onPrimaryContainer, + ), + ), + const Spacer(), + ], + ), + const SizedBox(height: 10), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w900), + ), + const SizedBox(height: 2), + Text( + label, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + diff --git a/lib/features/home/presentation/widgets/recent_scan_item_card.dart b/lib/features/home/presentation/widgets/recent_scan_item_card.dart new file mode 100644 index 0000000..ebe6341 --- /dev/null +++ b/lib/features/home/presentation/widgets/recent_scan_item_card.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +class RecentScanItemCard extends StatelessWidget { + const RecentScanItemCard({ + super.key, + required this.title, + this.subtitle, + this.onTap, + this.leadingIcon = Icons.qr_code, + this.leadingIconSize = 22, + this.trailingIcon = Icons.chevron_right_rounded, + this.trailingIconSize = 22, + this.margin = const EdgeInsets.only(bottom: 10), + this.padding = const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + this.borderRadius = 16, + this.showTrailing = true, + this.titleMaxLines = 1, + this.subtitleMaxLines = 1, + }); + + final String title; + final String? subtitle; + final VoidCallback? onTap; + + final IconData leadingIcon; + final double leadingIconSize; + + final IconData trailingIcon; + final double trailingIconSize; + + final EdgeInsets margin; + final EdgeInsets padding; + final double borderRadius; + final bool showTrailing; + final int titleMaxLines; + final int subtitleMaxLines; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return Padding( + padding: margin, + child: Material( + color: Colors.transparent, + child: Ink( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.5), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onTap, + child: Padding( + padding: padding, + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Icon( + leadingIcon, + size: leadingIconSize, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: titleMaxLines, + overflow: TextOverflow.ellipsis, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + maxLines: subtitleMaxLines, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + if (showTrailing) ...[ + const SizedBox(width: 8), + Icon( + trailingIcon, + size: trailingIconSize, + color: colorScheme.onSurfaceVariant, + ), + ], + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/injection_container.dart b/lib/features/injection_container.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source.dart b/lib/features/scan/data/data_sources/scan_remote_data_source.dart new file mode 100644 index 0000000..3ffed98 --- /dev/null +++ b/lib/features/scan/data/data_sources/scan_remote_data_source.dart @@ -0,0 +1,6 @@ +abstract interface class ScanRemoteDataSource { + Future> submitScan({ + required String rawValue, + }); +} + diff --git a/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart new file mode 100644 index 0000000..b3ac074 --- /dev/null +++ b/lib/features/scan/data/data_sources/scan_remote_data_source_fake.dart @@ -0,0 +1,14 @@ +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; + +class FakeScanRemoteDataSource implements ScanRemoteDataSource { + @override + Future> submitScan({ + required String rawValue, + }) async { + return { + 'rawValue': rawValue, + 'scannedAt': DateTime.now().toIso8601String(), + }; + } +} + diff --git a/lib/features/scan/data/models/scanned_qr_model.dart b/lib/features/scan/data/models/scanned_qr_model.dart new file mode 100644 index 0000000..d3c0c2f --- /dev/null +++ b/lib/features/scan/data/models/scanned_qr_model.dart @@ -0,0 +1,30 @@ +import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; + +class ScannedQrModel { + const ScannedQrModel({ + required this.rawValue, + required this.scannedAt, + }); + + factory ScannedQrModel.fromJson(Map json) { + return ScannedQrModel( + rawValue: json['rawValue'] as String, + scannedAt: DateTime.parse(json['scannedAt'] as String), + ); + } + + final String rawValue; + final DateTime scannedAt; + + Map toJson() { + return { + 'rawValue': rawValue, + 'scannedAt': scannedAt.toIso8601String(), + }; + } + + ScannedQr toEntity() { + return ScannedQr(rawValue: rawValue, scannedAt: scannedAt); + } +} + diff --git a/lib/features/scan/data/repositories/scan_repository_impl.dart b/lib/features/scan/data/repositories/scan_repository_impl.dart new file mode 100644 index 0000000..5a41875 --- /dev/null +++ b/lib/features/scan/data/repositories/scan_repository_impl.dart @@ -0,0 +1,17 @@ +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/scan/data/models/scanned_qr_model.dart'; +import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; +import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; + +class ScanRepositoryImpl implements ScanRepository { + const ScanRepositoryImpl(this._remoteDataSource); + + final ScanRemoteDataSource _remoteDataSource; + + @override + Future processRawValue(String rawValue) async { + final payload = await _remoteDataSource.submitScan(rawValue: rawValue); + return ScannedQrModel.fromJson(payload).toEntity(); + } +} + diff --git a/lib/features/scan/domain/entities/scanned_qr.dart b/lib/features/scan/domain/entities/scanned_qr.dart new file mode 100644 index 0000000..330114f --- /dev/null +++ b/lib/features/scan/domain/entities/scanned_qr.dart @@ -0,0 +1,10 @@ +class ScannedQr { + const ScannedQr({ + required this.rawValue, + required this.scannedAt, + }); + + final String rawValue; + final DateTime scannedAt; +} + diff --git a/lib/features/scan/domain/repositories/scan_repository.dart b/lib/features/scan/domain/repositories/scan_repository.dart new file mode 100644 index 0000000..bc4d8e0 --- /dev/null +++ b/lib/features/scan/domain/repositories/scan_repository.dart @@ -0,0 +1,6 @@ +import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; + +abstract interface class ScanRepository { + Future processRawValue(String rawValue); +} + diff --git a/lib/features/scan/domain/use_cases/process_scan_use_case.dart b/lib/features/scan/domain/use_cases/process_scan_use_case.dart new file mode 100644 index 0000000..60683a7 --- /dev/null +++ b/lib/features/scan/domain/use_cases/process_scan_use_case.dart @@ -0,0 +1,13 @@ +import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart'; +import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; + +class ProcessScanUseCase { + const ProcessScanUseCase(this._repository); + + final ScanRepository _repository; + + Future call(String rawValue) { + return _repository.processRawValue(rawValue); + } +} + diff --git a/lib/features/scan/presentation/manager/scan_controller.dart b/lib/features/scan/presentation/manager/scan_controller.dart new file mode 100644 index 0000000..aaae2ea --- /dev/null +++ b/lib/features/scan/presentation/manager/scan_controller.dart @@ -0,0 +1,70 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:cb_prestige_qr/features/scan/scan_providers.dart'; + +part 'scan_controller.g.dart'; + +class ScanState { + const ScanState({ + this.isScanning = true, + this.isProcessing = false, + this.scannedValue, + this.errorMessage, + }); + + final bool isScanning; + final bool isProcessing; + final String? scannedValue; + final String? errorMessage; + + static const _sentinel = Object(); + + ScanState copyWith({ + bool? isScanning, + bool? isProcessing, + Object? scannedValue = _sentinel, + Object? errorMessage = _sentinel, + }) { + return ScanState( + isScanning: isScanning ?? this.isScanning, + isProcessing: isProcessing ?? this.isProcessing, + scannedValue: + identical(scannedValue, _sentinel) ? this.scannedValue : scannedValue as String?, + errorMessage: + identical(errorMessage, _sentinel) ? this.errorMessage : errorMessage as String?, + ); + } +} + +@riverpod +class ScanController extends _$ScanController { + @override + ScanState build() => const ScanState(); + + Future onBarcodeDetected(String value) async { + if (!state.isScanning || state.isProcessing) return; + state = state.copyWith( + isScanning: false, + isProcessing: true, + errorMessage: null, + ); + + try { + final result = await ref.read(processScanUseCaseProvider)(value); + state = state.copyWith( + isProcessing: false, + scannedValue: result.rawValue, + ); + } catch (e) { + state = state.copyWith( + isProcessing: false, + isScanning: true, + scannedValue: null, + errorMessage: e.toString(), + ); + } + } + + void resumeScanning() { + state = const ScanState(isScanning: true, isProcessing: false, scannedValue: null, errorMessage: null); + } +} diff --git a/lib/features/scan/presentation/manager/scan_controller.g.dart b/lib/features/scan/presentation/manager/scan_controller.g.dart new file mode 100644 index 0000000..c8a504e --- /dev/null +++ b/lib/features/scan/presentation/manager/scan_controller.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scan_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ScanController) +final scanControllerProvider = ScanControllerProvider._(); + +final class ScanControllerProvider + extends $NotifierProvider { + ScanControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'scanControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$scanControllerHash(); + + @$internal + @override + ScanController create() => ScanController(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ScanState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$scanControllerHash() => r'5984074ff43ae1904e2025b355a689580d402a09'; + +abstract class _$ScanController extends $Notifier { + ScanState build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ScanState, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/scan/presentation/pages/scan_page.dart b/lib/features/scan/presentation/pages/scan_page.dart new file mode 100644 index 0000000..88a7463 --- /dev/null +++ b/lib/features/scan/presentation/pages/scan_page.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:cb_prestige_qr/core/presentation/widgets/start_loading_overlay.dart'; +import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class ScanPage extends ConsumerStatefulWidget { + const ScanPage({super.key}); + + @override + ConsumerState createState() => _ScanPageState(); +} + +class _ScanPageState extends ConsumerState { + late final MobileScannerController _scannerController; + + @override + void initState() { + super.initState(); + _scannerController = MobileScannerController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ref.read(scanControllerProvider.notifier).resumeScanning(); + unawaited(_scannerController.start()); + }); + } + + @override + void dispose() { + _scannerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ref.listen(scanControllerProvider.select((s) => s.isScanning), ( + previous, + next, + ) { + if (previous == next) return; + if (next) { + unawaited(_scannerController.start()); + } else { + unawaited(_scannerController.stop()); + } + }); + + final scanState = ref.watch(scanControllerProvider); + + void onDetect(BarcodeCapture capture) { + if (!scanState.isScanning) return; + final value = capture.barcodes.isEmpty + ? null + : capture.barcodes.first.rawValue; + if (value == null || value.isEmpty) return; + + unawaited( + ref.read(scanControllerProvider.notifier).onBarcodeDetected(value), + ); + } + + return StartLoadingOverlay( + child: Scaffold( + appBar: AppBar(title: const Text('Scan QR')), + body: Stack( + children: [ + MobileScanner(controller: _scannerController, onDetect: onDetect), + Align( + alignment: Alignment.center, + child: Container( + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 3), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + if (scanState.isProcessing) + const Positioned.fill( + child: ColoredBox( + color: Color(0x66000000), + child: Center(child: CircularProgressIndicator()), + ), + ), + if (!scanState.isProcessing && + (scanState.scannedValue != null || + scanState.errorMessage != null)) + Positioned.fill( + child: ColoredBox( + color: const Color(0x66000000), + child: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Padding( + padding: const EdgeInsets.all(16), + child: Material( + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + scanState.errorMessage == null + ? 'Scanned Result' + : 'Scan Error', + style: Theme.of( + context, + ).textTheme.titleLarge, + ), + const SizedBox(height: 12), + SelectableText( + scanState.errorMessage ?? + scanState.scannedValue ?? + '', + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + ref + .read( + scanControllerProvider + .notifier, + ) + .resumeScanning(); + }, + child: const Text('Scan Again'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Back'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/scan/scan_providers.dart b/lib/features/scan/scan_providers.dart new file mode 100644 index 0000000..dd05067 --- /dev/null +++ b/lib/features/scan/scan_providers.dart @@ -0,0 +1,24 @@ +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source.dart'; +import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_source_fake.dart'; +import 'package:cb_prestige_qr/features/scan/data/repositories/scan_repository_impl.dart'; +import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart'; +import 'package:cb_prestige_qr/features/scan/domain/use_cases/process_scan_use_case.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'scan_providers.g.dart'; + + +@riverpod +ScanRemoteDataSource scanRemoteDataSource(Ref ref) { + return FakeScanRemoteDataSource(); +} + +@riverpod +ScanRepository scanRepository(Ref ref) { + return ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider)); +} + +@riverpod +ProcessScanUseCase processScanUseCase(Ref ref) { + return ProcessScanUseCase(ref.watch(scanRepositoryProvider)); +} + diff --git a/lib/features/scan/scan_providers.g.dart b/lib/features/scan/scan_providers.g.dart new file mode 100644 index 0000000..cca690e --- /dev/null +++ b/lib/features/scan/scan_providers.g.dart @@ -0,0 +1,147 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scan_providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(scanRemoteDataSource) +final scanRemoteDataSourceProvider = ScanRemoteDataSourceProvider._(); + +final class ScanRemoteDataSourceProvider + extends + $FunctionalProvider< + ScanRemoteDataSource, + ScanRemoteDataSource, + ScanRemoteDataSource + > + with $Provider { + ScanRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'scanRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$scanRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + ScanRemoteDataSource create(Ref ref) { + return scanRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ScanRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$scanRemoteDataSourceHash() => + r'f25d69a350b64c4dd0cf8ef124838bb351b5e4a1'; + +@ProviderFor(scanRepository) +final scanRepositoryProvider = ScanRepositoryProvider._(); + +final class ScanRepositoryProvider + extends $FunctionalProvider + with $Provider { + ScanRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'scanRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$scanRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + ScanRepository create(Ref ref) { + return scanRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ScanRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$scanRepositoryHash() => r'3e719b962dbb1014143710238128234371ef141e'; + +@ProviderFor(processScanUseCase) +final processScanUseCaseProvider = ProcessScanUseCaseProvider._(); + +final class ProcessScanUseCaseProvider + extends + $FunctionalProvider< + ProcessScanUseCase, + ProcessScanUseCase, + ProcessScanUseCase + > + with $Provider { + ProcessScanUseCaseProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'processScanUseCaseProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$processScanUseCaseHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + ProcessScanUseCase create(Ref ref) { + return processScanUseCase(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ProcessScanUseCase value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$processScanUseCaseHash() => + r'ce9f6a3d0de27fe0f7f8e7fa1ac9c1f88ad57e43'; diff --git a/lib/features/settings/data/data_sources/settings_local_data_source.dart b/lib/features/settings/data/data_sources/settings_local_data_source.dart new file mode 100644 index 0000000..af896ea --- /dev/null +++ b/lib/features/settings/data/data_sources/settings_local_data_source.dart @@ -0,0 +1,33 @@ +import '../../domain/entities/settings_content.dart'; + +abstract class SettingsLocalDataSource { + SettingsContent getSettings(); + void setNotificationsEnabled(bool value); + void setHapticsEnabled(bool value); +} + +class SettingsLocalDataSourceImpl implements SettingsLocalDataSource { + SettingsLocalDataSourceImpl(); + + bool _notificationsEnabled = true; + bool _hapticsEnabled = true; + + @override + SettingsContent getSettings() { + return SettingsContent( + notificationsEnabled: _notificationsEnabled, + hapticsEnabled: _hapticsEnabled, + appVersionLabel: 'v1.0.0', + ); + } + + @override + void setNotificationsEnabled(bool value) { + _notificationsEnabled = value; + } + + @override + void setHapticsEnabled(bool value) { + _hapticsEnabled = value; + } +} diff --git a/lib/features/settings/data/repositories/settings_repository_impl.dart b/lib/features/settings/data/repositories/settings_repository_impl.dart new file mode 100644 index 0000000..7b8053c --- /dev/null +++ b/lib/features/settings/data/repositories/settings_repository_impl.dart @@ -0,0 +1,22 @@ +import '../../domain/entities/settings_content.dart'; +import '../../domain/repositories/settings_repository.dart'; +import '../data_sources/settings_local_data_source.dart'; + +class SettingsRepositoryImpl implements SettingsRepository { + const SettingsRepositoryImpl(this._localDataSource); + + final SettingsLocalDataSource _localDataSource; + + @override + Future getSettings() async => _localDataSource.getSettings(); + + @override + Future setNotificationsEnabled(bool value) async { + _localDataSource.setNotificationsEnabled(value); + } + + @override + Future setHapticsEnabled(bool value) async { + _localDataSource.setHapticsEnabled(value); + } +} diff --git a/lib/features/settings/domain/entities/settings_content.dart b/lib/features/settings/domain/entities/settings_content.dart new file mode 100644 index 0000000..3633e12 --- /dev/null +++ b/lib/features/settings/domain/entities/settings_content.dart @@ -0,0 +1,11 @@ +class SettingsContent { + const SettingsContent({ + required this.notificationsEnabled, + required this.hapticsEnabled, + required this.appVersionLabel, + }); + + final bool notificationsEnabled; + final bool hapticsEnabled; + final String appVersionLabel; +} diff --git a/lib/features/settings/domain/repositories/settings_repository.dart b/lib/features/settings/domain/repositories/settings_repository.dart new file mode 100644 index 0000000..e08579e --- /dev/null +++ b/lib/features/settings/domain/repositories/settings_repository.dart @@ -0,0 +1,7 @@ +import '../entities/settings_content.dart'; + +abstract class SettingsRepository { + Future getSettings(); + Future setNotificationsEnabled(bool value); + Future setHapticsEnabled(bool value); +} diff --git a/lib/features/settings/domain/use_cases/get_settings.dart b/lib/features/settings/domain/use_cases/get_settings.dart new file mode 100644 index 0000000..bf9ba5d --- /dev/null +++ b/lib/features/settings/domain/use_cases/get_settings.dart @@ -0,0 +1,10 @@ +import '../entities/settings_content.dart'; +import '../repositories/settings_repository.dart'; + +class GetSettings { + const GetSettings(this._repository); + + final SettingsRepository _repository; + + Future call() => _repository.getSettings(); +} diff --git a/lib/features/settings/domain/use_cases/set_haptics_enabled.dart b/lib/features/settings/domain/use_cases/set_haptics_enabled.dart new file mode 100644 index 0000000..68be02b --- /dev/null +++ b/lib/features/settings/domain/use_cases/set_haptics_enabled.dart @@ -0,0 +1,9 @@ +import '../repositories/settings_repository.dart'; + +class SetHapticsEnabled { + const SetHapticsEnabled(this._repository); + + final SettingsRepository _repository; + + Future call(bool value) => _repository.setHapticsEnabled(value); +} diff --git a/lib/features/settings/domain/use_cases/set_notifications_enabled.dart b/lib/features/settings/domain/use_cases/set_notifications_enabled.dart new file mode 100644 index 0000000..ab6752b --- /dev/null +++ b/lib/features/settings/domain/use_cases/set_notifications_enabled.dart @@ -0,0 +1,9 @@ +import '../repositories/settings_repository.dart'; + +class SetNotificationsEnabled { + const SetNotificationsEnabled(this._repository); + + final SettingsRepository _repository; + + Future call(bool value) => _repository.setNotificationsEnabled(value); +} diff --git a/lib/features/settings/presentation/manager/settings_ui_state.dart b/lib/features/settings/presentation/manager/settings_ui_state.dart new file mode 100644 index 0000000..c72dde4 --- /dev/null +++ b/lib/features/settings/presentation/manager/settings_ui_state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class SettingsUiState { + const SettingsUiState({ + required this.notificationsEnabled, + required this.hapticsEnabled, + required this.appVersionLabel, + }); + + final bool notificationsEnabled; + final bool hapticsEnabled; + final String appVersionLabel; + + SettingsUiState copyWith({ + bool? notificationsEnabled, + bool? hapticsEnabled, + String? appVersionLabel, + }) { + return SettingsUiState( + notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, + hapticsEnabled: hapticsEnabled ?? this.hapticsEnabled, + appVersionLabel: appVersionLabel ?? this.appVersionLabel, + ); + } +} diff --git a/lib/features/settings/presentation/manager/settings_view_model.dart b/lib/features/settings/presentation/manager/settings_view_model.dart new file mode 100644 index 0000000..cadedbd --- /dev/null +++ b/lib/features/settings/presentation/manager/settings_view_model.dart @@ -0,0 +1,61 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../data/data_sources/settings_local_data_source.dart'; +import '../../data/repositories/settings_repository_impl.dart'; +import '../../domain/entities/settings_content.dart'; +import '../../domain/repositories/settings_repository.dart'; +import '../../domain/use_cases/get_settings.dart'; +import '../../domain/use_cases/set_haptics_enabled.dart'; +import '../../domain/use_cases/set_notifications_enabled.dart'; +import 'settings_ui_state.dart'; + +final _settingsLocalDataSourceProvider = Provider( + (ref) => SettingsLocalDataSourceImpl(), +); + +final _settingsRepositoryProvider = Provider( + (ref) => SettingsRepositoryImpl(ref.watch(_settingsLocalDataSourceProvider)), +); + +final _getSettingsProvider = Provider( + (ref) => GetSettings(ref.watch(_settingsRepositoryProvider)), +); + +final _setNotificationsEnabledProvider = Provider( + (ref) => SetNotificationsEnabled(ref.watch(_settingsRepositoryProvider)), +); + +final _setHapticsEnabledProvider = Provider( + (ref) => SetHapticsEnabled(ref.watch(_settingsRepositoryProvider)), +); + +final settingsViewModelProvider = + AsyncNotifierProvider( + SettingsViewModel.new, + ); + +class SettingsViewModel extends AsyncNotifier { + @override + Future build() async { + final content = await ref.watch(_getSettingsProvider)(); + return _mapContentToUiState(content); + } + + SettingsUiState _mapContentToUiState(SettingsContent content) { + return SettingsUiState( + notificationsEnabled: content.notificationsEnabled, + hapticsEnabled: content.hapticsEnabled, + appVersionLabel: content.appVersionLabel, + ); + } + + Future toggleNotifications(bool value) async { + await ref.watch(_setNotificationsEnabledProvider)(value); + state = state.whenData((s) => s.copyWith(notificationsEnabled: value)); + } + + Future toggleHaptics(bool value) async { + await ref.watch(_setHapticsEnabledProvider)(value); + state = state.whenData((s) => s.copyWith(hapticsEnabled: value)); + } +} diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart new file mode 100644 index 0000000..f0c333a --- /dev/null +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../manager/settings_ui_state.dart'; +import '../manager/settings_view_model.dart'; + +class SettingsPage extends ConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final viewModel = ref.read(settingsViewModelProvider.notifier); + final stateAsync = ref.watch(settingsViewModelProvider); + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Settings'), + backgroundColor: theme.scaffoldBackgroundColor, + surfaceTintColor: theme.scaffoldBackgroundColor, + elevation: 0, + scrolledUnderElevation: 0, + ), + body: stateAsync.when( + data: (state) => _SettingsBody( + state: state, + onNotificationsChanged: viewModel.toggleNotifications, + onHapticsChanged: viewModel.toggleHaptics, + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( + child: TextButton( + onPressed: () => ref.invalidate(settingsViewModelProvider), + child: const Text('Retry'), + ), + ), + ), + ); + } +} + +class _SettingsBody extends StatelessWidget { + const _SettingsBody({ + required this.state, + required this.onNotificationsChanged, + required this.onHapticsChanged, + }); + + final SettingsUiState state; + final ValueChanged onNotificationsChanged; + final ValueChanged onHapticsChanged; + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + _SectionCard( + title: 'Preferences', + children: [ + SwitchListTile.adaptive( + value: state.notificationsEnabled, + onChanged: onNotificationsChanged, + title: const Text('Notifications'), + subtitle: const Text('Get updates about scans'), + ), + const Divider(height: 1), + SwitchListTile.adaptive( + value: state.hapticsEnabled, + onChanged: onHapticsChanged, + title: const Text('Haptics'), + subtitle: const Text('Vibration feedback'), + ), + ], + ), + const SizedBox(height: 12), + _SectionCard( + title: 'About', + children: [ + ListTile( + title: const Text('Version'), + subtitle: Text(state.appVersionLabel), + ), + ], + ), + ], + ); + } +} + +class _SectionCard extends StatelessWidget { + const _SectionCard({required this.title, required this.children}); + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 8), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), + ), + ), + ...children, + ], + ), + ); + } +} diff --git a/lib/features/splash/presentation/pages/splash_screen.dart b/lib/features/splash/presentation/pages/splash_screen.dart new file mode 100644 index 0000000..ec963b8 --- /dev/null +++ b/lib/features/splash/presentation/pages/splash_screen.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:flutter/material.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({ + super.key, + this.duration = const Duration(seconds: 2), + this.imageAssetPath = 'assets/images/splash_1.png', + }); + + final Duration duration; + final String imageAssetPath; + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + Timer? _timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _timer = Timer(widget.duration, () { + if (!mounted) return; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const MainShell()), + ); + }); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SizedBox.expand( + child: Image.asset( + widget.imageAssetPath, + fit: BoxFit.cover, + ), + ), + ); + } +} + diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..490cfe0 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,34 @@ +import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'CB Prestige Banking', + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xff896e4b), + brightness: Brightness.dark, + ), + scaffoldBackgroundColor: const Color(0xff25262b), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xff896e4b), + foregroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + ), + ), + home: const MainShell(), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..e3bb824 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,858 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + analyzer_buffer: + dependency: transitive + description: + name: analyzer_buffer + sha256: ff4bd291778c7417fe53fe24ee0d0a1f1ffe281a2d4ea887e7094f16e36eace7 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c + url: "https://pub.dev" + source: hosted + version: "4.0.5" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" + url: "https://pub.dev" + source: hosted + version: "2.13.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + 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" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42" + url: "https://pub.dev" + source: hosted + version: "0.21.3+1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_svg: + dependency: "direct dev" + description: + name: flutter_svg + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + 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" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "08527ec06aaef75e4b78694e045ef0cd8346594eaf9cc18b0f866398b07b93b1" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + 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" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.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" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce + url: "https://pub.dev" + source: hosted + version: "7.2.0" + mockito: + dependency: transitive + description: + name: mockito + sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 + url: "https://pub.dev" + source: hosted + version: "5.6.3" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.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" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 + url: "https://pub.dev" + source: hosted + version: "1.0.0-dev.9" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "16471a1260b94e939394d78f1c63a9350936ac4a68c9fbdab40be47268c0b04f" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "6f9220534d7a353b53c875ea191a84d28cb4e52ac420a66a1bd7318329d977b0" + url: "https://pub.dev" + source: hosted + version: "4.0.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" + url: "https://pub.dev" + source: hosted + version: "4.2.2" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + 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" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + 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: + dependency: transitive + description: + name: test + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + url: "https://pub.dev" + source: hosted + version: "1.30.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + url: "https://pub.dev" + source: hosted + version: "0.6.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" + url: "https://pub.dev" + source: hosted + version: "1.1.20" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.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" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + 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.11.3 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..054846f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,112 @@ +name: cb_prestige_qr +description: "A new Flutter project." +# 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.11.3 + +# 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 + hooks_riverpod: ^3.3.1 + flutter_hooks: ^0.21.3+1 + riverpod_annotation: ^4.0.2 + carousel_slider: ^5.1.2 + mobile_scanner: ^7.2.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 + riverpod_generator: ^4.0.3 + build_runner: ^2.13.1 + flutter_svg: ^2.2.4 + flutter_native_splash: 2.4.7 + + +# 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 + assets: + - assets/images/ + - assets/svg/ + +flutter_native_splash: + color: "#25262B" + image: assets/images/splash.png + android: true + ios: true + + android_12: + color: "#25262B" + image: assets/images/splash.png + + # To add assets to your application, add an assets section, like this: + + # 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 new file mode 100644 index 0000000..1e52a14 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cb_prestige_qr/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}