initial commit

main
tahaalam 1 week ago
commit a2be510e2e

45
.gitignore vendored

@ -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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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

@ -0,0 +1,42 @@
# 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: "c519ee916eaeb88923e67befb89c0f1dabfa83e6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: android
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: ios
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: linux
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: macos
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
- platform: web
create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6
# 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'

@ -0,0 +1,16 @@
# finger_print_verification
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:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

@ -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.cloudsolutions.finger_print_verification"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.cloudsolutions.finger_print_verification"
// 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.debug
}
}
}
flutter {
source = "../.."
}

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -0,0 +1,63 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.hardware.usb.host" />
<application
android:label="finger_print_verification"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

@ -0,0 +1,5 @@
package com.cloudsolutions.finger_print_verification
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device vendor-id="6997" product-id="289" />
</resources>

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return 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.2.1" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

34
ios/.gitignore vendored

@ -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

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

@ -0,0 +1 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"

@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

@ -0,0 +1,619 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 = 12.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)";
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.fingerPrintVerification;
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.cloudsolutions.fingerPrintVerification.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.cloudsolutions.fingerPrintVerification.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.cloudsolutions.fingerPrintVerification.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 = 12.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 = 12.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)";
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.fingerPrintVerification;
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)";
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.fingerPrintVerification;
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 */;
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@ -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.

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Finger Print Verification</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>finger_print_verification</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

@ -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.
}
}

@ -0,0 +1,571 @@
// ignore_for_file: avoid_catches_without_on_clauses
import 'dart:developer';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'api_response.dart';
import 'http_method.dart';
import 'token_manager.dart';
// Custom exception
/// Thrown **synchronously** when any [ApiClient] call method is invoked before
/// [ApiClient.initialize] has been called.
///
/// This is an exception (not a silent [ApiFailure]) because it represents a
/// **programming error** the developer forgot to call [ApiClient.initialize]
/// in `main()`. It should crash loudly in development so it is never shipped.
final class ApiClientNotInitializedException implements Exception {
const ApiClientNotInitializedException();
@override
String toString() =>
'ApiClientNotInitializedException: '
'ApiClient.initialize() must be called before making any API calls. '
'Add `ApiClient.initialize(baseUrl: …)` to your main() function.';
}
// ApiClient
/// A thin, zero-boilerplate HTTP wrapper around [Dio] for Flutter apps.
///
/// ### Quick start
/// ```dart
/// // 1. Once in main():
/// ApiClient.initialize(baseUrl: 'https://api.example.com');
///
/// // 2. Make a call:
/// final result = await ApiClient.get<User>(
/// url: '/users/me',
/// shouldKeepToken: true,
/// fromJson: (j) => User.fromJson(j as Map<String, dynamic>),
/// );
/// switch (result) {
/// case ApiSuccess(:final data): print(data.name);
/// case ApiFailure(:final message): print('Error: $message');
/// }
///
/// // 3. Cancel all in-flight requests (e.g. from a BLoC's close()):
/// ApiClient.cancel();
/// ```
///
/// ---
/// ### Design decisions
///
/// **Named methods vs. a single `call(HttpMethod, )`**
/// Separate named methods (`get`, `post`, `put`, `patch`, `delete`) are
/// preferred because:
/// 1. Call sites are instantly readable `ApiClient.post(url: )` vs
/// `ApiClient.call(HttpMethod.post, url: )`.
/// 2. Each method can omit inapplicable parameters (e.g. no `body` on `get`).
/// 3. IDE auto-complete surfaces the right signature without guessing.
/// All named methods delegate to the private [_call] dispatcher so there is
/// zero logic duplication.
///
/// **URL resolution**
/// If [url] already starts with `http://` or `https://`, Dio uses it **as-is**
/// (absolute URL) the configured [BaseOptions.baseUrl] is _not_ prepended.
/// For relative paths, Dio concatenates `baseUrl + url` automatically.
/// This matches browser / fetch / axios semantics and prevents accidentally
/// double-prepending the base when the caller passes a full URL for a
/// third-party endpoint.
class ApiClient {
ApiClient._();
static Dio? _dio;
static bool _isInitialized = false;
static bool Function(Response<dynamic>)? _globalIsSuccess;
/// Shared cancel token for all in-flight requests.
///
/// After [cancel] is invoked, a fresh [CancelToken] is created immediately
/// so subsequent calls are never pre-cancelled the client stays usable.
///
/// ### Extension point for per-request cancellation
/// To layer per-request cancellation on in the future, accept an optional
/// `String? requestTag` in every call method and maintain a
/// `static final Map<String, CancelToken> _taggedTokens = {}` registry
/// inside [_call]. No structural rewrite of the existing code is needed.
static CancelToken _cancelToken = CancelToken();
// Initialization
/// Configure the global [Dio] instance. **Call exactly once at app startup,
/// before any API call is made.**
///
/// Calling [initialize] again replaces the existing instance (useful in
/// tests that need different base URLs or interceptors).
///
/// Parameters:
/// - [baseUrl] prepended to every relative URL.
/// - [headers] merged on top of the default `Content-Type` /
/// `Accept` headers for every request.
/// - [connectTimeout] how long to wait for a connection; default 30 s.
/// - [receiveTimeout] how long to wait for a full response; default 30 s.
/// - [isSuccess] global predicate that decides whether a [Response]
/// is successful. Falls back to HTTP 200299 when
/// `null`.
/// - [interceptors] extra [Interceptor]s (logging, auth-refresh, etc.)
/// added to the [Dio] instance.
static void initialize({
required String baseUrl,
Map<String, dynamic>? headers,
Duration connectTimeout = const Duration(seconds: 30),
Duration receiveTimeout = const Duration(seconds: 30),
bool Function(Response<dynamic>)? isSuccess,
List<Interceptor> interceptors = const [],
}) {
_globalIsSuccess = isSuccess;
_isInitialized = false; // guard against partially-constructed state
_dio = Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: <String, dynamic>{
'Content-Type': 'application/json',
'Accept': 'application/json',
...?headers,
},
),
);
if (interceptors.isNotEmpty) {
_dio!.interceptors.addAll(interceptors);
}
_isInitialized = true;
}
// Cancellation
/// Cancel **all** currently in-flight requests that were made through
/// [ApiClient].
///
/// A fresh [CancelToken] is created immediately afterward so new requests
/// are never affected the client remains fully usable after cancellation.
///
/// Cancelled requests resolve as [ApiFailure] with `code: -1` and the
/// provided [reason] as their [ApiFailure.message].
///
/// Typical usage inside a BLoC's `close()`:
/// ```dart
/// @override
/// Future<void> close() {
/// ApiClient.cancel();
/// return super.close();
/// }
/// ```
static void cancel({String reason = 'Request was cancelled'}) {
_cancelToken.cancel(reason);
// Regenerate immediately so subsequent requests are never pre-cancelled.
_cancelToken = CancelToken();
}
// Public call methods
/// Perform an HTTP **GET** request.
///
/// - [url] absolute (`https://`) or relative path resolved
/// against [baseUrl].
/// - [queryParams] appended as URL query parameters.
/// - [headers] per-call headers merged on top of defaults.
/// - [shouldKeepToken] when `true`, the stored [TokenManager] token is
/// attached as `Authorization: Bearer <token>`, and any
/// `Authorization` header returned in the response is
/// persisted back to [TokenManager].
/// - [isSuccess] per-call predicate that overrides the global default
/// set in [initialize] for this single call.
/// - [fromJson] converts the successful response body to [T].
///
/// Returns [ApiSuccess<T>] on success or [ApiFailure<T>] on any error.
/// **Never throws** (except [ApiClientNotInitializedException] if
/// [initialize] was never called this is a programming error).
static Future<ApiResponse<T>> get<T>({
required String url,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
bool shouldKeepToken = false,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) =>
_call(
method: HttpMethod.get,
url: url,
queryParams: queryParams,
headers: headers,
shouldKeepToken: shouldKeepToken,
isSuccess: isSuccess,
fromJson: fromJson,
);
/// Perform an HTTP **POST** request.
///
/// - [body] request body; Dio serialises `Map` / `List` to JSON
/// automatically. Pass a [FormData] for multipart uploads.
static Future<ApiResponse<T>> post<T>({
required String url,
dynamic body,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
bool shouldKeepToken = false,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) =>
_call(
method: HttpMethod.post,
url: url,
body: body,
queryParams: queryParams,
headers: headers,
shouldKeepToken: shouldKeepToken,
isSuccess: isSuccess,
fromJson: fromJson,
);
/// Perform an HTTP **PUT** request.
///
/// - [body] request body (serialised to JSON by Dio).
static Future<ApiResponse<T>> put<T>({
required String url,
dynamic body,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
bool shouldKeepToken = false,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) =>
_call(
method: HttpMethod.put,
url: url,
body: body,
queryParams: queryParams,
headers: headers,
shouldKeepToken: shouldKeepToken,
isSuccess: isSuccess,
fromJson: fromJson,
);
/// Perform an HTTP **PATCH** request.
///
/// - [body] request body (serialised to JSON by Dio).
static Future<ApiResponse<T>> patch<T>({
required String url,
dynamic body,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
bool shouldKeepToken = false,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) =>
_call(
method: HttpMethod.patch,
url: url,
body: body,
queryParams: queryParams,
headers: headers,
shouldKeepToken: shouldKeepToken,
isSuccess: isSuccess,
fromJson: fromJson,
);
/// Perform an HTTP **DELETE** request.
///
/// - [body] optional request body (serialised to JSON by Dio).
static Future<ApiResponse<T>> delete<T>({
required String url,
dynamic body,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
bool shouldKeepToken = false,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) =>
_call(
method: HttpMethod.delete,
url: url,
body: body,
queryParams: queryParams,
headers: headers,
shouldKeepToken: shouldKeepToken,
isSuccess: isSuccess,
fromJson: fromJson,
);
// Core dispatcher
/// Unified internal dispatcher every public method delegates here.
///
/// Execution pipeline (in order):
/// 1. **Guard** throw [ApiClientNotInitializedException] if [initialize]
/// was never called.
/// 2. **Connectivity** return [ApiFailure] immediately if offline.
/// 3. **Token injection** if [shouldKeepToken] is `true` and
/// [TokenManager.hasToken], add `Authorization: Bearer ` to headers.
/// 4. **Execute** fire the Dio call with the shared [_cancelToken].
/// 5. **Token extraction** if [shouldKeepToken] is `true` and the response
/// contains an `Authorization` header, persist the new token.
/// 6. **Success predicate** per-call global HTTP 200299 fallback.
/// 7. **Parse** call [fromJson] wrap in [ApiSuccess].
/// 8. **Error mapping** catch [DioException] and any other exception and
/// return an appropriate [ApiFailure].
static Future<ApiResponse<T>> _call<T>({
required HttpMethod method,
required String url,
dynamic body,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? headers,
required bool shouldKeepToken,
bool Function(Response<dynamic>)? isSuccess,
required T Function(dynamic json) fromJson,
}) async {
// 1. Guard
if (!_isInitialized || _dio == null) {
throw const ApiClientNotInitializedException();
}
// 2. Connectivity check
// We wrap this in its own try/catch: if the connectivity plugin itself
// throws (e.g. on simulator/unknown platform), we proceed and let the
// actual network error surface naturally rather than blocking the call.
try {
final results = await Connectivity().checkConnectivity();
final isOffline = results.isEmpty ||
results.every((r) => r == ConnectivityResult.none);
if (isOffline) {
return ApiFailure<T>(
message:
'No internet connection. Please check your network settings.',
);
}
} catch (_) {
// Connectivity check failed proceed optimistically.
}
// 3. Build headers + optional Bearer token
final mergedHeaders = <String, dynamic>{...?headers};
if (shouldKeepToken && TokenManager.hasToken) {
mergedHeaders['Authorization'] = 'Bearer ${TokenManager.getToken()}';
}
final options = Options(
headers: mergedHeaders.isNotEmpty ? mergedHeaders : null,
);
// 4. Execute Dio call
try {
late final Response<dynamic> response;
switch (method) {
case HttpMethod.get:
response = await _dio!.get(
url,
queryParameters: queryParams,
options: options,
cancelToken: _cancelToken,
);
case HttpMethod.post:
response = await _dio!.post(
url,
data: body,
queryParameters: queryParams,
options: options,
cancelToken: _cancelToken,
);
case HttpMethod.put:
response = await _dio!.put(
url,
data: body,
queryParameters: queryParams,
options: options,
cancelToken: _cancelToken,
);
case HttpMethod.patch:
response = await _dio!.patch(
url,
data: body,
queryParameters: queryParams,
options: options,
cancelToken: _cancelToken,
);
case HttpMethod.delete:
response = await _dio!.delete(
url,
data: body,
queryParameters: queryParams,
options: options,
cancelToken: _cancelToken,
);
}
log("+++++++++++++++++++++++++++");
log("$body");
log("+++++++++++++++++++++++++++");
log("$response");
log("+++++++++++++++++++++++++++");
// 5. Token extraction from response
if (shouldKeepToken) {
// Dio lowercases header names so check both casings for safety.
final raw = response.headers.value('authorization') ??
response.headers.value('Authorization');
if (raw != null && raw.isNotEmpty) {
final token =
raw.startsWith('Bearer ') ? raw.substring(7) : raw;
TokenManager.setToken(token);
}
}
// 6. Success predicate
// Priority order: per-call global HTTP 200299 fallback.
final succeeded = isSuccess != null
? isSuccess(response)
: _globalIsSuccess != null
? _globalIsSuccess!(response)
: (response.statusCode != null &&
response.statusCode! >= 200 &&
response.statusCode! < 300);
if (!succeeded) {
return ApiFailure<T>(
code: response.statusCode,
message: _extractErrorMessage(response.data) ??
'Request failed with status ${response.statusCode}',
);
}
// 7. Parse
try {
return ApiSuccess<T>(fromJson(response.data));
} catch (e) {
return ApiFailure<T>(
code: response.statusCode,
message: 'Response parsing failed: $e',
);
}
} on DioException catch (e) {
return _mapDioException<T>(e);
} catch (e) {
return ApiFailure<T>(message: 'Unexpected error: $e');
}
}
// Helpers
/// Map a [DioException] to the most descriptive [ApiFailure] available.
///
/// Cancellations are identified by `code: -1` so callers can distinguish
/// them from real HTTP failures without string-matching on [ApiFailure.message].
static ApiFailure<T> _mapDioException<T>(DioException e) {
// Cancelled request distinct code so BLoCs can decide not to show
// an error dialog when the user navigated away.
if (e.type == DioExceptionType.cancel) {
return ApiFailure<T>(
code: -1,
message: e.message ?? 'Request was cancelled',
);
}
final code = e.response?.statusCode;
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiFailure<T>(
code: code,
message: 'Connection timed out. Please try again.',
);
case DioExceptionType.connectionError:
return ApiFailure<T>(
code: code,
message: 'Unable to reach the server. Check your connection.',
);
default:
// Try to pull a message out of the error response body first.
final serverMsg =
e.response != null ? _extractErrorMessage(e.response!.data) : null;
return ApiFailure<T>(
code: code,
message: serverMsg ?? e.message ?? 'An unknown error occurred.',
);
}
}
/// Attempt to pull a human-readable error string from a response body.
///
/// Handles the three most common API error patterns:
/// ```json
/// { "message": "" }
/// { "error": "" }
/// { "error": { "message": "" } }
/// ```
static String? _extractErrorMessage(dynamic data) {
if (data is! Map<String, dynamic>) return null;
if (data['message'] is String) return data['message'] as String;
if (data['error'] is String) return data['error'] as String;
if (data['error'] is Map) {
final inner = data['error'] as Map;
if (inner['message'] is String) return inner['message'] as String;
}
return null;
}
}
// Usage example
//
// main.dart
//
// import 'package:your_app/core/network/api_client.dart';
// import 'package:dio/dio.dart';
// import 'package:flutter/material.dart';
//
// void main() {
// // Configure once before runApp().
// ApiClient.initialize(
// baseUrl: 'https://api.example.com',
// connectTimeout: const Duration(seconds: 15),
// receiveTimeout: const Duration(seconds: 15),
// headers: {'X-App-Version': '1.0.0'},
// // Global rule: treat only status 200 as success.
// isSuccess: (response) => response.statusCode == 200,
// interceptors: [LogInterceptor(requestBody: true)],
// );
// runApp(const MyApp());
// }
//
//
// GET with custom isSuccess predicate + shouldKeepToken
//
// Future<void> fetchProfile() async {
// final result = await ApiClient.get<User>(
// url: '/users/me', // relative Dio prepends baseUrl
// shouldKeepToken: true, // adds Authorization: Bearer <token>
// // Per-call override: accept both 200 and 201 for this endpoint.
// isSuccess: (r) => r.statusCode == 200 || r.statusCode == 201,
// fromJson: (j) => User.fromJson(j as Map<String, dynamic>),
// );
//
// switch (result) {
// case ApiSuccess(:final data):
// print('Welcome, ${data.name}');
// case ApiFailure(:final code, :final message):
// print('Error $code: $message');
// }
// }
//
//
// Cancel all in-flight requests (e.g. from a BLoC's close()) ────────────┐
//
// @override
// Future<void> close() {
// ApiClient.cancel(); // safe client stays usable for new calls
// return super.close();
// }
//

@ -0,0 +1,51 @@
/// Sealed response type returned by every [ApiClient] call.
///
/// Use Dart 3 exhaustive pattern-matching so both outcomes are always handled:
/// ```dart
/// final result = await ApiClient.get<User>(
/// url: '/users/me',
/// fromJson: (j) => User.fromJson(j as Map<String, dynamic>),
/// );
///
/// switch (result) {
/// case ApiSuccess(:final data):
/// print('Got user: ${data.name}');
/// case ApiFailure(:final code, :final message):
/// print('Error $code: $message');
/// }
/// ```
sealed class ApiResponse<T> {}
/// Represents a successful API call whose response body was parsed into [data]
/// via the caller-supplied `fromJson` function.
final class ApiSuccess<T> extends ApiResponse<T> {
/// The parsed response payload.
final T data;
ApiSuccess(this.data);
}
/// Represents a failed API call, carrying an optional HTTP status [code] and a
/// human-readable [message].
///
/// Failure sources include:
/// * No internet connectivity `code: null`
/// * Connection / receive timeout `code: null` or server code
/// * HTTP error (non-2xx or `isSuccess` predicate returned `false`) HTTP code
/// * Response JSON parsing error HTTP code
/// * Request cancellation via `ApiClient.cancel()` `code: -1`
/// * Client not initialized (`ApiClientNotInitializedException`) `code: null`
/// * Unknown / unexpected error `code: null`
final class ApiFailure<T> extends ApiResponse<T> {
/// HTTP status code when available, or `null` when the failure happened
/// before / after the network layer (e.g. no connectivity, parsing error).
///
/// A synthetic value of `-1` is used for cancellations so callers can
/// distinguish them from real HTTP errors without string-matching.
final int? code;
/// Human-readable description of the failure suitable for display.
final String message;
ApiFailure({this.code, required this.message});
}

@ -0,0 +1,14 @@
/// HTTP verb enum used internally by [ApiClient._call] to dispatch to the
/// correct [Dio] method.
///
/// The public [ApiClient] API exposes **named methods** (`get`, `post`, )
/// rather than accepting an [HttpMethod] directly. The enum is kept as a
/// clearly-typed pivot inside [ApiClient._call] so all request logic lives in
/// one place with zero duplication.
enum HttpMethod {
get,
post,
put,
patch,
delete,
}

@ -0,0 +1,14 @@
/// Core networking layer.
///
/// Import this single file to access [ApiClient], [ApiResponse],
/// [ApiSuccess], [ApiFailure], [TokenManager], and
/// [ApiClientNotInitializedException].
///
/// ```dart
/// import 'package:finger_print_verification/core/network/network.dart';
/// ```
library;
export 'api_client.dart';
export 'api_response.dart';
export 'http_method.dart';
export 'token_manager.dart';

@ -0,0 +1,41 @@
/// Lightweight **in-memory** authentication token holder.
///
/// The token is **never persisted to disk** it lives only for the duration
/// of the current process. Use [setToken] after a successful login response
/// and [clearToken] on log-out.
///
/// [ApiClient] reads from here automatically whenever `shouldKeepToken: true`
/// is passed to any call method, and updates the stored token when the server
/// returns a new `Authorization` response header.
///
/// Example:
/// ```dart
/// // After a login call:
/// TokenManager.setToken(loginResponse.accessToken);
///
/// // Any subsequent call with shouldKeepToken: true will attach:
/// // Authorization: Bearer <token>
///
/// // On log-out:
/// TokenManager.clearToken();
/// ```
final class TokenManager {
// Pure-static class disallow instantiation.
TokenManager._();
static String? _token;
/// Store an authentication token in memory.
///
/// Pass the **raw token value** (without a `Bearer ` prefix).
static void setToken(String token) => _token = token;
/// Retrieve the currently stored token, or `null` if none has been set yet.
static String? getToken() => _token;
/// Wipe the stored token (e.g. on log-out or session expiry).
static void clearToken() => _token = null;
/// `true` when a non-empty token is currently held in memory.
static bool get hasToken => _token != null && _token!.isNotEmpty;
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,82 @@
part of 'fingerprint_bloc.dart';
abstract class FingerprintEvent extends Equatable {
const FingerprintEvent();
@override
List<Object?> get props => [];
}
/// Dispatched on app startup to trigger the background login / token-fetch.
class AppStarted extends FingerprintEvent {
const AppStarted();
}
/// Dropdown: document type changed
class DocumentTypeChanged extends FingerprintEvent {
final DocumentType documentType;
const DocumentTypeChanged(this.documentType);
@override
List<Object?> get props => [documentType];
}
/// Dropdown: finger selection changed.
/// [fingerIndex] is the numeric key from [fingerIndexMap].
class FingerTypeChanged extends FingerprintEvent {
final int fingerIndex;
const FingerTypeChanged(this.fingerIndex);
@override
List<Object?> get props => [fingerIndex];
}
/// Text field: national ID changed
class NationalIdChanged extends FingerprintEvent {
final String value;
const NationalIdChanged(this.value);
@override
List<Object?> get props => [value];
}
/// Text field: nationality changed (passport mode)
class NationalityChanged extends FingerprintEvent {
final String value;
const NationalityChanged(this.value);
@override
List<Object?> get props => [value];
}
/// Text field: passport number changed
class PassportNumberChanged extends FingerprintEvent {
final String value;
const PassportNumberChanged(this.value);
@override
List<Object?> get props => [value];
}
/// Connect to the device
class ConnectDevice extends FingerprintEvent {
const ConnectDevice();
}
/// Trigger a fingerprint scan
class ScanFingerprintEvent extends FingerprintEvent {
const ScanFingerprintEvent();
}
/// Reset form to initial state
class ResetBlocEvent extends FingerprintEvent {
const ResetBlocEvent();
}
/// Send captured fingerprint data
class SendFingerprintEvent extends FingerprintEvent {
const SendFingerprintEvent();
}
/// Verify the captured fingerprint against stored records
class VerifyFingerprintEvent extends FingerprintEvent {
const VerifyFingerprintEvent();
}

@ -0,0 +1,133 @@
part of 'fingerprint_bloc.dart';
enum DocumentType { nationalId, passport }
enum FormStatus {
idle,
loggingIn,
connecting,
scanning,
scanned,
verifying,
verified,
error,
}
class FingerprintFormState extends Equatable {
final DocumentType documentType;
/// The selected finger's numeric key from [fingerIndexMap].
/// Defaults to 1 (Right Thumb).
final int selectedFingerIndex;
final String nationalIdInput;
final String nationality;
final String passportNumber;
final FormStatus status;
final String? base64Data;
final String? errorMessage;
/// Live device status text shown during the connect retry loop.
final String? deviceStatusMessage;
/// True once the scanner has successfully connected.
final bool isDeviceConnected;
/// True when the Scan button triggered the connection phase
/// (so the scan button shows "Connecting…" instead of the connect button).
final bool isScanConnecting;
final String? nationalIdError;
final String? nationalityError;
final String? passportNumberError;
/// Populated after a successful verification API call.
final AuthResultUiModel? authResult;
const FingerprintFormState({
this.documentType = DocumentType.nationalId,
this.selectedFingerIndex = 1,
this.nationalIdInput = '',
this.nationality = '',
this.passportNumber = '',
this.status = FormStatus.idle,
this.base64Data,
this.errorMessage,
this.deviceStatusMessage,
this.isDeviceConnected = false,
this.isScanConnecting = false,
this.nationalIdError,
this.nationalityError,
this.passportNumberError,
this.authResult,
});
FingerprintFormState copyWith({
DocumentType? documentType,
int? selectedFingerIndex,
String? nationalIdInput,
String? nationality,
String? passportNumber,
FormStatus? status,
String? base64Data,
String? errorMessage,
String? deviceStatusMessage,
bool? isDeviceConnected,
bool? isScanConnecting,
String? nationalIdError,
String? nationalityError,
String? passportNumberError,
AuthResultUiModel? authResult,
bool clearBase64 = false,
bool clearError = false,
bool clearDeviceStatus = false,
bool clearNationalIdError = false,
bool clearNationalityError = false,
bool clearPassportNumberError = false,
bool clearAuthResult = false,
}) {
return FingerprintFormState(
documentType: documentType ?? this.documentType,
selectedFingerIndex: selectedFingerIndex ?? this.selectedFingerIndex,
nationalIdInput: nationalIdInput ?? this.nationalIdInput,
nationality: nationality ?? this.nationality,
passportNumber: passportNumber ?? this.passportNumber,
status: status ?? this.status,
base64Data: clearBase64 ? null : (base64Data ?? this.base64Data),
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
deviceStatusMessage: clearDeviceStatus
? null
: (deviceStatusMessage ?? this.deviceStatusMessage),
isDeviceConnected: isDeviceConnected ?? this.isDeviceConnected,
isScanConnecting: isScanConnecting ?? this.isScanConnecting,
nationalIdError:
clearNationalIdError ? null : (nationalIdError ?? this.nationalIdError),
nationalityError:
clearNationalityError ? null : (nationalityError ?? this.nationalityError),
passportNumberError: clearPassportNumberError
? null
: (passportNumberError ?? this.passportNumberError),
authResult: clearAuthResult ? null : (authResult ?? this.authResult),
);
}
@override
List<Object?> get props => [
documentType,
selectedFingerIndex,
nationalIdInput,
nationality,
passportNumber,
status,
base64Data,
errorMessage,
deviceStatusMessage,
isDeviceConnected,
isScanConnecting,
nationalIdError,
nationalityError,
passportNumberError,
authResult,
];
}

@ -0,0 +1,139 @@
/// Raw API response model mirrors the JSON returned by the verification endpoint.
///
/// Example JSON:
/// ```json
/// {
/// "success": true,
/// "statusMessage": "Authentication successful!",
/// "personInfo": {
/// "firstName": "ASIF",
/// "lastName": "EHTESHAM UL HAQ",
/// "fatherName": "RAZA EHTESHAM",
/// "grandfatherName": "MUHAMMAD",
/// "occupation": "Computer Science Specialist",
/// "gender": "1",
/// "sponsor": "",
/// "idNumber": "2257773628",
/// "birthDate": "1985-01-04",
/// "nationality": "304"
/// },
/// "error": {
/// "type": "",
/// "message": ""
/// }
/// }
/// ```
class AuthResponseModel {
final bool success;
final String statusMessage;
final PersonInfoResponse? personInfo;
final ErrorResponse? error;
const AuthResponseModel({
required this.success,
required this.statusMessage,
this.personInfo,
this.error,
});
factory AuthResponseModel.fromJson(Map<String, dynamic> json) {
return AuthResponseModel(
success: json['success'] as bool? ?? false,
statusMessage: json['statusMessage'] as String? ?? '',
personInfo: json['personInfo'] != null
? PersonInfoResponse.fromJson(
json['personInfo'] as Map<String, dynamic>)
: null,
error: json['error'] != null
? ErrorResponse.fromJson(json['error'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() => {
'success': success,
'statusMessage': statusMessage,
'personInfo': personInfo?.toJson(),
'error': error?.toJson(),
};
}
/// Sub-model for the person information section of the response.
class PersonInfoResponse {
final String firstName;
final String lastName;
final String fatherName;
final String grandfatherName;
final String occupation;
final String gender;
final String sponsor;
final String idNumber;
final String birthDate;
final String nationality;
const PersonInfoResponse({
required this.firstName,
required this.lastName,
required this.fatherName,
required this.grandfatherName,
required this.occupation,
required this.gender,
required this.sponsor,
required this.idNumber,
required this.birthDate,
required this.nationality,
});
factory PersonInfoResponse.fromJson(Map<String, dynamic> json) {
return PersonInfoResponse(
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
fatherName: json['fatherName'] as String? ?? '',
grandfatherName: json['grandfatherName'] as String? ?? '',
occupation: json['occupation'] as String? ?? '',
gender: json['gender'] as String? ?? '',
sponsor: json['sponsor'] as String? ?? '',
idNumber: json['idNumber'] as String? ?? '',
birthDate: json['birthDate'] as String? ?? '',
nationality: json['nationality'] as String? ?? '',
);
}
Map<String, dynamic> toJson() => {
'firstName': firstName,
'lastName': lastName,
'fatherName': fatherName,
'grandfatherName': grandfatherName,
'occupation': occupation,
'gender': gender,
'sponsor': sponsor,
'idNumber': idNumber,
'birthDate': birthDate,
'nationality': nationality,
};
}
/// Sub-model for the error section of the response.
class ErrorResponse {
final String type;
final String message;
const ErrorResponse({
required this.type,
required this.message,
});
factory ErrorResponse.fromJson(Map<String, dynamic> json) {
return ErrorResponse(
type: json['type'] as String? ?? '',
message: json['message'] as String? ?? '',
);
}
Map<String, dynamic> toJson() => {
'type': type,
'message': message,
};
bool get hasError => type.isNotEmpty || message.isNotEmpty;
}

@ -0,0 +1,147 @@
import 'auth_response_model.dart';
import 'fingerprint_verify_response_model.dart';
/// Presentation-layer model derived from [AuthResponseModel].
///
/// All raw API fields are mapped and sanitised here so the UI layer
/// never has to inspect raw JSON.
class AuthResultUiModel {
final bool isSuccess;
final String statusMessage;
// Person Information
final String firstName;
final String lastName;
final String fatherName;
final String grandfatherName;
final String occupation;
final String gender;
final String sponsor;
final String idNumber;
final String birthDate;
final String nationality;
// Error
/// null when there is no error.
final String? errorType;
/// null when there is no error.
final String? errorMessage;
const AuthResultUiModel({
required this.isSuccess,
required this.statusMessage,
required this.firstName,
required this.lastName,
required this.fatherName,
required this.grandfatherName,
required this.occupation,
required this.gender,
required this.sponsor,
required this.idNumber,
required this.birthDate,
required this.nationality,
this.errorType,
this.errorMessage,
});
/// Convert a raw [AuthResponseModel] (legacy/mock API response) into a UI model.
factory AuthResultUiModel.fromResponse(AuthResponseModel response) {
final info = response.personInfo;
final err = response.error;
return AuthResultUiModel(
isSuccess: response.success,
statusMessage: response.statusMessage,
firstName: info?.firstName ?? '',
lastName: info?.lastName ?? '',
fatherName: info?.fatherName ?? '',
grandfatherName: info?.grandfatherName ?? '',
occupation: info?.occupation ?? '',
gender: info?.gender ?? '',
sponsor: info?.sponsor ?? '',
idNumber: info?.idNumber ?? '',
birthDate: info?.birthDate ?? '',
nationality: info?.nationality ?? '',
errorType:
(err?.type.isNotEmpty ?? false) ? err!.type : null,
errorMessage:
(err?.message.isNotEmpty ?? false) ? err!.message : null,
);
}
/// Convert a [FingerprintVerifyResponseModel] (real API response for both
/// the National-ID and Passport authentication endpoints) into a UI model.
factory AuthResultUiModel.fromFingerprintVerifyResponse(
FingerprintVerifyResponseModel response) {
final data = response.data;
final info = data?.personInfo;
// Derive gender label from sex code
String genderLabel = '';
if (info?.sexCode == '1' || info?.sexCode.toLowerCase() == 'm') {
genderLabel = 'Male';
} else if (info?.sexCode == '2' || info?.sexCode.toLowerCase() == 'f') {
genderLabel = 'Female';
} else if (info?.sexCode.isNotEmpty ?? false) {
genderLabel = info!.sexCode;
}
// Use English occupation if available, otherwise Arabic
final occupationLabel = (info?.occupationDescEn.isNotEmpty ?? false)
? info!.occupationDescEn
: (info?.occupationDescAr ?? '');
// Combine first name + family name for "last name" field in the UI
final lastName = (info?.familyName.isNotEmpty ?? false)
? info!.familyName
: '';
// Build error message from data fields when isError is true
String? errorType;
String? errorMessage;
if (data?.isError == true) {
errorType = data?.errorType.isNotEmpty == true ? data!.errorType : null;
errorMessage =
data?.errorMessage.isNotEmpty == true ? data!.errorMessage : null;
}
return AuthResultUiModel(
isSuccess: response.success && (data?.matchFlag ?? false),
statusMessage: response.message.isNotEmpty
? response.message
: (data?.matchFlag == true
? 'Authentication successful!'
: 'Authentication failed'),
firstName: info?.firstName ?? '',
lastName: lastName,
fatherName: info?.fatherName ?? '',
grandfatherName: info?.grandFatherName ?? '',
occupation: occupationLabel,
gender: genderLabel,
sponsor: info?.sponsorName ?? '',
idNumber: info?.id ?? '',
birthDate: info?.birthDateG ?? '',
nationality: info?.nationalityCode ?? '',
errorType: errorType,
errorMessage: errorMessage,
);
}
/// Convenience getter first and last name joined on two visual lines.
String get fullNameDisplay =>
[firstName, lastName].where((s) => s.isNotEmpty).join('\n');
bool get hasPersonInfo =>
firstName.isNotEmpty ||
lastName.isNotEmpty ||
fatherName.isNotEmpty ||
grandfatherName.isNotEmpty ||
occupation.isNotEmpty ||
gender.isNotEmpty ||
idNumber.isNotEmpty ||
birthDate.isNotEmpty ||
nationality.isNotEmpty;
bool get hasError => errorType != null || errorMessage != null;
}

@ -0,0 +1,143 @@
/// Shared response model for both fingerprint authentication endpoints:
/// - [POST api/Fingerprint/authenticate] (National ID flow)
/// - [POST api/Fingerprint/authenticate-passport] (Passport flow)
///
/// Both APIs return the same JSON shape.
class FingerprintVerifyResponseModel {
final bool success;
final String message;
final FingerprintVerifyData? data;
final List<String> errors;
const FingerprintVerifyResponseModel({
required this.success,
required this.message,
this.data,
this.errors = const [],
});
factory FingerprintVerifyResponseModel.fromJson(Map<String, dynamic> json) {
return FingerprintVerifyResponseModel(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
data: json['data'] != null
? FingerprintVerifyData.fromJson(
json['data'] as Map<String, dynamic>)
: null,
errors: (json['errors'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
const [],
);
}
}
/// The [data] payload inside [FingerprintVerifyResponseModel].
class FingerprintVerifyData {
final bool matchFlag;
final FingerprintPersonInfo? personInfo;
// Error / status fields returned directly in data
final int? errorCode;
final String errorType;
final String errorDetail;
final String errorMessage;
final int? statusCode;
final bool isError;
final String? rawResponseBody;
final String? responseReceivedAt;
const FingerprintVerifyData({
required this.matchFlag,
this.personInfo,
this.errorCode,
this.errorType = '',
this.errorDetail = '',
this.errorMessage = '',
this.statusCode,
this.isError = false,
this.rawResponseBody,
this.responseReceivedAt,
});
factory FingerprintVerifyData.fromJson(Map<String, dynamic> json) {
return FingerprintVerifyData(
matchFlag: json['matchFlag'] as bool? ?? false,
personInfo: json['personInfo'] != null
? FingerprintPersonInfo.fromJson(
json['personInfo'] as Map<String, dynamic>)
: null,
errorCode: json['errorCode'] as int?,
errorType: json['errorType'] as String? ?? '',
errorDetail: json['errorDetail'] as String? ?? '',
errorMessage: json['errorMessage'] as String? ?? '',
statusCode: json['statusCode'] as int?,
isError: json['isError'] as bool? ?? false,
rawResponseBody: json['rawResponseBody'] as String?,
responseReceivedAt: json['responseReceivedAt'] as String?,
);
}
}
/// Person information returned inside [FingerprintVerifyData].
class FingerprintPersonInfo {
final String id;
final String firstNameT;
final String firstName;
final String fatherName;
final String fatherNameT;
final String grandFatherName;
final String grandFatherNameT;
final String familyName;
final String familyNameT;
final String? birthDateG;
final String sexCode;
final String occupationDescAr;
final String occupationDescEn;
final String occupationCode;
final String nationalityCode;
final String sponsorName;
final String sponsorNameT;
const FingerprintPersonInfo({
this.id = '',
this.firstNameT = '',
this.firstName = '',
this.fatherName = '',
this.fatherNameT = '',
this.grandFatherName = '',
this.grandFatherNameT = '',
this.familyName = '',
this.familyNameT = '',
this.birthDateG,
this.sexCode = '',
this.occupationDescAr = '',
this.occupationDescEn = '',
this.occupationCode = '',
this.nationalityCode = '',
this.sponsorName = '',
this.sponsorNameT = '',
});
factory FingerprintPersonInfo.fromJson(Map<String, dynamic> json) {
return FingerprintPersonInfo(
id: json['id'] as String? ?? '',
firstNameT: json['firstNameT'] as String? ?? '',
firstName: json['firstName'] as String? ?? '',
fatherName: json['fatherName'] as String? ?? '',
fatherNameT: json['fatherNameT'] as String? ?? '',
grandFatherName: json['grandFatherName'] as String? ?? '',
grandFatherNameT: json['grandFatherNameT'] as String? ?? '',
familyName: json['familyName'] as String? ?? '',
familyNameT: json['familyNameT'] as String? ?? '',
birthDateG: json['birthDateG'] as String?,
sexCode: json['sexCode'] as String? ?? '',
occupationDescAr: json['occupationDescAr'] as String? ?? '',
occupationDescEn: json['occupationDescEn'] as String? ?? '',
occupationCode: json['occupationCode'] as String? ?? '',
nationalityCode: json['nationalityCode'] as String? ?? '',
sponsorName: json['sponsorName'] as String? ?? '',
sponsorNameT: json['sponsorNameT'] as String? ?? '',
);
}
}

@ -0,0 +1,59 @@
/// Response model for the [POST api/Auth/login] endpoint.
///
/// Example JSON:
/// ```json
/// {
/// "success": true,
/// "message": "Login successful",
/// "data": {
/// "token": "eyJhbGci…",
/// "expiresAt": "2026-06-22T11:43:12.071Z"
/// },
/// "errors": []
/// }
/// ```
class LoginResponseModel {
final bool success;
final String message;
final LoginDataModel? data;
final List<String> errors;
const LoginResponseModel({
required this.success,
required this.message,
this.data,
this.errors = const [],
});
factory LoginResponseModel.fromJson(Map<String, dynamic> json) {
return LoginResponseModel(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
data: json['data'] != null
? LoginDataModel.fromJson(json['data'] as Map<String, dynamic>)
: null,
errors: (json['errors'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
const [],
);
}
}
/// Token payload returned inside [LoginResponseModel.data].
class LoginDataModel {
final String token;
final String expiresAt;
const LoginDataModel({
required this.token,
required this.expiresAt,
});
factory LoginDataModel.fromJson(Map<String, dynamic> json) {
return LoginDataModel(
token: json['token'] as String? ?? '',
expiresAt: json['expiresAt'] as String? ?? '',
);
}
}

@ -0,0 +1,214 @@
/// Maps finger position index (sent to the API as [fingerType]) to its
/// human-readable display label shown in the finger-selection dropdown.
const Map<int, String> fingerIndexMap = {
1: 'Right Thumb',
2: 'Right Index Finger',
3: 'Right Middle Finger',
4: 'Right Ring Finger',
5: 'Right Baby Finger (or Pinky)',
6: 'Left Thumb',
7: 'Left Index Finger',
8: 'Left Middle Finger',
9: 'Left Ring Finger',
10: 'Left Baby Finger (or Pinky)',
};
const List<Map<String, String>> countryAlpha2List = [
{'name': 'Afghanistan', 'code': 'AF'},
{'name': 'Albania', 'code': 'AL'},
{'name': 'Algeria', 'code': 'DZ'},
{'name': 'Andorra', 'code': 'AD'},
{'name': 'Angola', 'code': 'AO'},
{'name': 'Antigua and Barbuda', 'code': 'AG'},
{'name': 'Argentina', 'code': 'AR'},
{'name': 'Armenia', 'code': 'AM'},
{'name': 'Australia', 'code': 'AU'},
{'name': 'Austria', 'code': 'AT'},
{'name': 'Azerbaijan', 'code': 'AZ'},
{'name': 'Bahamas', 'code': 'BS'},
{'name': 'Bahrain', 'code': 'BH'},
{'name': 'Bangladesh', 'code': 'BD'},
{'name': 'Barbados', 'code': 'BB'},
{'name': 'Belarus', 'code': 'BY'},
{'name': 'Belgium', 'code': 'BE'},
{'name': 'Belize', 'code': 'BZ'},
{'name': 'Benin', 'code': 'BJ'},
{'name': 'Bhutan', 'code': 'BT'},
{'name': 'Bolivia', 'code': 'BO'},
{'name': 'Bosnia and Herzegovina', 'code': 'BA'},
{'name': 'Botswana', 'code': 'BW'},
{'name': 'Brazil', 'code': 'BR'},
{'name': 'Brunei', 'code': 'BN'},
{'name': 'Bulgaria', 'code': 'BG'},
{'name': 'Burkina Faso', 'code': 'BF'},
{'name': 'Burundi', 'code': 'BI'},
{'name': 'Cabo Verde', 'code': 'CV'},
{'name': 'Cambodia', 'code': 'KH'},
{'name': 'Cameroon', 'code': 'CM'},
{'name': 'Canada', 'code': 'CA'},
{'name': 'Central African Republic', 'code': 'CF'},
{'name': 'Chad', 'code': 'TD'},
{'name': 'Chile', 'code': 'CL'},
{'name': 'China', 'code': 'CN'},
{'name': 'Colombia', 'code': 'CO'},
{'name': 'Comoros', 'code': 'KM'},
{'name': 'Congo', 'code': 'CG'},
{'name': 'Costa Rica', 'code': 'CR'},
{'name': 'Croatia', 'code': 'HR'},
{'name': 'Cuba', 'code': 'CU'},
{'name': 'Cyprus', 'code': 'CY'},
{'name': 'Czechia', 'code': 'CZ'},
{'name': 'Democratic Republic of the Congo', 'code': 'CD'},
{'name': 'Denmark', 'code': 'DK'},
{'name': 'Djibouti', 'code': 'DJ'},
{'name': 'Dominica', 'code': 'DM'},
{'name': 'Dominican Republic', 'code': 'DO'},
{'name': 'Ecuador', 'code': 'EC'},
{'name': 'Egypt', 'code': 'EG'},
{'name': 'El Salvador', 'code': 'SV'},
{'name': 'Equatorial Guinea', 'code': 'GQ'},
{'name': 'Eritrea', 'code': 'ER'},
{'name': 'Estonia', 'code': 'EE'},
{'name': 'Eswatini', 'code': 'SZ'},
{'name': 'Ethiopia', 'code': 'ET'},
{'name': 'Fiji', 'code': 'FJ'},
{'name': 'Finland', 'code': 'FI'},
{'name': 'France', 'code': 'FR'},
{'name': 'Gabon', 'code': 'GA'},
{'name': 'Gambia', 'code': 'GM'},
{'name': 'Georgia', 'code': 'GE'},
{'name': 'Germany', 'code': 'DE'},
{'name': 'Ghana', 'code': 'GH'},
{'name': 'Greece', 'code': 'GR'},
{'name': 'Grenada', 'code': 'GD'},
{'name': 'Guatemala', 'code': 'GT'},
{'name': 'Guinea', 'code': 'GN'},
{'name': 'Guinea-Bissau', 'code': 'GW'},
{'name': 'Guyana', 'code': 'GY'},
{'name': 'Haiti', 'code': 'HT'},
{'name': 'Honduras', 'code': 'HN'},
{'name': 'Hungary', 'code': 'HU'},
{'name': 'Iceland', 'code': 'IS'},
{'name': 'India', 'code': 'IN'},
{'name': 'Indonesia', 'code': 'ID'},
{'name': 'Iran', 'code': 'IR'},
{'name': 'Iraq', 'code': 'IQ'},
{'name': 'Ireland', 'code': 'IE'},
{'name': 'Israel', 'code': 'IL'},
{'name': 'Italy', 'code': 'IT'},
{'name': 'Jamaica', 'code': 'JM'},
{'name': 'Japan', 'code': 'JP'},
{'name': 'Jordan', 'code': 'JO'},
{'name': 'Kazakhstan', 'code': 'KZ'},
{'name': 'Kenya', 'code': 'KE'},
{'name': 'Kiribati', 'code': 'KI'},
{'name': 'Kuwait', 'code': 'KW'},
{'name': 'Kyrgyzstan', 'code': 'KG'},
{'name': 'Laos', 'code': 'LA'},
{'name': 'Latvia', 'code': 'LV'},
{'name': 'Lebanon', 'code': 'LB'},
{'name': 'Lesotho', 'code': 'LS'},
{'name': 'Liberia', 'code': 'LR'},
{'name': 'Libya', 'code': 'LY'},
{'name': 'Liechtenstein', 'code': 'LI'},
{'name': 'Lithuania', 'code': 'LT'},
{'name': 'Luxembourg', 'code': 'LU'},
{'name': 'Madagascar', 'code': 'MG'},
{'name': 'Malawi', 'code': 'MW'},
{'name': 'Malaysia', 'code': 'MY'},
{'name': 'Maldives', 'code': 'MV'},
{'name': 'Mali', 'code': 'ML'},
{'name': 'Malta', 'code': 'MT'},
{'name': 'Marshall Islands', 'code': 'MH'},
{'name': 'Mauritania', 'code': 'MR'},
{'name': 'Mauritius', 'code': 'MU'},
{'name': 'Mexico', 'code': 'MX'},
{'name': 'Micronesia', 'code': 'FM'},
{'name': 'Moldova', 'code': 'MD'},
{'name': 'Monaco', 'code': 'MC'},
{'name': 'Mongolia', 'code': 'MN'},
{'name': 'Montenegro', 'code': 'ME'},
{'name': 'Morocco', 'code': 'MA'},
{'name': 'Mozambique', 'code': 'MZ'},
{'name': 'Myanmar', 'code': 'MM'},
{'name': 'Namibia', 'code': 'NA'},
{'name': 'Nauru', 'code': 'NR'},
{'name': 'Nepal', 'code': 'NP'},
{'name': 'Netherlands', 'code': 'NL'},
{'name': 'New Zealand', 'code': 'NZ'},
{'name': 'Nicaragua', 'code': 'NI'},
{'name': 'Niger', 'code': 'NE'},
{'name': 'Nigeria', 'code': 'NG'},
{'name': 'North Korea', 'code': 'KP'},
{'name': 'North Macedonia', 'code': 'MK'},
{'name': 'Norway', 'code': 'NO'},
{'name': 'Oman', 'code': 'OM'},
{'name': 'Pakistan', 'code': 'PK'},
{'name': 'Palau', 'code': 'PW'},
{'name': 'Palestine', 'code': 'PS'},
{'name': 'Panama', 'code': 'PA'},
{'name': 'Papua New Guinea', 'code': 'PG'},
{'name': 'Paraguay', 'code': 'PY'},
{'name': 'Peru', 'code': 'PE'},
{'name': 'Philippines', 'code': 'PH'},
{'name': 'Poland', 'code': 'PL'},
{'name': 'Portugal', 'code': 'PT'},
{'name': 'Qatar', 'code': 'QA'},
{'name': 'Romania', 'code': 'RO'},
{'name': 'Russia', 'code': 'RU'},
{'name': 'Rwanda', 'code': 'RW'},
{'name': 'Saint Kitts and Nevis', 'code': 'KN'},
{'name': 'Saint Lucia', 'code': 'LC'},
{'name': 'Saint Vincent and the Grenadines', 'code': 'VC'},
{'name': 'Samoa', 'code': 'WS'},
{'name': 'San Marino', 'code': 'SM'},
{'name': 'Sao Tome and Principe', 'code': 'ST'},
{'name': 'Saudi Arabia', 'code': 'SA'},
{'name': 'Senegal', 'code': 'SN'},
{'name': 'Serbia', 'code': 'RS'},
{'name': 'Seychelles', 'code': 'SC'},
{'name': 'Sierra Leone', 'code': 'SL'},
{'name': 'Singapore', 'code': 'SG'},
{'name': 'Slovakia', 'code': 'SK'},
{'name': 'Slovenia', 'code': 'SI'},
{'name': 'Solomon Islands', 'code': 'SB'},
{'name': 'Somalia', 'code': 'SO'},
{'name': 'South Africa', 'code': 'ZA'},
{'name': 'South Korea', 'code': 'KR'},
{'name': 'South Sudan', 'code': 'SS'},
{'name': 'Spain', 'code': 'ES'},
{'name': 'Sri Lanka', 'code': 'LK'},
{'name': 'Sudan', 'code': 'SD'},
{'name': 'Suriname', 'code': 'SR'},
{'name': 'Sweden', 'code': 'SE'},
{'name': 'Switzerland', 'code': 'CH'},
{'name': 'Syria', 'code': 'SY'},
{'name': 'Taiwan', 'code': 'TW'},
{'name': 'Tajikistan', 'code': 'TJ'},
{'name': 'Tanzania', 'code': 'TZ'},
{'name': 'Thailand', 'code': 'TH'},
{'name': 'Timor-Leste', 'code': 'TL'},
{'name': 'Togo', 'code': 'TG'},
{'name': 'Tonga', 'code': 'TO'},
{'name': 'Trinidad and Tobago', 'code': 'TT'},
{'name': 'Tunisia', 'code': 'TN'},
{'name': 'Turkey', 'code': 'TR'},
{'name': 'Turkmenistan', 'code': 'TM'},
{'name': 'Tuvalu', 'code': 'TV'},
{'name': 'Uganda', 'code': 'UG'},
{'name': 'Ukraine', 'code': 'UA'},
{'name': 'United Arab Emirates', 'code': 'AE'},
{'name': 'United Kingdom', 'code': 'GB'},
{'name': 'United States', 'code': 'US'},
{'name': 'Uruguay', 'code': 'UY'},
{'name': 'Uzbekistan', 'code': 'UZ'},
{'name': 'Vanuatu', 'code': 'VU'},
{'name': 'Vatican City', 'code': 'VA'},
{'name': 'Venezuela', 'code': 'VE'},
{'name': 'Vietnam', 'code': 'VN'},
{'name': 'Yemen', 'code': 'YE'},
{'name': 'Zambia', 'code': 'ZM'},
{'name': 'Zimbabwe', 'code': 'ZW'},
];

@ -0,0 +1,10 @@
abstract class FingerprintManager {
/// Connect to the fingerprint sensor
Future<void> connect();
/// Disconnect from the fingerprint sensor
Future<void> disconnect();
/// Scan the fingerprint and return the base64 encoded result
Future<String?> scan();
}

@ -0,0 +1,108 @@
import 'dart:convert';
import 'dart:math';
import 'package:image/image.dart' as img;
class FingerprintValidation {
final bool isComplete;
final double coverageRatio; // % of "ridge" pixels
final double contrast; // std dev of pixel intensity
final String reason;
FingerprintValidation({
required this.isComplete,
required this.coverageRatio,
required this.contrast,
required this.reason,
});
}
FingerprintValidation validateFingerprintBase64(
String base64Image, {
double minCoverage = 0.20, // tune per scanner DPI/size
double maxCoverage = 0.65,
double minContrast = 25.0,
double edgeCutoffThreshold = 0.45, // % of border pixels that are "ridge"
}) {
final bytes = base64Decode(base64Image);
final image = img.decodeImage(bytes);
if (image == null) {
return FingerprintValidation(
isComplete: false,
coverageRatio: 0,
contrast: 0,
reason: 'decode_failed',
);
}
final gray = img.grayscale(image);
final w = gray.width, h = gray.height;
int ridgePixels = 0;
int total = 0;
double sum = 0, sumSq = 0;
// simple Otsu-ish threshold: use mean as baseline, refine below
final List<int> intensities = [];
for (final px in gray) {
final v = img.getLuminance(px).toDouble();
intensities.add(v.toInt());
sum += v;
total++;
}
final mean = sum / total;
for (final v in intensities) {
sumSq += (v - mean) * (v - mean);
}
final contrast = sqrt(sumSq / total);
// threshold: pixels darker than mean - 0.5*std are "ridges"
final threshold = mean - (contrast * 0.5);
for (final v in intensities) {
if (v < threshold) ridgePixels++;
}
final coverage = ridgePixels / total;
// edge/border check if ridge pattern is heavily cut off at edges,
// finger was likely partial or misplaced
int borderRidge = 0, borderTotal = 0;
for (int x = 0; x < w; x++) {
for (final y in [0, h - 1]) {
final v = img.getLuminance(gray.getPixel(x, y)).toDouble();
borderTotal++;
if (v < threshold) borderRidge++;
}
}
for (int y = 0; y < h; y++) {
for (final x in [0, w - 1]) {
final v = img.getLuminance(gray.getPixel(x, y)).toDouble();
borderTotal++;
if (v < threshold) borderRidge++;
}
}
final borderRatio = borderTotal == 0 ? 0 : borderRidge / borderTotal;
String reason = 'ok';
bool isComplete = true;
if (contrast < minContrast) {
isComplete = false;
reason = 'Press finger on the scanner with more pressure';
} else if (coverage < minCoverage) {
isComplete = false;
reason = 'Place finger fully on the scanner';
} else if (coverage > maxCoverage) {
isComplete = false;
reason = 'Place finger lightly on the scanner';
} else if (borderRatio > edgeCutoffThreshold) {
isComplete = false;
reason = 'Finger is not centered on the scanner';
}
return FingerprintValidation(
isComplete: isComplete,
coverageRatio: coverage,
contrast: contrast,
reason: reason,
);
}

@ -0,0 +1,283 @@
import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'package:zkfinger10/finger_status.dart';
import 'package:zkfinger10/finger_status_type.dart';
import 'package:zkfinger10/zk_finger.dart';
import 'finger_print_manager.dart';
class ZktFingerPrintScanner implements FingerprintManager {
bool _isConnected = false;
// Scan state (fresh per scan call)
Completer<String>? _scanCompleter;
StreamController<String>? _base64ImageController;
StreamSubscription<dynamic>? _imageSubscription;
// Status stream state
StreamSubscription<dynamic>? _statusSubscription;
FingerStatus? lastStatus;
FingerStatusType? _tempStatusType;
/// Set during connect(); completed by _updateStatus when a success status
/// arrives. Reset at the start of every retry attempt.
Completer<void>? _deviceReadyCompleter;
static const int _maxConnectAttempts = 5;
static const Duration _attemptTimeout = Duration(seconds: 4);
static const Duration _retryDelay = Duration(seconds: 4);
static const List<FingerStatusType> _successStatuses = [
FingerStatusType.STARTED_ALREADY,
FingerStatusType.STARTED_SUCCESS,
];
static const List<FingerStatusType> _usbConnectionFailureStatuses = [
FingerStatusType.FINGER_USB_PERMISSION_DENIED,
FingerStatusType.FINGER_USB_PERMISSION_ERROR,
];
// Stream accessor
Stream<String> get base64ImageStream =>
_base64ImageController?.stream ?? const Stream.empty();
ZktFingerPrintScanner();
// Status callback
void _updateStatus(dynamic value) {
try {
final statusMap = value as Map<dynamic, dynamic>;
final statusType =
FingerStatusType.values[statusMap['fingerStatus'] as int];
lastStatus = FingerStatus(
statusMap['message'],
statusType,
statusMap['id'],
statusMap['data'],
);
if (statusType == _tempStatusType &&
_tempStatusType == FingerStatusType.CAPTURE_ERROR) {
// Ignore repeated CAPTURE_ERROR device is stuck, do nothing
} else {
_tempStatusType = statusType;
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted &&
_usbConnectionFailureStatuses.contains(statusType) ){
_deviceReadyCompleter!.completeError(Exception(
'Device not connected'
));
}
// If we are in the connect-retry loop and got a success status, resolve
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted &&
lastStatus != null &&
_successStatuses.contains(statusType) && _successStatuses.contains(lastStatus!.statusType)) {
_deviceReadyCompleter!.complete();
}
}
} catch (e) {
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(e);
}
}
}
// Image callback
void _onNativeImage(dynamic imageBytes) {
try {
final base64Image = uint8ListTob64(imageBytes as Uint8List);
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.add(base64Image);
}
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.complete(base64Image);
}
} catch (e) {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.addError(e);
}
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(e);
}
}
}
// Public API
@override
Future<void> connect() async {
try {
if (_isConnected) await disconnect();
final result = await ZkFinger.openConnection();
if (result != true) {
throw Exception('Failed to initialize fingerprint device');
}
// Start status stream
await _statusSubscription?.cancel();
_statusSubscription =
ZkFinger.statusChangeStream.receiveBroadcastStream().listen(
_updateStatus,
onError: (e) {
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(e);
}
},
);
// Retry loop: up to 5 attempts, 5 s window each
bool deviceReady = false;
for (int attempt = 1; attempt <= _maxConnectAttempts; attempt++) {
_deviceReadyCompleter = Completer<void>();
final result = await ZkFinger.openConnection();
if (result != true) {
throw Exception('Failed to initialize fingerprint device');
}
try {
await _deviceReadyCompleter!.future.timeout(_attemptTimeout);
// Success status received within the window
deviceReady = true;
break;
} on TimeoutException {
if (attempt < _maxConnectAttempts) {
await Future.delayed(_retryDelay);
}
}
}
if (!deviceReady) {
// Clean up status subscription and close hardware
await _statusSubscription?.cancel();
_statusSubscription = null;
try { await ZkFinger.closeConnection(); } catch (_) {}
throw Exception(
'Device not ready after $_maxConnectAttempts attempts. '
'Last status: ${lastStatus?.message ?? "no status received"}',
);
}
_isConnected = true;
// Subscribe to image stream now that device is confirmed ready
_imageSubscription =
ZkFinger.imageStream.receiveBroadcastStream().listen(
_onNativeImage,
onError: (e) {
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(e);
}
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.addError(e);
}
},
);
} catch (e) {
_isConnected = false;
rethrow; // preserve original message
}
}
@override
Future<void> disconnect() async {
try { await ZkFinger.closeConnection(); } catch (_) {}
try { await _imageSubscription?.cancel(); } catch (_) {}
_imageSubscription = null;
try { await _statusSubscription?.cancel(); } catch (_) {}
_statusSubscription = null;
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(StateError('Disconnected'));
}
_deviceReadyCompleter = null;
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(StateError('Disconnected'));
}
_scanCompleter = null;
try {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
await _base64ImageController!.close();
}
} catch (_) {}
_base64ImageController = null;
_isConnected = false;
_tempStatusType = null;
}
/// Returns a base64-encoded fingerprint for a single scan.
/// Creates a fresh [Completer] + [StreamController] each call so
/// consecutive scans always work.
@override
Future<String?> scan(
{Duration timeout = const Duration(seconds: 20)}) async {
try {
if (!_isConnected) await connect();
try {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
await _base64ImageController!.close();
}
} catch (_) {}
_base64ImageController = StreamController<String>.broadcast();
_scanCompleter = Completer<String>();
final timer = Timer(timeout, () {
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!
.completeError(TimeoutException('Scan timed out', timeout));
}
});
try {
return await _scanCompleter!.future;
} finally {
timer.cancel();
}
} catch (e) {
throw Exception('Fingerprint scan failed: ${e.toString()}');
}
}
// Helpers
String uint8ListTob64(Uint8List uint8list) {
return 'data:image/png;base64,${base64Encode(uint8list)}';
}
bool get isConnected => _isConnected;
void dispose() {
_imageSubscription?.cancel();
_statusSubscription?.cancel();
_scanCompleter = null;
_deviceReadyCompleter = null;
_base64ImageController?.close();
_base64ImageController = null;
}
}

@ -0,0 +1,647 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../models/auth_result_ui_model.dart';
/// Full-screen view that displays the authentication result returned by the
/// verification API. Redesigned with a "futuristic hospital, KSA" theme:
/// dark glassy panels, glowing cyan/green accents, Material 3 components,
/// and a responsive two-column layout on tablets.
class AuthResultScreen extends StatelessWidget {
final AuthResultUiModel result;
const AuthResultScreen({super.key, required this.result});
@override
Widget build(BuildContext context) {
final accent = result.isSuccess ? AppTheme.saudiGreen : AppTheme.redAlert;
return Scaffold(
backgroundColor: AppTheme.ink,
appBar: AppBar(
leading: const BackButton(),
title: const Text('Authentication Result'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: accent.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
result.isSuccess ? Icons.verified_outlined : Icons.error_outline,
size: 14,
color: accent,
),
const SizedBox(width: 5),
Text(
result.isSuccess ? 'VERIFIED' : 'FAILED',
style: TextStyle(fontSize: 11, color: accent, fontWeight: FontWeight.w700),
),
],
),
),
),
],
),
body: TechGridBackground(
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final tablet = constraints.maxWidth >= 760;
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: tablet ? 1000 : 720),
child: tablet ? _TabletBody(result: result) : _PhoneBody(result: result),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: tablet ? 1000 : 720),
child: FilledButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.fingerprint),
label: const Text('Return to Fingerprint'),
),
),
),
),
],
);
},
),
),
),
);
}
}
// Phone body: stacked sections
class _PhoneBody extends StatelessWidget {
final AuthResultUiModel result;
const _PhoneBody({required this.result});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_StatusBanner(result: result),
const SizedBox(height: 16),
if (result.isSuccess) _PersonInfoPanel(result: result),
if (!result.isSuccess) _ErrorPanel(result: result),
],
);
}
}
// Tablet body: status banner full width, info/error two-column grid
class _TabletBody extends StatelessWidget {
final AuthResultUiModel result;
const _TabletBody({required this.result});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_StatusBanner(result: result),
const SizedBox(height: 20),
if (result.isSuccess) _PersonInfoPanel(result: result, twoColumn: true),
if (!result.isSuccess) _ErrorPanel(result: result),
],
);
}
}
// Status banner
class _StatusBanner extends StatelessWidget {
final AuthResultUiModel result;
const _StatusBanner({required this.result});
@override
Widget build(BuildContext context) {
final accent = result.isSuccess ? AppTheme.saudiGreen : AppTheme.redAlert;
final message = result.statusMessage.isEmpty
? (result.isSuccess ? 'Authentication successful!' : 'Authentication failed')
: result.statusMessage;
return GlowPanel(
glowColor: accent,
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 52,
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: accent.withValues(alpha: 0.12),
border: Border.all(color: accent.withValues(alpha: 0.4)),
),
child: Icon(
result.isSuccess ? Icons.check_circle_outline : Icons.cancel_outlined,
color: accent,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'STATUS',
style: TextStyle(
color: AppTheme.textMuted,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
const SizedBox(height: 4),
Text(
message,
style: TextStyle(color: accent, fontWeight: FontWeight.w700, fontSize: 16),
),
],
),
),
],
),
);
}
}
// Person information panel
class _PersonInfoPanel extends StatelessWidget {
final AuthResultUiModel result;
final bool twoColumn;
const _PersonInfoPanel({required this.result, this.twoColumn = false});
@override
Widget build(BuildContext context) {
final fullName = [result.firstName, result.lastName].where((s) => s.isNotEmpty).join(' ');
final rows = <_DetailRow>[
_DetailRow(label: 'Father', value: result.fatherName, icon: Icons.man_outlined),
_DetailRow(label: 'Grandfather', value: result.grandfatherName, icon: Icons.elderly_outlined),
_DetailRow(label: 'Occupation', value: result.occupation, icon: Icons.work_outline),
_DetailRow(label: 'Gender', value: result.gender, icon: Icons.wc_outlined),
_DetailRow(label: 'Sponsor', value: result.sponsor, icon: Icons.shield_outlined),
_DetailRow(label: 'ID Number', value: result.idNumber, icon: Icons.badge_outlined),
_DetailRow(label: 'Birth Date', value: result.birthDate, icon: Icons.cake_outlined),
_DetailRow(label: 'Nationality', value: result.nationality, icon: Icons.flag_outlined),
];
return GlowPanel(
glowColor: AppTheme.saudiGreen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.saudiGreen.withValues(alpha: 0.12),
border: Border.all(color: AppTheme.saudiGreen.withValues(alpha: 0.4)),
),
child: const Icon(Icons.person_outline, color: AppTheme.saudiGreen, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'PERSON INFORMATION',
style: TextStyle(
color: AppTheme.saudiGreen,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
Text(
fullName.isEmpty ? '' : fullName,
style: const TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
if (twoColumn)
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 4,
crossAxisSpacing: 16,
childAspectRatio: 5.4,
children: rows,
)
else
Column(children: rows),
],
),
);
}
}
// Error panel
class _ErrorPanel extends StatelessWidget {
final AuthResultUiModel result;
const _ErrorPanel({required this.result});
@override
Widget build(BuildContext context) {
return GlowPanel(
glowColor: AppTheme.redAlert,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.redAlert.withValues(alpha: 0.12),
border: Border.all(color: AppTheme.redAlert.withValues(alpha: 0.4)),
),
child: const Icon(Icons.error_outline, color: AppTheme.redAlert, size: 22),
),
const SizedBox(width: 12),
const Text(
'ERROR DETAILS',
style: TextStyle(
color: AppTheme.redAlert,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
_DetailRow(label: 'Type', value: result.errorType ?? '', icon: Icons.category_outlined),
_DetailRow(label: 'Message', value: result.errorMessage ?? '', icon: Icons.notes_outlined),
],
),
);
}
}
// Reusable detail row (icon + label + value)
class _DetailRow extends StatelessWidget {
final String label;
final String value;
final IconData icon;
const _DetailRow({required this.label, required this.value, required this.icon});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: AppTheme.textMuted),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppTheme.textMuted,
letterSpacing: 0.4,
),
),
const SizedBox(height: 2),
Text(
value.isEmpty ? '' : value,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}
// import 'package:flutter/material.dart';
// import '../models/auth_result_ui_model.dart';
//
// /// Full-screen view that displays the authentication result returned by the
// /// verification API. Designed with the Material 3 design system.
// class AuthResultScreen extends StatelessWidget {
// final AuthResultUiModel result;
//
// const AuthResultScreen({super.key, required this.result});
//
// @override
// Widget build(BuildContext context) {
// final cs = Theme.of(context).colorScheme;
//
// return Scaffold(
// backgroundColor: cs.surfaceContainerLowest,
// appBar: AppBar(
// backgroundColor: cs.surface,
// surfaceTintColor: cs.surfaceTint,
// leading: const BackButton(),
// title: const Text('Authentication Result'),
// titleTextStyle: Theme.of(context).textTheme.titleLarge?.copyWith(
// fontWeight: FontWeight.bold,
// ),
// centerTitle: false,
// elevation: 0,
// scrolledUnderElevation: 1,
// ),
// body: SafeArea(
// child: Column(
// children: [
// // Scrollable content
// Expanded(
// child: SingleChildScrollView(
// padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
// child: Card(
// elevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// side: BorderSide(color: cs.outlineVariant),
// ),
// child: Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 20, horizontal: 16),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Status
// _StatusSection(result: result),
//
// _divider(),
//
// // Person information
// _PersonInfoSection(result: result),
//
// _divider(),
//
// // Error
// _ErrorSection(result: result),
// ],
// ),
// ),
// ),
// ),
// ),
//
// // Return button
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
// child: FilledButton.icon(
// onPressed: () => Navigator.of(context).pop(),
// icon: const Icon(Icons.fingerprint),
// label: const Text('Return to Fingerprint'),
// style: FilledButton.styleFrom(
// minimumSize: const Size.fromHeight(52),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12),
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// );
// }
//
// Widget _divider() => const Padding(
// padding: EdgeInsets.symmetric(vertical: 12),
// child: Divider(height: 1),
// );
// }
//
// // Status section
//
// class _StatusSection extends StatelessWidget {
// final AuthResultUiModel result;
//
// const _StatusSection({required this.result});
//
// @override
// Widget build(BuildContext context) {
// final tt = Theme.of(context).textTheme;
// final successColor = const Color(0xFF2E7D32); // green-800
// final errorColor = Theme.of(context).colorScheme.error;
//
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text('Status:', style: tt.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
// const SizedBox(height: 4),
// Text(
// result.statusMessage.isEmpty
// ? (result.isSuccess ? 'Authentication successful!' : 'Authentication failed')
// : result.statusMessage,
// style: tt.bodyMedium?.copyWith(
// color: result.isSuccess ? successColor : errorColor,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// );
// }
// }
//
// // Person information section
//
// class _PersonInfoSection extends StatelessWidget {
// final AuthResultUiModel result;
//
// const _PersonInfoSection({required this.result});
//
// @override
// Widget build(BuildContext context) {
// final tt = Theme.of(context).textTheme;
// const successGreen = Color(0xFF2E7D32);
//
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Section header
// Row(
// children: [
// const Icon(Icons.check, color: successGreen, size: 18),
// const SizedBox(width: 6),
// Text(
// 'Person Information:',
// style: tt.titleSmall?.copyWith(
// fontWeight: FontWeight.bold,
// color: successGreen,
// ),
// ),
// ],
// ),
// const SizedBox(height: 12),
//
// // Full Name (two visual lines: first name + last name)
// _DetailRow(
// label: 'Full Name:',
// value: result.firstName,
// valueBold: true,
// ),
// if (result.lastName.isNotEmpty) ...[
// const SizedBox(height: 2),
// Padding(
// padding: const EdgeInsets.only(bottom: 8),
// child: Text(
// result.lastName,
// style: tt.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
// ),
// ),
// ],
//
// _DetailRow(label: 'Father:', value: result.fatherName),
// _DetailRow(label: 'Grandfather:', value: result.grandfatherName),
// _DetailRow(label: 'Occupation:', value: result.occupation),
// _DetailRow(label: 'Gender:', value: result.gender),
// _DetailRow(label: 'Sponsor:', value: result.sponsor),
//
// const SizedBox(height: 4),
//
// _DetailRow(label: 'ID Number:', value: result.idNumber),
// _DetailRow(label: 'Birth Date:', value: result.birthDate),
// _DetailRow(label: 'Nationality:', value: result.nationality),
// ],
// );
// }
// }
//
// // Error section
//
// class _ErrorSection extends StatelessWidget {
// final AuthResultUiModel result;
//
// const _ErrorSection({required this.result});
//
// @override
// Widget build(BuildContext context) {
// final tt = Theme.of(context).textTheme;
// final errorColor = Theme.of(context).colorScheme.error;
//
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Section header
// Row(
// children: [
// Icon(Icons.close, color: errorColor, size: 18),
// const SizedBox(width: 6),
// Text(
// 'Error:',
// style: tt.titleSmall?.copyWith(
// fontWeight: FontWeight.bold,
// color: errorColor,
// ),
// ),
// ],
// ),
// const SizedBox(height: 12),
// _DetailRow(label: 'Type:', value: result.errorType ?? ''),
// _DetailRow(label: 'Message:', value: result.errorMessage ?? ''),
// ],
// );
// }
// }
//
// // Reusable detail row (label + value)
//
// class _DetailRow extends StatelessWidget {
// final String label;
// final String value;
// final bool valueBold;
//
// const _DetailRow({
// required this.label,
// required this.value,
// this.valueBold = false,
// });
//
// @override
// Widget build(BuildContext context) {
// final tt = Theme.of(context).textTheme;
//
// return Padding(
// padding: const EdgeInsets.only(bottom: 8),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// label,
// style: tt.bodySmall?.copyWith(
// fontWeight: FontWeight.bold,
// color: Theme.of(context).colorScheme.onSurfaceVariant,
// ),
// ),
// const SizedBox(height: 2),
// Text(
// value.isEmpty ? '' : value,
// style: tt.bodyMedium?.copyWith(
// fontWeight: valueBold ? FontWeight.bold : FontWeight.normal,
// ),
// ),
// ],
// ),
// );
// }
// }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,276 @@
import 'package:finger_print_verification/theme/app_theme.dart';
import 'package:flutter/material.dart';
import '../models/auth_result_ui_model.dart';
///
/// Shared, public building blocks for rendering an [AuthResultUiModel].
///
/// These used to live as private widgets inside `auth_result_screen.dart`.
/// They're now public and standalone so the SAME widgets can be reused:
/// full-screen, on phones, via [AuthResultScreen] (pushed route)
/// inline, on tablets, embedded directly in [FingerprintView]'s result
/// pane no navigation needed, the result just appears next to the form.
///
class ResultStatusBanner extends StatelessWidget {
final AuthResultUiModel result;
const ResultStatusBanner({super.key, required this.result});
@override
Widget build(BuildContext context) {
final accent = result.isSuccess ? AppTheme.saudiGreen : AppTheme.redAlert;
final message = result.statusMessage.isEmpty
? (result.isSuccess ? 'Authentication successful!' : 'Authentication failed')
: result.statusMessage;
return GlowPanel(
glowColor: accent,
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 52,
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: accent.withValues(alpha: 0.12),
border: Border.all(color: accent.withValues(alpha: 0.4)),
),
child: Icon(
result.isSuccess ? Icons.check_circle_outline : Icons.cancel_outlined,
color: accent,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'STATUS',
style: TextStyle(
color: AppTheme.textMuted,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
const SizedBox(height: 4),
Text(
message,
style: TextStyle(color: accent, fontWeight: FontWeight.w700, fontSize: 16),
),
],
),
),
],
),
);
}
}
class ResultPersonInfoPanel extends StatelessWidget {
final AuthResultUiModel result;
final bool twoColumn;
const ResultPersonInfoPanel({super.key, required this.result, this.twoColumn = false});
@override
Widget build(BuildContext context) {
final fullName = [result.firstName, result.lastName].where((s) => s.isNotEmpty).join(' ');
final rows = <ResultDetailRow>[
ResultDetailRow(label: 'Father', value: result.fatherName, icon: Icons.man_outlined),
ResultDetailRow(label: 'Grandfather', value: result.grandfatherName, icon: Icons.elderly_outlined),
ResultDetailRow(label: 'Occupation', value: result.occupation, icon: Icons.work_outline),
ResultDetailRow(label: 'Gender', value: result.gender, icon: Icons.wc_outlined),
ResultDetailRow(label: 'Sponsor', value: result.sponsor, icon: Icons.shield_outlined),
ResultDetailRow(label: 'ID Number', value: result.idNumber, icon: Icons.badge_outlined),
ResultDetailRow(label: 'Birth Date', value: result.birthDate, icon: Icons.cake_outlined),
ResultDetailRow(label: 'Nationality', value: result.nationality, icon: Icons.flag_outlined),
];
return GlowPanel(
glowColor: AppTheme.saudiGreen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.saudiGreen.withValues(alpha: 0.12),
border: Border.all(color: AppTheme.saudiGreen.withValues(alpha: 0.4)),
),
child: const Icon(Icons.person_outline, color: AppTheme.saudiGreen, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'PERSON INFORMATION',
style: TextStyle(
color: AppTheme.saudiGreen,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
Text(
fullName.isEmpty ? '' : fullName,
style: const TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
if (twoColumn)
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 4,
crossAxisSpacing: 16,
childAspectRatio: 5.4,
children: rows,
)
else
Column(children: rows),
],
),
);
}
}
class ResultErrorPanel extends StatelessWidget {
final AuthResultUiModel result;
const ResultErrorPanel({super.key, required this.result});
@override
Widget build(BuildContext context) {
return GlowPanel(
glowColor: AppTheme.redAlert,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.redAlert.withValues(alpha: 0.12),
border: Border.all(color: AppTheme.redAlert.withValues(alpha: 0.4)),
),
child: const Icon(Icons.error_outline, color: AppTheme.redAlert, size: 22),
),
const SizedBox(width: 12),
const Text(
'ERROR DETAILS',
style: TextStyle(
color: AppTheme.redAlert,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
ResultDetailRow(label: 'Type', value: result.errorType ?? '', icon: Icons.category_outlined),
ResultDetailRow(label: 'Message', value: result.errorMessage ?? '', icon: Icons.notes_outlined),
],
),
);
}
}
/// Reusable detail row (icon + label + value).
class ResultDetailRow extends StatelessWidget {
final String label;
final String value;
final IconData icon;
const ResultDetailRow({super.key, required this.label, required this.value, required this.icon});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: AppTheme.textMuted),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppTheme.textMuted,
letterSpacing: 0.4,
),
),
const SizedBox(height: 2),
Text(
value.isEmpty ? '' : value,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}
/// Full result body (banner + person-info-or-error), used by both the
/// pushed phone route and the inline tablet pane.
class ResultBody extends StatelessWidget {
final AuthResultUiModel result;
final bool twoColumn;
const ResultBody({super.key, required this.result, this.twoColumn = false});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ResultStatusBanner(result: result),
const SizedBox(height: 16),
if (result.isSuccess) ResultPersonInfoPanel(result: result, twoColumn: twoColumn),
if (!result.isSuccess) ResultErrorPanel(result: result),
],
);
}
}

@ -0,0 +1,39 @@
import 'package:finger_print_verification/core/network/network.dart';
import 'package:finger_print_verification/fingerprint/view/fingerprint.dart';
import 'package:finger_print_verification/theme/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() {
// Configure the global API client BEFORE runApp()
// All relative URLs (e.g. '/verify') will be resolved against this base URL.
// Replace the placeholder with your real endpoint before going to production.
ApiClient.initialize(
baseUrl: 'https://hmgpharmacyapi.hmg.com/fingerprint/',
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 30),
interceptors: [
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (object) => debugPrint(object.toString()), // Recommended for Flutter
),
]
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fingerprint Verification',
theme:AppTheme.light(),
home: const FingerprintView(),
);
}
}

@ -0,0 +1,256 @@
import 'package:flutter/material.dart';
///
/// "Futuristic Hospital — Dr. Sulaiman Al Habib (HMG)" design tokens.
///
/// LIGHT MODE variant: clean white clinical surfaces with HMG red (#D02127)
/// as the single brand accent used for primary actions, focus states,
/// active scanner glow, and status highlights. Panels are white cards with
/// soft red-tinted shadows/borders instead of the previous dark glass look,
/// keeping a crisp, futuristic-kiosk feel while staying bright and readable.
///
/// Drop this file in lib/theme/app_theme.dart and wrap MaterialApp with
/// `theme: AppTheme.light()`.
///
class AppTheme {
AppTheme._();
// Core brand colors HMG red & white
static const Color red = Color(0xFFD02127); // HMG primary brand red
static const Color redDark = Color(0xFFA8191F); // pressed / deep variant
static const Color redTint = Color(0xFFFCE9EA); // faint red surface tint
static const Color background = Color(0xFFFFFFFF); // page background
static const Color surface = Color(0xFFFFFFFF); // card surfaces
static const Color surfaceMuted = Color(0xFFF7F4F4); // sunken fields, chips
static const Color saudiGreen = Color(0xFF1E9E6B); // success / verified (semantic only)
static const Color amberAlert = Color(0xFFC8821A); // pending / connecting
static const Color redAlert = Color(0xFFD02127); // error reuses brand red
static const Color textPrimary = Color(0xFF1A1213);
static const Color textMuted = Color(0xFF7A6C6D);
static const Color hairline = Color(0xFFE7DCDD);
// Backward-compatible aliases anywhere older screen code references
// AppTheme.cyan / AppTheme.gold / AppTheme.ink*, it now resolves to the
// light-mode equivalents so existing screens compile unchanged.
static const Color cyan = red;
static const Color gold = red;
static const Color ink = background;
static const Color inkRaised = surface;
static const Color inkRaised2 = surfaceMuted;
static ThemeData light() {
final scheme = ColorScheme.fromSeed(
seedColor: red,
brightness: Brightness.light,
primary: red,
secondary: redDark,
error: redAlert,
surface: surface,
).copyWith(
surfaceContainerLowest: background,
surfaceContainerLow: surface,
surfaceContainer: surfaceMuted,
surfaceContainerHigh: surfaceMuted,
outline: hairline,
outlineVariant: hairline,
onSurface: textPrimary,
onSurfaceVariant: textMuted,
onPrimary: Colors.white,
);
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: scheme,
scaffoldBackgroundColor: background,
fontFamily: 'Roboto',
textTheme: const TextTheme().apply(
bodyColor: textPrimary,
displayColor: textPrimary,
),
appBarTheme: const AppBarTheme(
backgroundColor: background,
surfaceTintColor: Colors.transparent,
elevation: 0,
foregroundColor: textPrimary,
titleTextStyle: TextStyle(
color: textPrimary,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
cardTheme: CardThemeData(
color: surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: hairline),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surfaceMuted,
labelStyle: const TextStyle(color: textMuted),
hintStyle: const TextStyle(color: textMuted),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: hairline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: hairline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: red, width: 1.4),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: redAlert),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: red,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(54),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: surfaceMuted,
foregroundColor: textPrimary,
elevation: 0,
minimumSize: const Size.fromHeight(54),
side: const BorderSide(color: hairline),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
dividerTheme: const DividerThemeData(color: hairline, thickness: 1),
);
}
}
/// A reusable card-style panel with a soft red-tinted shadow/border the
/// signature surface treatment of this light theme. Replaces the old dark
/// "glow panel"; same API so existing screens compile unchanged.
class GlowPanel extends StatelessWidget {
final Widget child;
final Color glowColor;
final EdgeInsetsGeometry padding;
final double borderRadius;
const GlowPanel({
super.key,
required this.child,
this.glowColor = AppTheme.red,
this.padding = const EdgeInsets.all(20),
this.borderRadius = 20,
});
@override
Widget build(BuildContext context) {
return Container(
padding: padding,
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(color: glowColor.withValues(alpha: 0.22)),
boxShadow: [
BoxShadow(
color: glowColor.withValues(alpha: 0.10),
blurRadius: 20,
offset: const Offset(0, 6),
),
const BoxShadow(
color: Color(0x0A1A1213),
blurRadius: 12,
offset: Offset(0, 2),
),
],
),
child: child,
);
}
}
/// Subtle hex/grid backdrop to reinforce the "med-tech kiosk" feel without
/// hurting readability. Cheap to render pure CustomPaint, no images.
/// In light mode this is a very faint red grid on white.
class TechGridBackground extends StatelessWidget {
final Widget child;
const TechGridBackground({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: const Alignment(0.9, -0.9),
radius: 1.4,
colors: [
AppTheme.red.withValues(alpha: 0.05),
AppTheme.background,
],
),
),
),
),
Positioned.fill(child: CustomPaint(painter: _GridPainter())),
child,
],
);
}
}
class _GridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = AppTheme.red.withValues(alpha: 0.025)
..strokeWidth = 1;
const step = 36.0;
for (double x = 0; x < size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
}
for (double y = 0; y < size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Centers content and clamps width on tablets/desktop so forms don't
/// stretch edge-to-edge on large screens.
class ResponsiveCenter extends StatelessWidget {
final Widget child;
final double maxWidth;
const ResponsiveCenter({super.key, required this.child, this.maxWidth = 760});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: child,
),
);
}
}
/// Breakpoint helper.
bool isTablet(BuildContext context) =>
MediaQuery.sizeOf(context).shortestSide >= 600;

1
linux/.gitignore vendored

@ -0,0 +1 @@
flutter/ephemeral

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "finger_print_verification")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.cloudsolutions.finger_print_verification")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

@ -0,0 +1,130 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "finger_print_verification");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "finger_print_verification");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
macos/.gitignore vendored

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

@ -0,0 +1,12 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import connectivity_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
}

@ -0,0 +1,43 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save