├── .fvm ├── release ├── version ├── flutter_sdk └── fvm_config.json ├── ios ├── Assets │ └── .gitkeep ├── Resources │ └── PrivacyInfo.xcprivacy ├── .gitignore ├── Classes │ └── FlutterLivenessDetectionRandomizedPlugin.swift └── flutter_liveness_detection_randomized_plugin.podspec ├── .fvmrc ├── lib ├── src │ ├── core │ │ ├── enums │ │ │ ├── index.dart │ │ │ └── liveness_detection_step.dart │ │ ├── services │ │ │ ├── index.dart │ │ │ └── liveness_cooldown_service.dart │ │ ├── utils │ │ │ ├── index.dart │ │ │ └── machine_learning_kit_helper.dart │ │ ├── constants │ │ │ ├── index.dart │ │ │ └── liveness_detection_step_constant.dart │ │ └── index.dart │ ├── presentation │ │ ├── views │ │ │ ├── index.dart │ │ │ └── liveness_detection_view.dart │ │ └── widgets │ │ │ ├── index.dart │ │ │ ├── circular_progress_widget │ │ │ ├── circular_progress_painter.dart │ │ │ └── circular_progress_widget.dart │ │ │ ├── liveness_cooldown_widget.dart │ │ │ ├── liveness_detection_tutorial_widget.dart │ │ │ └── liveness_detection_step_overlay_widget.dart │ └── models │ │ ├── index.dart │ │ ├── liveness_detection_label_model.dart │ │ ├── liveness_detection_config.dart │ │ ├── liveness_detection_cooldown.dart │ │ ├── liveness_detection_step_item.dart │ │ └── liveness_detection_threshold.dart ├── flutter_liveness_detection_randomized_plugin_method_channel.dart ├── index.dart ├── flutter_liveness_detection_randomized_plugin_platform_interface.dart └── flutter_liveness_detection_randomized_plugin.dart ├── .vscode ├── settings.json └── launch.json ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── Podfile │ └── Podfile.lock ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── bagussubagja │ │ │ │ │ │ └── flutter_liveness_detection_randomized_plugin │ │ │ │ │ │ └── flutter_liveness_detection_randomized_plugin_example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── integration_test │ └── plugin_integration_test.dart ├── analysis_options.yaml ├── pubspec.yaml ├── lib │ └── main.dart └── pubspec.lock ├── android ├── settings.gradle ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── bagussubagja │ │ │ └── flutter_liveness_detection_randomized_plugin │ │ │ └── flutter_liveness_detection_randomized_plugin │ │ │ └── FlutterLivenessDetectionRandomizedPlugin.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── bagussubagja │ │ └── flutter_liveness_detection_randomized_plugin │ │ └── flutter_liveness_detection_randomized_plugin │ │ └── FlutterLivenessDetectionRandomizedPluginTest.kt └── build.gradle ├── analysis_options.yaml ├── .gitignore ├── test ├── flutter_liveness_detection_randomized_plugin_method_channel_test.dart └── flutter_liveness_detection_randomized_plugin_test.dart ├── LICENSE ├── .metadata ├── pubspec.yaml ├── example_customized_label_usage.dart ├── .github └── workflows │ └── ci-cd.yml ├── CHANGELOG.md └── README.md /.fvm/release: -------------------------------------------------------------------------------- 1 | 3.38.1 -------------------------------------------------------------------------------- /.fvm/version: -------------------------------------------------------------------------------- 1 | 3.38.1 -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.38.1" 3 | } -------------------------------------------------------------------------------- /.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | /Users/bagussubagja/fvm/versions/3.38.1 -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.38.1" 3 | } -------------------------------------------------------------------------------- /lib/src/core/enums/index.dart: -------------------------------------------------------------------------------- 1 | export 'liveness_detection_step.dart'; -------------------------------------------------------------------------------- /lib/src/core/services/index.dart: -------------------------------------------------------------------------------- 1 | export 'liveness_cooldown_service.dart'; -------------------------------------------------------------------------------- /lib/src/core/utils/index.dart: -------------------------------------------------------------------------------- 1 | export 'machine_learning_kit_helper.dart'; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/versions/3.38.1" 3 | } -------------------------------------------------------------------------------- /lib/src/core/constants/index.dart: -------------------------------------------------------------------------------- 1 | export 'liveness_detection_step_constant.dart'; -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | package android 2 | 3 | rootProject.name = 'flutter_liveness_detection_randomized_plugin' -------------------------------------------------------------------------------- /lib/src/presentation/views/index.dart: -------------------------------------------------------------------------------- 1 | export '../widgets/index.dart'; 2 | export 'liveness_detection_view.dart'; 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /lib/src/core/enums/liveness_detection_step.dart: -------------------------------------------------------------------------------- 1 | enum LivenessDetectionStep { 2 | blink, 3 | lookRight, 4 | lookLeft, 5 | lookUp, 6 | lookDown, 7 | smile 8 | } -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /lib/src/presentation/widgets/index.dart: -------------------------------------------------------------------------------- 1 | export 'liveness_detection_tutorial_widget.dart'; 2 | export 'liveness_detection_step_overlay_widget.dart'; 3 | export 'liveness_cooldown_widget.dart'; -------------------------------------------------------------------------------- /lib/src/core/index.dart: -------------------------------------------------------------------------------- 1 | export '../presentation/views/index.dart'; 2 | export '../models/index.dart'; 3 | export 'enums/index.dart'; 4 | export 'utils/index.dart'; 5 | export 'services/index.dart'; 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bagussubagja/flutter-liveness-detection-randomized-plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/src/models/index.dart: -------------------------------------------------------------------------------- 1 | export 'liveness_detection_label_model.dart'; 2 | export 'liveness_detection_step_item.dart'; 3 | export 'liveness_detection_config.dart'; 4 | export 'liveness_detection_threshold.dart'; 5 | export 'liveness_detection_cooldown.dart'; -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 6 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/bagussubagja/flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTrackingDomains 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTracking 12 | 13 | NSCameraUsageDescription 14 | Camera access is required for liveness detection 15 | 16 | 17 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | build/ 30 | 31 | # FVM Version Cache 32 | .fvm/ -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_liveness_detection_randomized_plugin_example 2 | 3 | Demonstrates how to use the flutter_liveness_detection_randomized_plugin plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Classes/FlutterLivenessDetectionRandomizedPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class FlutterLivenessDetectionRandomizedPlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | let channel = FlutterMethodChannel(name: "flutter_liveness_detection_randomized_plugin", binaryMessenger: registrar.messenger()) 7 | let instance = FlutterLivenessDetectionRandomizedPlugin() 8 | registrar.addMethodCallDelegate(instance, channel: channel) 9 | } 10 | 11 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 12 | switch call.method { 13 | case "getPlatformVersion": 14 | result("iOS " + UIDevice.current.systemVersion) 15 | default: 16 | result(FlutterMethodNotImplemented) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.6.0" apply false 22 | id "org.jetbrains.kotlin.android" version "2.1.0" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /lib/flutter_liveness_detection_randomized_plugin_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'flutter_liveness_detection_randomized_plugin_platform_interface.dart'; 5 | 6 | /// An implementation of [FlutterLivenessDetectionRandomizedPluginPlatform] that uses method channels. 7 | class MethodChannelFlutterLivenessDetectionRandomizedPlugin extends FlutterLivenessDetectionRandomizedPluginPlatform { 8 | /// The method channel used to interact with the native platform. 9 | @visibleForTesting 10 | final methodChannel = const MethodChannel('flutter_liveness_detection_randomized_plugin'); 11 | 12 | @override 13 | Future getPlatformVersion() async { 14 | final version = await methodChannel.invokeMethod('getPlatformVersion'); 15 | return version; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/core/constants/liveness_detection_step_constant.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | List stepLiveness = [ 4 | LivenessDetectionStepItem( 5 | step: LivenessDetectionStep.blink, 6 | title: "Blink 2-3 Times", 7 | ), 8 | LivenessDetectionStepItem( 9 | step: LivenessDetectionStep.lookUp, 10 | title: "Look UP", 11 | ), 12 | LivenessDetectionStepItem( 13 | step: LivenessDetectionStep.lookDown, 14 | title: "Look DOWN", 15 | ), 16 | LivenessDetectionStepItem( 17 | step: LivenessDetectionStep.lookRight, 18 | title: "Look RIGHT", 19 | ), 20 | LivenessDetectionStepItem( 21 | step: LivenessDetectionStep.lookLeft, 22 | title: "Look LEFT", 23 | ), 24 | LivenessDetectionStepItem( 25 | step: LivenessDetectionStep.smile, 26 | title: "Smile", 27 | ), 28 | ]; 29 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | 6 | @testable import flutter_liveness_detection_randomized_plugin 7 | 8 | // This demonstrates a simple unit test of the Swift portion of this plugin's implementation. 9 | // 10 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 11 | 12 | class RunnerTests: XCTestCase { 13 | 14 | func testGetPlatformVersion() { 15 | let plugin = FlutterLivenessDetectionRandomizedPlugin() 16 | 17 | let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) 18 | 19 | let resultExpectation = expectation(description: "result block must be called.") 20 | plugin.handle(call) { result in 21 | XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) 22 | resultExpectation.fulfill() 23 | } 24 | waitForExpectations(timeout: 1) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_liveness_detection_randomized_plugin_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const HomeView()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /test/flutter_liveness_detection_randomized_plugin_method_channel_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin_method_channel.dart'; 4 | 5 | void main() { 6 | TestWidgetsFlutterBinding.ensureInitialized(); 7 | 8 | MethodChannelFlutterLivenessDetectionRandomizedPlugin platform = MethodChannelFlutterLivenessDetectionRandomizedPlugin(); 9 | const MethodChannel channel = MethodChannel('flutter_liveness_detection_randomized_plugin'); 10 | 11 | setUp(() { 12 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( 13 | channel, 14 | (MethodCall methodCall) async { 15 | return '42'; 16 | }, 17 | ); 18 | }); 19 | 20 | tearDown(() { 21 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); 22 | }); 23 | 24 | test('getPlatformVersion', () async { 25 | expect(await platform.getPlatformVersion(), '42'); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bagus Subagja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/integration_test/plugin_integration_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter integration test. 2 | // 3 | // Since integration tests run in a full Flutter application, they can interact 4 | // with the host side of a plugin implementation, unlike Dart unit tests. 5 | // 6 | // For more information about Flutter integration tests, please see 7 | // https://flutter.dev/to/integration-testing 8 | 9 | 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:integration_test/integration_test.dart'; 12 | 13 | import 'package:flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin.dart'; 14 | 15 | void main() { 16 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 17 | 18 | testWidgets('getPlatformVersion test', (WidgetTester tester) async { 19 | final FlutterLivenessDetectionRandomizedPlugin plugin = FlutterLivenessDetectionRandomizedPlugin.instance; 20 | final String? version = await plugin.getPlatformVersion(); 21 | // The version string depends on the host platform running the test, so 22 | // just assert that some non-empty string is returned. 23 | expect(version?.isNotEmpty, true); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" 8 | channel: "stable" 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | - platform: ios 22 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 23 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /lib/index.dart: -------------------------------------------------------------------------------- 1 | export 'dart:convert'; 2 | export 'dart:io'; 3 | export 'dart:async'; 4 | export 'dart:math'; 5 | export 'package:camera/camera.dart'; 6 | export 'package:equatable/equatable.dart'; 7 | export 'package:flutter/material.dart'; 8 | export 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; 9 | export 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_step_item.dart'; 10 | export 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_config.dart'; 11 | export 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_threshold.dart'; 12 | export 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_cooldown.dart'; 13 | export 'package:flutter_liveness_detection_randomized_plugin/src/core/services/liveness_cooldown_service.dart'; 14 | export 'package:flutter_liveness_detection_randomized_plugin/src/presentation/widgets/liveness_cooldown_widget.dart'; 15 | 16 | export 'src/core/index.dart'; 17 | export './flutter_liveness_detection_randomized_plugin.dart'; 18 | export './flutter_liveness_detection_randomized_plugin_method_channel.dart'; 19 | export './flutter_liveness_detection_randomized_plugin_platform_interface.dart'; -------------------------------------------------------------------------------- /lib/src/core/utils/machine_learning_kit_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | class MachineLearningKitHelper { 4 | MachineLearningKitHelper._privateConstructor(); 5 | static final MachineLearningKitHelper instance = 6 | MachineLearningKitHelper._privateConstructor(); 7 | 8 | final FaceDetector faceDetector = FaceDetector( 9 | options: FaceDetectorOptions( 10 | enableContours: true, 11 | enableClassification: true, 12 | enableLandmarks: true, 13 | enableTracking: true, 14 | performanceMode: FaceDetectorMode.accurate, 15 | ), 16 | ); 17 | 18 | Future> processInputImage(InputImage imgFile) async { 19 | const maxAttempts = 3; 20 | 21 | for (var attempt = 0; attempt < maxAttempts; attempt++) { 22 | try { 23 | final List faces = await faceDetector.processImage(imgFile); 24 | if (faces.isNotEmpty) return faces; 25 | } catch (e) { 26 | debugPrint('Face detection error (attempt ${attempt + 1}): $e'); 27 | if (e.toString().contains('InputImageConverterError') || 28 | e.toString().contains('ImageFormat is not supported')) { 29 | return []; 30 | } 31 | } 32 | } 33 | 34 | return []; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/models/liveness_detection_label_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | LivenessDetectionLabelModel livenessDetectionLabelModelFromJson(String str) => LivenessDetectionLabelModel.fromJson(json.decode(str)); 4 | 5 | String livenessDetectionLabelModelToJson(LivenessDetectionLabelModel data) => json.encode(data.toJson()); 6 | 7 | class LivenessDetectionLabelModel { 8 | String? smile; 9 | String? lookUp; 10 | String? lookDown; 11 | String? lookLeft; 12 | String? lookRight; 13 | String? blink; 14 | 15 | LivenessDetectionLabelModel({ 16 | this.smile, 17 | this.lookUp, 18 | this.lookDown, 19 | this.lookLeft, 20 | this.lookRight, 21 | this.blink, 22 | }); 23 | 24 | factory LivenessDetectionLabelModel.fromJson(Map json) => LivenessDetectionLabelModel( 25 | smile: json["smile"], 26 | lookUp: json["lookUp"], 27 | lookDown: json["lookDown"], 28 | lookLeft: json["lookLeft"], 29 | lookRight: json["lookRight"], 30 | blink: json["blink"], 31 | ); 32 | 33 | Map toJson() => { 34 | "smile": smile, 35 | "lookUp": lookUp, 36 | "lookDown": lookDown, 37 | "lookLeft": lookLeft, 38 | "lookRight": lookRight, 39 | "blink": blink, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/bagussubagja/flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin/FlutterLivenessDetectionRandomizedPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin 2 | 3 | import io.flutter.plugin.common.MethodCall 4 | import io.flutter.plugin.common.MethodChannel 5 | import kotlin.test.Test 6 | import org.mockito.Mockito 7 | 8 | /* 9 | * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. 10 | * 11 | * Once you have built the plugin's example app, you can run these tests from the command 12 | * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or 13 | * you can run them directly from IDEs that support JUnit such as Android Studio. 14 | */ 15 | 16 | internal class FlutterLivenessDetectionRandomizedPluginTest { 17 | @Test 18 | fun onMethodCall_getPlatformVersion_returnsExpectedValue() { 19 | val plugin = FlutterLivenessDetectionRandomizedPlugin() 20 | 21 | val call = MethodCall("getPlatformVersion", null) 22 | val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) 23 | plugin.onMethodCall(call, mockResult) 24 | 25 | Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_liveness_detection_randomized_plugin 2 | description: "A Flutter plugin for liveness detection with randomized challenge response method" 3 | version: 1.1.0 4 | homepage: "https://bagussubagja.vercel.app" 5 | repository: "https://github.com/bagussubagja/flutter-liveness-detection-randomized-plugin" 6 | 7 | environment: 8 | sdk: '>=3.10.0 <4.0.0' 9 | flutter: '>=3.38.1' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | plugin_platform_interface: ^2.0.2 15 | google_mlkit_face_detection: ^0.13.1 16 | camera: 0.10.6 17 | equatable: ^2.0.5 18 | image: ^4.0.15 19 | path_provider: ^2.1.3 20 | lottie: ^2.4.0 21 | collection: ^1.18.0 22 | screen_brightness: ^2.1.2 23 | shared_preferences: ^2.2.2 24 | 25 | dependency_overrides: 26 | camera_android_camerax: 0.6.10+1 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | flutter_lints: ^4.0.0 32 | 33 | flutter: 34 | plugin: 35 | platforms: 36 | android: 37 | package: com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin 38 | pluginClass: FlutterLivenessDetectionRandomizedPlugin 39 | ios: 40 | pluginClass: FlutterLivenessDetectionRandomizedPlugin 41 | 42 | assets: 43 | - packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-id-anim.json 44 | - packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-detected.json -------------------------------------------------------------------------------- /lib/flutter_liveness_detection_randomized_plugin_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 2 | 3 | import 'flutter_liveness_detection_randomized_plugin_method_channel.dart'; 4 | 5 | abstract class FlutterLivenessDetectionRandomizedPluginPlatform extends PlatformInterface { 6 | /// Constructs a FlutterLivenessDetectionRandomizedPluginPlatform. 7 | FlutterLivenessDetectionRandomizedPluginPlatform() : super(token: _token); 8 | 9 | static final Object _token = Object(); 10 | 11 | static FlutterLivenessDetectionRandomizedPluginPlatform _instance = MethodChannelFlutterLivenessDetectionRandomizedPlugin(); 12 | 13 | /// The default instance of [FlutterLivenessDetectionRandomizedPluginPlatform] to use. 14 | /// 15 | /// Defaults to [MethodChannelFlutterLivenessDetectionRandomizedPlugin]. 16 | static FlutterLivenessDetectionRandomizedPluginPlatform get instance => _instance; 17 | 18 | /// Platform-specific implementations should set this with their own 19 | /// platform-specific class that extends [FlutterLivenessDetectionRandomizedPluginPlatform] when 20 | /// they register themselves. 21 | static set instance(FlutterLivenessDetectionRandomizedPluginPlatform instance) { 22 | PlatformInterface.verifyToken(instance, _token); 23 | _instance = instance; 24 | } 25 | 26 | Future getPlatformVersion() { 27 | throw UnimplementedError('platformVersion() has not been implemented.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "flutter-liveness-detection-randomized-plugin", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "flutter-liveness-detection-randomized-plugin (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "flutter-liveness-detection-randomized-plugin (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | }, 24 | { 25 | "name": "example", 26 | "cwd": "example", 27 | "request": "launch", 28 | "type": "dart" 29 | }, 30 | { 31 | "name": "example (profile mode)", 32 | "cwd": "example", 33 | "request": "launch", 34 | "type": "dart", 35 | "flutterMode": "profile" 36 | }, 37 | { 38 | "name": "example (release mode)", 39 | "cwd": "example", 40 | "request": "launch", 41 | "type": "dart", 42 | "flutterMode": "release" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/src/models/liveness_detection_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:camera/camera.dart'; 2 | import 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_label_model.dart'; 3 | 4 | class LivenessDetectionConfig { 5 | final bool startWithInfoScreen; 6 | final int? durationLivenessVerify; 7 | final bool showDurationUiText; 8 | final bool useCustomizedLabel; 9 | final LivenessDetectionLabelModel? customizedLabel; 10 | final bool isEnableMaxBrightness; 11 | final int imageQuality; 12 | final ResolutionPreset cameraResolution; 13 | final bool enableCooldownOnFailure; 14 | final int maxFailedAttempts; 15 | final int cooldownMinutes; 16 | final bool isEnableSnackBar; 17 | final bool shuffleListWithSmileLast; 18 | final bool showCurrentStep; 19 | final bool isDarkMode; 20 | 21 | LivenessDetectionConfig({ 22 | this.startWithInfoScreen = false, 23 | this.durationLivenessVerify = 45, 24 | this.showDurationUiText = false, 25 | this.useCustomizedLabel = false, 26 | this.customizedLabel, 27 | this.isEnableMaxBrightness = true, 28 | this.imageQuality = 100, 29 | this.cameraResolution = ResolutionPreset.high, 30 | this.enableCooldownOnFailure = true, 31 | this.maxFailedAttempts = 3, 32 | this.cooldownMinutes = 10, 33 | this.isEnableSnackBar = true, 34 | this.shuffleListWithSmileLast = true, 35 | this.showCurrentStep = false, 36 | this.isDarkMode = true, 37 | }) : assert( 38 | !useCustomizedLabel || customizedLabel != null, 39 | 'customizedLabel must not be null when useCustomizedLabel is true', 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /ios/flutter_liveness_detection_randomized_plugin.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flutter_liveness_detection_randomized_plugin.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_liveness_detection_randomized_plugin' 7 | s.version = '0.0.1' 8 | s.summary = 'A Flutter plugin for liveness detection with randomized challenge response method' 9 | s.description = <<-DESC 10 | A Flutter plugin for liveness detection with randomized challenge response method 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '12.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | 24 | # If your plugin requires a privacy manifest, for example if it uses any 25 | # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your 26 | # plugin's privacy impact, and then uncomment this line. For more information, 27 | # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files 28 | # s.resource_bundles = {'flutter_liveness_detection_randomized_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} 29 | end 30 | -------------------------------------------------------------------------------- /lib/src/models/liveness_detection_cooldown.dart: -------------------------------------------------------------------------------- 1 | class LivenessDetectionCooldown { 2 | final int failedAttempts; 3 | final DateTime? cooldownEndTime; 4 | final bool isInCooldown; 5 | 6 | const LivenessDetectionCooldown({ 7 | this.failedAttempts = 0, 8 | this.cooldownEndTime, 9 | this.isInCooldown = false, 10 | }); 11 | 12 | LivenessDetectionCooldown copyWith({ 13 | int? failedAttempts, 14 | DateTime? cooldownEndTime, 15 | bool? isInCooldown, 16 | }) { 17 | return LivenessDetectionCooldown( 18 | failedAttempts: failedAttempts ?? this.failedAttempts, 19 | cooldownEndTime: cooldownEndTime ?? this.cooldownEndTime, 20 | isInCooldown: isInCooldown ?? this.isInCooldown, 21 | ); 22 | } 23 | 24 | Duration get remainingCooldownTime { 25 | if (cooldownEndTime == null || !isInCooldown) { 26 | return Duration.zero; 27 | } 28 | final remaining = cooldownEndTime!.difference(DateTime.now()); 29 | return remaining.isNegative ? Duration.zero : remaining; 30 | } 31 | 32 | Map toJson() { 33 | return { 34 | 'failedAttempts': failedAttempts, 35 | 'cooldownEndTime': cooldownEndTime?.millisecondsSinceEpoch, 36 | 'isInCooldown': isInCooldown, 37 | }; 38 | } 39 | 40 | factory LivenessDetectionCooldown.fromJson(Map json) { 41 | return LivenessDetectionCooldown( 42 | failedAttempts: json['failedAttempts'] ?? 0, 43 | cooldownEndTime: json['cooldownEndTime'] != null 44 | ? DateTime.fromMillisecondsSinceEpoch(json['cooldownEndTime']) 45 | : null, 46 | isInCooldown: json['isInCooldown'] ?? false, 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /test/flutter_liveness_detection_randomized_plugin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin.dart'; 3 | import 'package:flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin_platform_interface.dart'; 4 | import 'package:flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin_method_channel.dart'; 5 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 6 | 7 | class MockFlutterLivenessDetectionRandomizedPluginPlatform 8 | with MockPlatformInterfaceMixin 9 | implements FlutterLivenessDetectionRandomizedPluginPlatform { 10 | 11 | @override 12 | Future getPlatformVersion() => Future.value('42'); 13 | } 14 | 15 | void main() { 16 | final FlutterLivenessDetectionRandomizedPluginPlatform initialPlatform = FlutterLivenessDetectionRandomizedPluginPlatform.instance; 17 | 18 | test('$MethodChannelFlutterLivenessDetectionRandomizedPlugin is the default instance', () { 19 | expect(initialPlatform, isInstanceOf()); 20 | }); 21 | 22 | test('getPlatformVersion', () async { 23 | FlutterLivenessDetectionRandomizedPlugin flutterLivenessDetectionRandomizedPlugin = FlutterLivenessDetectionRandomizedPlugin.instance; 24 | MockFlutterLivenessDetectionRandomizedPluginPlatform fakePlatform = MockFlutterLivenessDetectionRandomizedPluginPlatform(); 25 | FlutterLivenessDetectionRandomizedPluginPlatform.instance = fakePlatform; 26 | 27 | expect(await flutterLivenessDetectionRandomizedPlugin.getPlatformVersion(), '42'); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/bagussubagja/flutter_liveness_detection_randomized_plugin/flutter_liveness_detection_randomized_plugin/FlutterLivenessDetectionRandomizedPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin 2 | 3 | import androidx.annotation.NonNull 4 | 5 | import io.flutter.embedding.engine.plugins.FlutterPlugin 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel 8 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 9 | import io.flutter.plugin.common.MethodChannel.Result 10 | 11 | /** FlutterLivenessDetectionRandomizedPlugin */ 12 | class FlutterLivenessDetectionRandomizedPlugin: FlutterPlugin, MethodCallHandler { 13 | /// The MethodChannel that will the communication between Flutter and native Android 14 | /// 15 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 16 | /// when the Flutter Engine is detached from the Activity 17 | private lateinit var channel : MethodChannel 18 | 19 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 20 | channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_liveness_detection_randomized_plugin") 21 | channel.setMethodCallHandler(this) 22 | } 23 | 24 | override fun onMethodCall(call: MethodCall, result: Result) { 25 | if (call.method == "getPlatformVersion") { 26 | result.success("Android ${android.os.Build.VERSION.RELEASE}") 27 | } else { 28 | result.notImplemented() 29 | } 30 | } 31 | 32 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 33 | channel.setMethodCallHandler(null) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin_example" 10 | compileSdk = 36 11 | ndkVersion = "28.2.13676358" 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin_example" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = 24 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | packagingOptions { 34 | jniLibs { 35 | useLegacyPackaging true 36 | } 37 | } 38 | 39 | buildTypes { 40 | release { 41 | // TODO: Add your own signing config for the release build. 42 | // Signing with the debug keys for now, so `flutter run --release` works. 43 | signingConfig = signingConfigs.debug 44 | } 45 | } 46 | } 47 | 48 | flutter { 49 | source = "../.." 50 | } 51 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Flutter Liveness Detection Randomized Plugin 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_liveness_detection_randomized_plugin_example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | NSCameraUsageDescription 49 | Camera access is required for liveness detection 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/src/models/liveness_detection_step_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | class LivenessDetectionStepItem { 4 | final LivenessDetectionStep step; 5 | final String title; 6 | final double? thresholdToCheck; 7 | 8 | LivenessDetectionStepItem({ 9 | required this.step, 10 | required this.title, 11 | this.thresholdToCheck, 12 | }); 13 | 14 | LivenessDetectionStepItem copyWith({ 15 | LivenessDetectionStep? step, 16 | String? title, 17 | double? thresholdToCheck, 18 | }) { 19 | return LivenessDetectionStepItem( 20 | step: step ?? this.step, 21 | title: title ?? this.title, 22 | thresholdToCheck: thresholdToCheck ?? this.thresholdToCheck, 23 | ); 24 | } 25 | Map toMap() { 26 | final result = {}; 27 | 28 | result.addAll({'step': step.index}); 29 | result.addAll({'title': title}); 30 | if (thresholdToCheck != null) { 31 | result.addAll({'thresholdToCheck': thresholdToCheck}); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | factory LivenessDetectionStepItem.fromMap(Map map) { 38 | return LivenessDetectionStepItem( 39 | step: LivenessDetectionStep.values[map['step'] ?? 0], 40 | title: map['title'] ?? '', 41 | thresholdToCheck: map['thresholdToCheck']?.toDouble(), 42 | ); 43 | } 44 | 45 | String toJson() => json.encode(toMap()); 46 | 47 | factory LivenessDetectionStepItem.fromJson(String source) => 48 | LivenessDetectionStepItem.fromMap(json.decode(source)); 49 | 50 | @override 51 | String toString() { 52 | return 'Liveness Detection (step: $step, title: $title, thresholdToCheck: $thresholdToCheck)'; 53 | } 54 | 55 | @override 56 | bool operator ==(Object other) { 57 | if (identical(this, other)) return true; 58 | 59 | return other is LivenessDetectionStepItem && 60 | other.step == step && 61 | other.title == title && 62 | other.thresholdToCheck == thresholdToCheck; 63 | } 64 | 65 | @override 66 | int get hashCode { 67 | return step.hashCode ^ 68 | title.hashCode ^ 69 | thresholdToCheck.hashCode; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/flutter_liveness_detection_randomized_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | class FlutterLivenessDetectionRandomizedPlugin { 4 | FlutterLivenessDetectionRandomizedPlugin._privateConstructor(); 5 | static final FlutterLivenessDetectionRandomizedPlugin instance = 6 | FlutterLivenessDetectionRandomizedPlugin._privateConstructor(); 7 | final List _thresholds = []; 8 | 9 | List get thresholdConfig { 10 | return _thresholds; 11 | } 12 | 13 | Future livenessDetection({ 14 | required BuildContext context, 15 | required LivenessDetectionConfig config, 16 | }) async { 17 | if (config.enableCooldownOnFailure) { 18 | LivenessCooldownService.instance.configure( 19 | maxFailedAttempts: config.maxFailedAttempts, 20 | cooldownMinutes: config.cooldownMinutes, 21 | ); 22 | final cooldownState = await LivenessCooldownService.instance.getCooldownState(); 23 | if (cooldownState.isInCooldown && context.mounted) { 24 | await Navigator.of(context).push( 25 | MaterialPageRoute( 26 | builder: (context) => LivenessCooldownWidget( 27 | cooldownState: cooldownState, 28 | isDarkMode: config.isDarkMode, 29 | maxFailedAttempts: config.maxFailedAttempts, 30 | ), 31 | ), 32 | ); 33 | return null; 34 | } 35 | } 36 | 37 | if (!context.mounted) return null; 38 | 39 | final String? capturedFacePath = await Navigator.of(context).push( 40 | MaterialPageRoute( 41 | builder: (context) => LivenessDetectionView( 42 | config: config, 43 | ), 44 | ), 45 | ); 46 | 47 | if (config.enableCooldownOnFailure) { 48 | if (capturedFacePath != null) { 49 | await LivenessCooldownService.instance.recordSuccessfulAttempt(); 50 | } else { 51 | await LivenessCooldownService.instance.recordFailedAttempt(); 52 | } 53 | } 54 | 55 | return capturedFacePath; 56 | } 57 | 58 | Future getPlatformVersion() { 59 | return FlutterLivenessDetectionRandomizedPluginPlatform.instance 60 | .getPlatformVersion(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group = "com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin" 2 | version = "1.0-SNAPSHOT" 3 | 4 | buildscript { 5 | ext.kotlin_version = "1.8.22" 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath("com.android.tools.build:gradle:8.1.0") 13 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: "com.android.library" 25 | apply plugin: "kotlin-android" 26 | 27 | android { 28 | if (project.android.hasProperty("namespace")) { 29 | namespace = "com.bagussubagja.flutter_liveness_detection_randomized_plugin.flutter_liveness_detection_randomized_plugin" 30 | } 31 | 32 | compileSdk = 34 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | 39 | packagingOptions { 40 | jniLibs { 41 | useLegacyPackaging true 42 | } 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = JavaVersion.VERSION_1_8 47 | } 48 | 49 | sourceSets { 50 | main.java.srcDirs += "src/main/kotlin" 51 | test.java.srcDirs += "src/test/kotlin" 52 | } 53 | 54 | defaultConfig { 55 | minSdk = 21 56 | } 57 | 58 | dependencies { 59 | testImplementation("org.jetbrains.kotlin:kotlin-test") 60 | testImplementation("org.mockito:mockito-core:5.0.0") 61 | } 62 | 63 | testOptions { 64 | unitTests.all { 65 | useJUnitPlatform() 66 | 67 | testLogging { 68 | events "passed", "skipped", "failed", "standardOut", "standardError" 69 | outputs.upToDateWhen {false} 70 | showStandardStreams = true 71 | } 72 | } 73 | } 74 | } 75 | 76 | // Add this block for subprojects 77 | subprojects { 78 | afterEvaluate { project -> 79 | if (project.hasProperty('android')) { 80 | project.android { 81 | if (namespace == null) { 82 | namespace project.group 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /example_customized_label_usage.dart: -------------------------------------------------------------------------------- 1 | // Example of correct customizedLabel usage 2 | 3 | // ignore_for_file: unused_local_variable 4 | 5 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 6 | 7 | void exampleUsage() { 8 | // ✅ CORRECT: Skip blink and lookDown, use custom labels for others 9 | final config1 = LivenessDetectionConfig( 10 | useCustomizedLabel: true, 11 | customizedLabel: LivenessDetectionLabelModel( 12 | blink: '', // Empty string = skip this challenge 13 | lookUp: 'Tengok Atas', // Custom label 14 | lookDown: '', // Empty string = skip this challenge 15 | lookLeft: null, // null = use default label "Look LEFT" 16 | lookRight: null, // null = use default label "Look RIGHT" 17 | smile: 'Senyum Dong!', // Custom label 18 | ), 19 | ); 20 | // Result: Only lookUp, lookLeft, lookRight, smile challenges will appear 21 | 22 | // ✅ CORRECT: Use all challenges with custom labels 23 | final config2 = LivenessDetectionConfig( 24 | useCustomizedLabel: true, 25 | customizedLabel: LivenessDetectionLabelModel( 26 | blink: 'Kedipkan Mata', 27 | lookUp: 'Lihat Atas', 28 | lookDown: 'Lihat Bawah', 29 | lookLeft: 'Lihat Kiri', 30 | lookRight: 'Lihat Kanan', 31 | smile: 'Tersenyum', 32 | ), 33 | ); 34 | // Result: All 6 challenges with Indonesian labels 35 | 36 | // ✅ CORRECT: Mix of custom, default, and skipped 37 | final config3 = LivenessDetectionConfig( 38 | useCustomizedLabel: true, 39 | customizedLabel: LivenessDetectionLabelModel( 40 | blink: null, // Use default "Blink 2-3 Times" 41 | lookUp: '', // Skip 42 | lookDown: '', // Skip 43 | lookLeft: 'Turn Left Please', 44 | lookRight: 'Turn Right Please', 45 | smile: null, // Use default "Smile" 46 | ), 47 | ); 48 | // Result: Only blink, lookLeft, lookRight, smile challenges 49 | 50 | // ❌ WRONG: This will throw assertion error 51 | // final configWrong = LivenessDetectionConfig( 52 | // useCustomizedLabel: true, 53 | // customizedLabel: null, // ERROR: Cannot be null when useCustomizedLabel is true 54 | // ); 55 | 56 | // ✅ CORRECT: Use default behavior 57 | final config4 = LivenessDetectionConfig( 58 | useCustomizedLabel: false, // customizedLabel will be ignored 59 | shuffleListWithSmileLast: true, // This works with default steps 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | analyze: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Flutter 16 | uses: subosito/flutter-action@v2 17 | with: 18 | flutter-version: '3.24.5' 19 | channel: 'stable' 20 | 21 | - name: Get dependencies 22 | run: flutter pub get 23 | 24 | - name: Analyze code 25 | run: flutter analyze --fatal-infos 26 | 27 | - name: Run tests 28 | run: flutter test 29 | 30 | build-and-release: 31 | needs: analyze 32 | runs-on: ubuntu-latest 33 | if: github.ref == 'refs/heads/master' && github.event_name == 'push' 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Setup Flutter 39 | uses: subosito/flutter-action@v2 40 | with: 41 | flutter-version: '3.24.5' 42 | channel: 'stable' 43 | 44 | - name: Setup Java 45 | uses: actions/setup-java@v4 46 | with: 47 | distribution: 'zulu' 48 | java-version: '17' 49 | 50 | - name: Get plugin dependencies 51 | run: flutter pub get 52 | 53 | - name: Get example dependencies 54 | working-directory: example 55 | run: flutter pub get 56 | 57 | - name: Build Android APK 58 | working-directory: example 59 | run: flutter build apk --release 60 | 61 | - name: Extract version from pubspec.yaml 62 | id: version 63 | run: | 64 | VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //') 65 | echo "version=$VERSION" >> $GITHUB_OUTPUT 66 | echo "Version: $VERSION" 67 | 68 | - name: Create Release 69 | uses: softprops/action-gh-release@v1 70 | with: 71 | tag_name: v${{ steps.version.outputs.version }} 72 | name: Release v${{ steps.version.outputs.version }} 73 | body: | 74 | ## Flutter Liveness Detection Plugin v${{ steps.version.outputs.version }} 75 | 76 | ### Demo App 77 | Download the demo app to try the liveness detection features: 78 | - Android APK: See assets below 79 | 80 | ### Changes 81 | See [CHANGELOG.md](CHANGELOG.md) for detailed changes. 82 | files: | 83 | example/build/app/outputs/flutter-apk/app-release.apk 84 | draft: false 85 | prerelease: false -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/presentation/widgets/circular_progress_widget/circular_progress_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class CircularProgressPainter extends CustomPainter { 6 | final double currentStep; 7 | final double maxStep; 8 | final double widthLine; 9 | final double heightLine; 10 | final Color? selectedColor; 11 | final Color? unselectedColor; 12 | final Gradient? gradientColor; 13 | 14 | CircularProgressPainter({ 15 | required this.maxStep, 16 | required this.widthLine, 17 | required this.heightLine, 18 | required this.currentStep, 19 | required this.selectedColor, 20 | required this.unselectedColor, 21 | required this.gradientColor, 22 | }); 23 | double get maxDefinedSize { 24 | return math.max(1, math.max(0, 0)); 25 | } 26 | 27 | @override 28 | void paint(Canvas canvas, Size size) { 29 | final w = size.width; 30 | final h = size.height; 31 | Paint paint = Paint()..style = PaintingStyle.stroke; 32 | 33 | final rect = Rect.fromCenter( 34 | center: Offset(w / 2, h / 2), 35 | height: h - maxDefinedSize, 36 | width: w - maxDefinedSize, 37 | ); 38 | 39 | if (gradientColor != null) { 40 | paint.shader = gradientColor!.createShader(rect); 41 | } 42 | _drawStepArc(canvas, paint, rect, size); 43 | } 44 | 45 | /// Draw a series of arcs, each composing the full steps of the indicator 46 | void _drawStepArc( 47 | Canvas canvas, 48 | Paint paint, 49 | Rect rect, 50 | Size size, 51 | ) { 52 | var centerX = rect.center.dx; 53 | var centerY = rect.center.dy; 54 | var radius = math.min(centerX, centerY); 55 | 56 | var draw = (360 * currentStep) / maxStep; 57 | var stepLine = 360 / maxStep; 58 | 59 | for (double i = 0; i < 360; i += stepLine) { 60 | var outerCircleRadius = (radius - (i < draw ? 0 : heightLine / 2)); 61 | var innerCircleRadius = (radius - heightLine); 62 | 63 | var x1 = centerX + outerCircleRadius * math.cos(i * math.pi / 180); 64 | var y1 = centerX + outerCircleRadius * math.sin(i * math.pi / 180); 65 | // 66 | var x2 = centerX + innerCircleRadius * math.cos(i * math.pi / 180); 67 | var y2 = centerX + innerCircleRadius * math.sin(i * math.pi / 180); 68 | var dashBrush = paint 69 | ..color = i < draw 70 | ? selectedColor ?? Colors.red 71 | : unselectedColor ?? Colors.yellow 72 | ..style = PaintingStyle.stroke 73 | ..strokeCap = StrokeCap.round 74 | ..strokeWidth = widthLine; 75 | canvas.drawLine(Offset(x1, y1), Offset(x2, y2), dashBrush); 76 | } 77 | } 78 | 79 | @override 80 | bool shouldRepaint(CustomPainter oldDelegate) => true; 81 | } 82 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_liveness_detection_randomized_plugin_example 2 | description: "Demonstrates how to use the flutter_liveness_detection_randomized_plugin plugin." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 6 | 7 | environment: 8 | sdk: '>=3.10.0 <4.0.0' 9 | flutter: '>=3.38.1' 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | flutter_liveness_detection_randomized_plugin: 22 | # When depending on this package from a real application you should use: 23 | # flutter_liveness_detection_randomized_plugin: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.8 32 | 33 | dev_dependencies: 34 | integration_test: 35 | sdk: flutter 36 | flutter_test: 37 | sdk: flutter 38 | 39 | # The "flutter_lints" package below contains a set of recommended lints to 40 | # encourage good coding practices. The lint set provided by the package is 41 | # activated in the `analysis_options.yaml` file located at the root of your 42 | # package. See that file for information about deactivating specific lint 43 | # rules and activating additional ones. 44 | flutter_lints: ^4.0.0 45 | 46 | # For information on the generic Dart part of this file, see the 47 | # following page: https://dart.dev/tools/pub/pubspec 48 | 49 | # The following section is specific to Flutter packages. 50 | flutter: 51 | # The following line ensures that the Material Icons font is 52 | # included with your application, so that you can use the icons in 53 | # the material Icons class. 54 | uses-material-design: true 55 | 56 | # To add assets to your application, add an assets section, like this: 57 | # assets: 58 | # - images/a_dot_burr.jpeg 59 | # - images/a_dot_ham.jpeg 60 | 61 | # An image asset can refer to one or more resolution-specific "variants", see 62 | # https://flutter.dev/to/resolution-aware-images 63 | 64 | # For details regarding adding assets from package dependencies, see 65 | # https://flutter.dev/to/asset-from-package 66 | 67 | # To add custom fonts to your application, add a fonts section here, 68 | # in this "flutter" section. Each entry in this list should have a 69 | # "family" key with the font family name, and a "fonts" key with a 70 | # list giving the asset and other descriptors for the font. For 71 | # example: 72 | # fonts: 73 | # - family: Schyler 74 | # fonts: 75 | # - asset: fonts/Schyler-Regular.ttf 76 | # - asset: fonts/Schyler-Italic.ttf 77 | # style: italic 78 | # - family: Trajan Pro 79 | # fonts: 80 | # - asset: fonts/TrajanPro.ttf 81 | # - asset: fonts/TrajanPro_Bold.ttf 82 | # weight: 700 83 | # 84 | # For details regarding fonts from package dependencies, 85 | # see https://flutter.dev/to/font-from-package 86 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 🚀 2 | 3 | ## BREAKING CHANGES 4 | - 🔄 **API Refactor**: All parameters now consolidated into `LivenessDetectionConfig` 5 | - 📦 **Simplified API**: `livenessDetection()` method now only requires `context` and `config` 6 | - 🛠️ **Migration Required**: Update your implementation to use the new unified config approach 7 | 8 | ## New Features 9 | - ⏱️ **NEW**: Automatic cooldown feature after 3 failed verification attempts. 10-minute waiting period with persistent countdown (survives app restarts). `enableCooldownOnFailure` parameter to control cooldown feature 10 | 11 | ## Bug Fixes 12 | - 🛠️ **Fixed customizedLabel logic**: Corrected skip challenge behavior (empty string now properly skips) 13 | - ✅ **Added validation**: `customizedLabel` must not be null when `useCustomizedLabel` is true 14 | - 🔄 **Improved consistency**: Unified steps handling logic across the codebase 15 | 16 | ## Other Changes 17 | - ✅ Moved `isEnableSnackBar` to config 18 | - ✅ Moved `shuffleListWithSmileLast` to config 19 | - ✅ Moved `showCurrentStep` to config 20 | - ✅ Moved `isDarkMode` to config 21 | - Update compile sdk and Gradle version for example & change deprecated .withOpacity(0.2) to .withAlpha(51) (Thanks to https://github.com/erikwibowo) 22 | 23 | ### Migration Guide: 24 | **Before (v1.0.x):** 25 | ```dart 26 | await plugin.livenessDetection( 27 | context: context, 28 | config: LivenessDetectionConfig(...), 29 | isEnableSnackBar: true, 30 | shuffleListWithSmileLast: true, 31 | showCurrentStep: true, 32 | isDarkMode: false, 33 | ); 34 | ``` 35 | 36 | **After (v1.1.0+):** 37 | ```dart 38 | await plugin.livenessDetection( 39 | context: context, 40 | config: LivenessDetectionConfig( 41 | isEnableSnackBar: true, 42 | shuffleListWithSmileLast: true, 43 | showCurrentStep: true, 44 | isDarkMode: false, 45 | // ... other parameters 46 | ), 47 | ); 48 | ``` 49 | 50 | 51 | ## 1.0.8 🚀 52 | 53 | - 📦 Add packagingOptions with useLegacyPackaging for Android compatibility 54 | - 🛠️ Fix InputImageConverterError for unsupported image formats 55 | - 📷 Add configurable camera resolution preset (cameraResolution parameter) 56 | - ⚡ Improved error handling for ML Kit face detection 57 | - 🔧 Platform-specific image format optimization (NV21 for Android, BGRA8888 for iOS) 58 | 59 | ## 1.0.7 🚀 60 | 61 | - ⚡ Update google_mlkit_face_detection for better compability to newest flutter version 62 | 63 | ## 1.0.6 🚀 64 | - 🛠️ Fix issue camera preview freeze while start liveness detection 65 | - 🎨 Face preview now looks better, no longer stretching 66 | - 🎨 Add parameter to adjust image quality liveness result 67 | 68 | ## 1.0.5 🚀 69 | 70 | - 🛠️ Improve security liveness challenge 71 | - 🎨 Add set to max brightness option 72 | - 🛠️ Update readme.md 73 | 74 | ## 1.0.4 🚀 75 | 76 | - ⚡ Improved performance during liveness challenge verification 77 | - 🎭 Customizable liveness challenge labels 78 | - ⏳ Flexible security verification duration 79 | - 🎲 Adjustable number of liveness challenges 80 | 81 | ## 1.0.3 🚀 82 | 83 | - 🛠️ Adjust to compatible camera dependency to prevent face not found 84 | - 🔐 Ajdust threshold for smile and look down challenge 85 | - 🎨 Add showCurrentStep parameter (default : false) 86 | - 🎨 Add Light and Dark mode 87 | 88 | ## 1.0.2 🚀 89 | 90 | ### Update README.md 91 | 92 | - 🛠️ Update readme.md file 93 | 94 | ## 1.0.1 🚀 95 | 96 | ### Update dependencies 🛠️ 97 | 98 | - 🛠️ Update camera dependencies and also add camera_android_camerax for better experience while using liveness detection 99 | 100 | ## 1.0.0 🚀 101 | 102 | ### Introducing Flutter Liveness Detection Randomized Plugin! 103 | 104 | ✨ First Major Release Highlights: 105 | - 🎯 Smart Liveness Detection System 106 | - 🎲 Dynamic Random Challenge Generator 107 | - 🔐 Enhanced Security Protocols 108 | - 📱 Cross-Platform Support (iOS & Android) 109 | - ⚡ Real-time Processing 110 | - 🎨 Sleek & Modern UI 111 | - 🛠️ Developer-Friendly Integration 112 | 113 | Ready to revolutionize your biometric authentication? Let's make your app more secure! 💪 -------------------------------------------------------------------------------- /lib/src/core/services/liveness_cooldown_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | import 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_cooldown.dart'; 5 | 6 | class LivenessCooldownService { 7 | static const String _cooldownKey = 'liveness_detection_cooldown'; 8 | int _maxFailedAttempts = 3; 9 | int _cooldownMinutes = 10; 10 | 11 | static LivenessCooldownService? _instance; 12 | static LivenessCooldownService get instance { 13 | _instance ??= LivenessCooldownService._(); 14 | return _instance!; 15 | } 16 | 17 | LivenessCooldownService._(); 18 | 19 | void configure({required int maxFailedAttempts, required int cooldownMinutes}) { 20 | _maxFailedAttempts = maxFailedAttempts; 21 | _cooldownMinutes = cooldownMinutes; 22 | } 23 | 24 | Timer? _cooldownTimer; 25 | final StreamController _cooldownController = 26 | StreamController.broadcast(); 27 | 28 | Stream get cooldownStream => 29 | _cooldownController.stream; 30 | 31 | Future getCooldownState() async { 32 | final prefs = await SharedPreferences.getInstance(); 33 | final cooldownJson = prefs.getString(_cooldownKey); 34 | 35 | if (cooldownJson == null) { 36 | return const LivenessDetectionCooldown(); 37 | } 38 | 39 | final cooldown = LivenessDetectionCooldown.fromJson( 40 | jsonDecode(cooldownJson), 41 | ); 42 | 43 | // Check if cooldown has expired 44 | if (cooldown.isInCooldown && 45 | cooldown.remainingCooldownTime.inSeconds <= 0) { 46 | return await _resetCooldown(); 47 | } 48 | 49 | return cooldown; 50 | } 51 | 52 | Future recordFailedAttempt() async { 53 | final currentState = await getCooldownState(); 54 | 55 | if (currentState.isInCooldown) { 56 | return currentState; 57 | } 58 | 59 | final newFailedAttempts = currentState.failedAttempts + 1; 60 | 61 | LivenessDetectionCooldown newState; 62 | 63 | if (newFailedAttempts >= _maxFailedAttempts) { 64 | // Start cooldown 65 | final cooldownEndTime = DateTime.now().add( 66 | Duration(minutes: _cooldownMinutes), 67 | ); 68 | 69 | newState = LivenessDetectionCooldown( 70 | failedAttempts: newFailedAttempts, 71 | cooldownEndTime: cooldownEndTime, 72 | isInCooldown: true, 73 | ); 74 | 75 | _startCooldownTimer(newState); 76 | } else { 77 | newState = LivenessDetectionCooldown( 78 | failedAttempts: newFailedAttempts, 79 | cooldownEndTime: null, 80 | isInCooldown: false, 81 | ); 82 | } 83 | 84 | await _saveCooldownState(newState); 85 | _cooldownController.add(newState); 86 | return newState; 87 | } 88 | 89 | Future recordSuccessfulAttempt() async { 90 | return await _resetCooldown(); 91 | } 92 | 93 | Future _resetCooldown() async { 94 | const newState = LivenessDetectionCooldown(); 95 | await _saveCooldownState(newState); 96 | _cooldownController.add(newState); 97 | _cooldownTimer?.cancel(); 98 | return newState; 99 | } 100 | 101 | Future _saveCooldownState(LivenessDetectionCooldown state) async { 102 | final prefs = await SharedPreferences.getInstance(); 103 | await prefs.setString(_cooldownKey, jsonEncode(state.toJson())); 104 | } 105 | 106 | void _startCooldownTimer(LivenessDetectionCooldown state) { 107 | _cooldownTimer?.cancel(); 108 | 109 | final remaining = state.remainingCooldownTime; 110 | if (remaining.inSeconds <= 0) return; 111 | 112 | _cooldownTimer = Timer(remaining, () async { 113 | await _resetCooldown(); 114 | }); 115 | } 116 | 117 | Future initializeCooldownTimer() async { 118 | final state = await getCooldownState(); 119 | if (state.isInCooldown && state.remainingCooldownTime.inSeconds > 0) { 120 | _startCooldownTimer(state); 121 | } 122 | _cooldownController.add(state); 123 | } 124 | 125 | void dispose() { 126 | _cooldownTimer?.cancel(); 127 | _cooldownController.close(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/presentation/widgets/circular_progress_widget/circular_progress_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_liveness_detection_randomized_plugin/src/presentation/widgets/circular_progress_widget/circular_progress_painter.dart'; 3 | 4 | class CircularProgressWidget extends StatefulWidget { 5 | const CircularProgressWidget({ 6 | super.key, 7 | required this.current, 8 | this.height, 9 | this.width, 10 | this.selectedColor, 11 | this.unselectedColor, 12 | this.child, 13 | this.gradientColor, 14 | this.maxStep = 100, 15 | this.widthLine = 3, 16 | this.heightLine = 20, 17 | this.curve = Curves.easeInOutQuint, 18 | this.duration = const Duration(seconds: 2), 19 | }); 20 | 21 | final double current; 22 | final double maxStep; 23 | final double widthLine; 24 | final double heightLine; 25 | final double? height; 26 | final double? width; 27 | final Color? selectedColor; 28 | final Color? unselectedColor; 29 | final Widget? child; 30 | final Gradient? gradientColor; 31 | final Curve curve; 32 | final Duration duration; 33 | 34 | @override 35 | // ignore: library_private_types_in_public_api 36 | _CircularProgressWidgetState createState() => _CircularProgressWidgetState(); 37 | } 38 | 39 | class _CircularProgressWidgetState extends State 40 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 41 | AnimationController? _animationController; 42 | Animation? _animation; 43 | double _current = 0.0; 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | _animationController = AnimationController( 49 | vsync: this, 50 | duration: widget.duration, 51 | ); 52 | _animation = Tween(begin: 0.0, end: widget.current).animate( 53 | CurvedAnimation( 54 | parent: _animationController!, 55 | curve: widget.curve, 56 | ), 57 | )..addListener(() { 58 | if (mounted) { 59 | setState(() { 60 | _current = _animation!.value; 61 | }); 62 | } 63 | }); 64 | 65 | _animationController!.forward(); 66 | } 67 | 68 | @override 69 | void dispose() { 70 | _animationController?.dispose(); 71 | super.dispose(); 72 | } 73 | 74 | @override 75 | void didUpdateWidget(CircularProgressWidget oldWidget) { 76 | super.didUpdateWidget(oldWidget); 77 | if (oldWidget.current != widget.current) { 78 | if (_animationController != null) { 79 | _animation = Tween( 80 | begin: oldWidget.current, 81 | end: widget.current, 82 | ).animate( 83 | CurvedAnimation( 84 | parent: _animationController!, 85 | curve: widget.curve, 86 | ), 87 | ); 88 | _animationController?.forward(from: 0.0); 89 | } else { 90 | _updateProgress(); 91 | } 92 | } 93 | } 94 | 95 | _updateProgress() { 96 | if (mounted) { 97 | setState(() => _current = widget.current); 98 | } 99 | } 100 | 101 | @override 102 | Widget build(BuildContext context) { 103 | super.build(context); 104 | return LayoutBuilder( 105 | builder: (BuildContext context, BoxConstraints constraints) { 106 | return SizedBox( 107 | height: widget.height ?? constraints.maxHeight, 108 | width: widget.width ?? constraints.maxWidth, 109 | child: RotationTransition( 110 | turns: const AlwaysStoppedAnimation(-90 / 360), 111 | child: CustomPaint( 112 | painter: CircularProgressPainter( 113 | currentStep: _current, 114 | selectedColor: widget.selectedColor, 115 | unselectedColor: widget.unselectedColor, 116 | gradientColor: widget.gradientColor, 117 | maxStep: widget.maxStep, 118 | widthLine: widget.widthLine, 119 | heightLine: widget.heightLine, 120 | ), 121 | child: RotationTransition( 122 | turns: const AlwaysStoppedAnimation(90 / 360), 123 | child: Padding( 124 | padding: EdgeInsets.all(widget.heightLine), 125 | child: Container( 126 | decoration: const BoxDecoration( 127 | shape: BoxShape.circle, 128 | ), 129 | clipBehavior: Clip.antiAlias, 130 | child: widget.child, 131 | ), 132 | ), 133 | ), 134 | ), 135 | ), 136 | ); 137 | }, 138 | ); 139 | } 140 | 141 | @override 142 | bool get wantKeepAlive => true; 143 | } 144 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - camera_avfoundation (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - flutter_liveness_detection_randomized_plugin (0.0.1): 6 | - Flutter 7 | - google_mlkit_commons (0.7.1): 8 | - Flutter 9 | - MLKitVision 10 | - google_mlkit_face_detection (0.11.0): 11 | - Flutter 12 | - google_mlkit_commons 13 | - GoogleMLKit/FaceDetection (~> 6.0.0) 14 | - GoogleDataTransport (9.4.1): 15 | - GoogleUtilities/Environment (~> 7.7) 16 | - nanopb (< 2.30911.0, >= 2.30908.0) 17 | - PromisesObjC (< 3.0, >= 1.2) 18 | - GoogleMLKit/FaceDetection (6.0.0): 19 | - GoogleMLKit/MLKitCore 20 | - MLKitFaceDetection (~> 5.0.0) 21 | - GoogleMLKit/MLKitCore (6.0.0): 22 | - MLKitCommon (~> 11.0.0) 23 | - GoogleToolboxForMac/Defines (4.2.1) 24 | - GoogleToolboxForMac/Logger (4.2.1): 25 | - GoogleToolboxForMac/Defines (= 4.2.1) 26 | - "GoogleToolboxForMac/NSData+zlib (4.2.1)": 27 | - GoogleToolboxForMac/Defines (= 4.2.1) 28 | - GoogleUtilities/Environment (7.13.3): 29 | - GoogleUtilities/Privacy 30 | - PromisesObjC (< 3.0, >= 1.2) 31 | - GoogleUtilities/Logger (7.13.3): 32 | - GoogleUtilities/Environment 33 | - GoogleUtilities/Privacy 34 | - GoogleUtilities/Privacy (7.13.3) 35 | - GoogleUtilities/UserDefaults (7.13.3): 36 | - GoogleUtilities/Logger 37 | - GoogleUtilities/Privacy 38 | - GoogleUtilitiesComponents (1.1.0): 39 | - GoogleUtilities/Logger 40 | - GTMSessionFetcher/Core (3.5.0) 41 | - integration_test (0.0.1): 42 | - Flutter 43 | - MLImage (1.0.0-beta5) 44 | - MLKitCommon (11.0.0): 45 | - GoogleDataTransport (< 10.0, >= 9.4.1) 46 | - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) 47 | - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" 48 | - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) 49 | - GoogleUtilitiesComponents (~> 1.0) 50 | - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) 51 | - MLKitFaceDetection (5.0.0): 52 | - MLKitCommon (~> 11.0) 53 | - MLKitVision (~> 7.0) 54 | - MLKitVision (7.0.0): 55 | - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) 56 | - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" 57 | - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) 58 | - MLImage (= 1.0.0-beta5) 59 | - MLKitCommon (~> 11.0) 60 | - nanopb (2.30910.0): 61 | - nanopb/decode (= 2.30910.0) 62 | - nanopb/encode (= 2.30910.0) 63 | - nanopb/decode (2.30910.0) 64 | - nanopb/encode (2.30910.0) 65 | - PromisesObjC (2.4.0) 66 | 67 | DEPENDENCIES: 68 | - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) 69 | - Flutter (from `Flutter`) 70 | - flutter_liveness_detection_randomized_plugin (from `.symlinks/plugins/flutter_liveness_detection_randomized_plugin/ios`) 71 | - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) 72 | - google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`) 73 | - integration_test (from `.symlinks/plugins/integration_test/ios`) 74 | 75 | SPEC REPOS: 76 | trunk: 77 | - GoogleDataTransport 78 | - GoogleMLKit 79 | - GoogleToolboxForMac 80 | - GoogleUtilities 81 | - GoogleUtilitiesComponents 82 | - GTMSessionFetcher 83 | - MLImage 84 | - MLKitCommon 85 | - MLKitFaceDetection 86 | - MLKitVision 87 | - nanopb 88 | - PromisesObjC 89 | 90 | EXTERNAL SOURCES: 91 | camera_avfoundation: 92 | :path: ".symlinks/plugins/camera_avfoundation/ios" 93 | Flutter: 94 | :path: Flutter 95 | flutter_liveness_detection_randomized_plugin: 96 | :path: ".symlinks/plugins/flutter_liveness_detection_randomized_plugin/ios" 97 | google_mlkit_commons: 98 | :path: ".symlinks/plugins/google_mlkit_commons/ios" 99 | google_mlkit_face_detection: 100 | :path: ".symlinks/plugins/google_mlkit_face_detection/ios" 101 | integration_test: 102 | :path: ".symlinks/plugins/integration_test/ios" 103 | 104 | SPEC CHECKSUMS: 105 | camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf 106 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 107 | flutter_liveness_detection_randomized_plugin: d8961eea48ebce31e93c24ca6f7f87a14b66f732 108 | google_mlkit_commons: 9f155ff61a70e0fad8692a7edb5aa2dc468536d3 109 | google_mlkit_face_detection: 35c4b9a56db1acee146600be8d75c45b7a2fe6fc 110 | GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a 111 | GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 112 | GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 113 | GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 114 | GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe 115 | GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 116 | integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e 117 | MLImage: 1824212150da33ef225fbd3dc49f184cf611046c 118 | MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 119 | MLKitFaceDetection: 7c0e8bf09ddd27105da32d088fca978a99fc30cc 120 | MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 121 | nanopb: 438bc412db1928dac798aa6fd75726007be04262 122 | PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 123 | 124 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 125 | 126 | COCOAPODS: 1.16.2 127 | -------------------------------------------------------------------------------- /lib/src/models/liveness_detection_threshold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | abstract class LivenessDetectionThreshold extends Equatable { 4 | const LivenessDetectionThreshold(); 5 | LivenessDetectionThreshold fromDict(Map map); 6 | Map toMap(); 7 | @override 8 | List get props => []; 9 | } 10 | 11 | class LivenessThresholdSmile extends LivenessDetectionThreshold { 12 | final double probability; 13 | 14 | LivenessThresholdSmile({ 15 | this.probability = 0.75, 16 | }) : super() { 17 | assert( 18 | probability < 1.0 || probability > 0.0, 19 | "Smile Probability has to be between 1.0 and 0.0", 20 | ); 21 | } 22 | 23 | LivenessThresholdSmile copyWith({ 24 | double? probability, 25 | }) { 26 | return LivenessThresholdSmile( 27 | probability: probability ?? this.probability, 28 | ); 29 | } 30 | 31 | @override 32 | Map toMap() { 33 | final result = {}; 34 | 35 | result.addAll({'probability': probability}); 36 | 37 | return result; 38 | } 39 | 40 | @override 41 | factory LivenessThresholdSmile.fromMap(Map map) { 42 | return LivenessThresholdSmile( 43 | probability: map['probability']?.toDouble() ?? 0.0, 44 | ); 45 | } 46 | 47 | String toJson() => json.encode(toMap()); 48 | 49 | factory LivenessThresholdSmile.fromJson(String source) => 50 | LivenessThresholdSmile.fromMap(json.decode(source)); 51 | 52 | @override 53 | String toString() => 'LivenessThresholdSmile(probability: $probability)'; 54 | 55 | @override 56 | bool operator ==(Object other) { 57 | if (identical(this, other)) return true; 58 | 59 | return other is LivenessThresholdSmile && other.probability == probability; 60 | } 61 | 62 | @override 63 | int get hashCode => probability.hashCode; 64 | 65 | @override 66 | LivenessThresholdSmile fromDict(Map map) { 67 | return LivenessThresholdSmile( 68 | probability: map['probability']?.toDouble() ?? 0.0, 69 | ); 70 | } 71 | } 72 | 73 | class LivenessThresholdBlink extends LivenessDetectionThreshold { 74 | 75 | final double leftEyeProbability; 76 | final double rightEyeProbability; 77 | 78 | LivenessThresholdBlink({ 79 | this.leftEyeProbability = 0.25, 80 | this.rightEyeProbability = 0.25, 81 | }) : super() { 82 | assert( 83 | leftEyeProbability < 1.0 || leftEyeProbability > 0.0, 84 | "Left Probability has to be between 1.0 and 0.0", 85 | ); 86 | assert( 87 | rightEyeProbability < 1.0 || rightEyeProbability > 0.0, 88 | "Right Probability has to be between 1.0 and 0.0", 89 | ); 90 | } 91 | 92 | LivenessThresholdBlink copyWith({ 93 | double? leftEyeProbability, 94 | double? rightEyeProbability, 95 | }) { 96 | return LivenessThresholdBlink( 97 | leftEyeProbability: leftEyeProbability ?? this.leftEyeProbability, 98 | rightEyeProbability: rightEyeProbability ?? this.rightEyeProbability, 99 | ); 100 | } 101 | 102 | @override 103 | Map toMap() { 104 | final result = {}; 105 | 106 | result.addAll({'leftEyeProbability': leftEyeProbability}); 107 | result.addAll({'rightEyeProbability': rightEyeProbability}); 108 | 109 | return result; 110 | } 111 | 112 | factory LivenessThresholdBlink.fromMap(Map map) { 113 | return LivenessThresholdBlink( 114 | leftEyeProbability: map['leftEyeProbability']?.toDouble() ?? 0.0, 115 | rightEyeProbability: map['rightEyeProbability']?.toDouble() ?? 0.0, 116 | ); 117 | } 118 | 119 | String toJson() => json.encode(toMap()); 120 | 121 | factory LivenessThresholdBlink.fromJson(String source) => 122 | LivenessThresholdBlink.fromMap(json.decode(source)); 123 | 124 | @override 125 | String toString() => 126 | 'LivenessThresholdBlink(leftEyeProbability: $leftEyeProbability, rightEyeProbability: $rightEyeProbability)'; 127 | 128 | @override 129 | bool operator ==(Object other) { 130 | if (identical(this, other)) return true; 131 | 132 | return other is LivenessThresholdBlink && 133 | other.leftEyeProbability == leftEyeProbability && 134 | other.rightEyeProbability == rightEyeProbability; 135 | } 136 | 137 | @override 138 | int get hashCode => 139 | leftEyeProbability.hashCode ^ rightEyeProbability.hashCode; 140 | 141 | @override 142 | LivenessDetectionThreshold fromDict(Map map) { 143 | return LivenessThresholdBlink( 144 | leftEyeProbability: map['leftEyeProbability']?.toDouble() ?? 0.0, 145 | rightEyeProbability: map['rightEyeProbability']?.toDouble() ?? 0.0, 146 | ); 147 | } 148 | } 149 | 150 | class LivenessThresholdHead extends LivenessDetectionThreshold{ 151 | final double rotationAngle; 152 | 153 | LivenessThresholdHead({ 154 | this.rotationAngle = 45.0, 155 | }) : super() { 156 | assert( 157 | rotationAngle > 180.0 || rotationAngle > 0.0, 158 | "To detect the livelyness of the face, it has to be properly visible in the camera. The threshold angle should be more than 0.0 degrees and less than 180 degrees.", 159 | ); 160 | } 161 | 162 | LivenessThresholdHead copyWith({ 163 | double? rotationAngle, 164 | }) { 165 | return LivenessThresholdHead( 166 | rotationAngle: rotationAngle ?? this.rotationAngle, 167 | ); 168 | } 169 | 170 | @override 171 | Map toMap() { 172 | final result = {}; 173 | 174 | result.addAll({'rotationAngle': rotationAngle}); 175 | 176 | return result; 177 | } 178 | 179 | factory LivenessThresholdHead.fromMap(Map map) { 180 | return LivenessThresholdHead( 181 | rotationAngle: map['rotationAngle']?.toDouble() ?? 0.0, 182 | ); 183 | } 184 | 185 | String toJson() => json.encode(toMap()); 186 | 187 | factory LivenessThresholdHead.fromJson(String source) => 188 | LivenessThresholdHead.fromMap(json.decode(source)); 189 | 190 | @override 191 | String toString() => 192 | 'LivenessThresholdHead(rotationAngle: $rotationAngle)'; 193 | 194 | @override 195 | bool operator ==(Object other) { 196 | if (identical(this, other)) return true; 197 | 198 | return other is LivenessThresholdHead && 199 | other.rotationAngle == rotationAngle; 200 | } 201 | 202 | @override 203 | int get hashCode => rotationAngle.hashCode; 204 | 205 | @override 206 | LivenessDetectionThreshold fromDict(Map map) { 207 | return LivenessThresholdHead( 208 | rotationAngle: map['rotationAngle']?.toDouble() ?? 0.0, 209 | ); 210 | } 211 | } -------------------------------------------------------------------------------- /lib/src/presentation/widgets/liveness_cooldown_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_liveness_detection_randomized_plugin/src/models/liveness_detection_cooldown.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | class LivenessCooldownWidget extends StatefulWidget { 7 | final LivenessDetectionCooldown cooldownState; 8 | final bool isDarkMode; 9 | final VoidCallback? onCooldownComplete; 10 | final int maxFailedAttempts; 11 | 12 | const LivenessCooldownWidget({ 13 | super.key, 14 | required this.cooldownState, 15 | this.isDarkMode = true, 16 | this.onCooldownComplete, 17 | this.maxFailedAttempts = 3, 18 | }); 19 | 20 | @override 21 | State createState() => _LivenessCooldownWidgetState(); 22 | } 23 | 24 | class _LivenessCooldownWidgetState extends State with WidgetsBindingObserver { 25 | Timer? _countdownTimer; 26 | Duration _remainingTime = Duration.zero; 27 | static const String _remainingTimeKey = 'cooldown_remaining_time'; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | WidgetsBinding.instance.addObserver(this); 33 | _loadRemainingTime(); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | WidgetsBinding.instance.removeObserver(this); 39 | _countdownTimer?.cancel(); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | void didChangeAppLifecycleState(AppLifecycleState state) { 45 | if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { 46 | _pauseCountdown(); 47 | } else if (state == AppLifecycleState.resumed) { 48 | _resumeCountdown(); 49 | } 50 | } 51 | 52 | Future _loadRemainingTime() async { 53 | final prefs = await SharedPreferences.getInstance(); 54 | final savedSeconds = prefs.getInt(_remainingTimeKey); 55 | 56 | if (savedSeconds != null) { 57 | _remainingTime = Duration(seconds: savedSeconds); 58 | } else { 59 | _remainingTime = widget.cooldownState.remainingCooldownTime; 60 | } 61 | 62 | if (mounted) { 63 | setState(() {}); 64 | _startCountdown(); 65 | } 66 | } 67 | 68 | Future _saveRemainingTime() async { 69 | final prefs = await SharedPreferences.getInstance(); 70 | await prefs.setInt(_remainingTimeKey, _remainingTime.inSeconds); 71 | } 72 | 73 | void _pauseCountdown() { 74 | _countdownTimer?.cancel(); 75 | _saveRemainingTime(); 76 | } 77 | 78 | void _resumeCountdown() { 79 | _loadRemainingTime(); 80 | } 81 | 82 | void _startCountdown() { 83 | _countdownTimer?.cancel(); 84 | 85 | if (_remainingTime.inSeconds <= 0) { 86 | _clearSavedTime(); 87 | widget.onCooldownComplete?.call(); 88 | return; 89 | } 90 | 91 | _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { 92 | if (!mounted) { 93 | timer.cancel(); 94 | return; 95 | } 96 | 97 | setState(() { 98 | _remainingTime = _remainingTime - const Duration(seconds: 1); 99 | }); 100 | 101 | _saveRemainingTime(); 102 | 103 | if (_remainingTime.inSeconds <= 0) { 104 | timer.cancel(); 105 | _clearSavedTime(); 106 | widget.onCooldownComplete?.call(); 107 | } 108 | }); 109 | } 110 | 111 | Future _clearSavedTime() async { 112 | final prefs = await SharedPreferences.getInstance(); 113 | await prefs.remove(_remainingTimeKey); 114 | } 115 | 116 | String _formatDuration(Duration duration) { 117 | final minutes = duration.inMinutes; 118 | final seconds = duration.inSeconds % 60; 119 | return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; 120 | } 121 | 122 | @override 123 | Widget build(BuildContext context) { 124 | return Scaffold( 125 | backgroundColor: widget.isDarkMode ? Colors.black : Colors.white, 126 | body: Center( 127 | child: Padding( 128 | padding: const EdgeInsets.all(24.0), 129 | child: Column( 130 | mainAxisAlignment: MainAxisAlignment.center, 131 | children: [ 132 | Icon( 133 | Icons.timer_outlined, 134 | size: 80, 135 | color: widget.isDarkMode ? Colors.white70 : Colors.black54, 136 | ), 137 | const SizedBox(height: 24), 138 | Text( 139 | 'Too Many Failed Attempts', 140 | style: TextStyle( 141 | fontSize: 24, 142 | fontWeight: FontWeight.bold, 143 | color: widget.isDarkMode ? Colors.white : Colors.black, 144 | ), 145 | textAlign: TextAlign.center, 146 | ), 147 | const SizedBox(height: 16), 148 | Text( 149 | 'You have failed liveness verification ${widget.maxFailedAttempts} times.\nPlease wait before trying again.', 150 | style: TextStyle( 151 | fontSize: 16, 152 | color: widget.isDarkMode ? Colors.white70 : Colors.black54, 153 | ), 154 | textAlign: TextAlign.center, 155 | ), 156 | const SizedBox(height: 32), 157 | Container( 158 | padding: const EdgeInsets.all(20), 159 | decoration: BoxDecoration( 160 | color: widget.isDarkMode ? Colors.grey[900] : Colors.grey[100], 161 | borderRadius: BorderRadius.circular(12), 162 | ), 163 | child: Column( 164 | children: [ 165 | Text( 166 | 'Remaining Wait Time', 167 | style: TextStyle( 168 | fontSize: 14, 169 | color: widget.isDarkMode ? Colors.white70 : Colors.black54, 170 | ), 171 | ), 172 | const SizedBox(height: 8), 173 | Text( 174 | _formatDuration(_remainingTime), 175 | style: TextStyle( 176 | fontSize: 36, 177 | fontWeight: FontWeight.bold, 178 | color: widget.isDarkMode ? Colors.white : Colors.black, 179 | fontFamily: 'monospace', 180 | ), 181 | ), 182 | ], 183 | ), 184 | ), 185 | const SizedBox(height: 32), 186 | ElevatedButton( 187 | onPressed: () => Navigator.of(context).pop(), 188 | style: ElevatedButton.styleFrom( 189 | backgroundColor: widget.isDarkMode ? Colors.grey[800] : Colors.grey[300], 190 | foregroundColor: widget.isDarkMode ? Colors.white : Colors.black, 191 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), 192 | ), 193 | child: const Text('Back'), 194 | ), 195 | ], 196 | ), 197 | ), 198 | ), 199 | ); 200 | } 201 | } -------------------------------------------------------------------------------- /lib/src/presentation/widgets/liveness_detection_tutorial_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | class LivenessDetectionTutorialScreen extends StatefulWidget { 4 | final VoidCallback onStartTap; 5 | final bool isDarkMode; 6 | final int? duration; 7 | const LivenessDetectionTutorialScreen( 8 | {super.key, 9 | required this.onStartTap, 10 | this.isDarkMode = false, 11 | required this.duration}); 12 | 13 | @override 14 | State createState() => 15 | _LivenessDetectionTutorialScreenState(); 16 | } 17 | 18 | class _LivenessDetectionTutorialScreenState 19 | extends State { 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | backgroundColor: widget.isDarkMode ? Colors.black : Colors.white, 29 | body: SafeArea( 30 | minimum: const EdgeInsets.all(12), 31 | child: Column( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | crossAxisAlignment: CrossAxisAlignment.center, 34 | children: [ 35 | const Spacer(), 36 | const SizedBox( 37 | height: 16, 38 | ), 39 | Text( 40 | 'Liveness Detection - Tutorial', 41 | style: TextStyle( 42 | fontWeight: FontWeight.bold, 43 | fontSize: 24, 44 | color: widget.isDarkMode ? Colors.white : Colors.black, 45 | ), 46 | ), 47 | const SizedBox( 48 | height: 32, 49 | ), 50 | Container( 51 | width: MediaQuery.of(context).size.width, 52 | padding: const EdgeInsets.symmetric(vertical: 16), 53 | decoration: BoxDecoration( 54 | borderRadius: BorderRadius.circular(8), 55 | color: widget.isDarkMode ? Colors.black87 : Colors.white, 56 | boxShadow: !widget.isDarkMode 57 | ? [ 58 | BoxShadow( 59 | color: Colors.grey.withAlpha(51), 60 | spreadRadius: 5, 61 | blurRadius: 7, 62 | offset: const Offset(0, 3), 63 | ), 64 | ] 65 | : null, 66 | ), 67 | child: Column( 68 | children: [ 69 | ListTile( 70 | leading: Text( 71 | '1', 72 | style: TextStyle( 73 | fontSize: 20, 74 | fontWeight: FontWeight.bold, 75 | color: 76 | widget.isDarkMode ? Colors.white : Colors.black), 77 | ), 78 | subtitle: Text( 79 | "Make sure you are in an area that has sufficient lighting and that your ears are not covered by anything", 80 | style: TextStyle( 81 | color: 82 | widget.isDarkMode ? Colors.white : Colors.black), 83 | ), 84 | title: Text( 85 | "Sufficient Lighting", 86 | style: TextStyle( 87 | fontSize: 20, 88 | fontWeight: FontWeight.bold, 89 | color: 90 | widget.isDarkMode ? Colors.white : Colors.black), 91 | ), 92 | ), 93 | ListTile( 94 | leading: Text( 95 | '2', 96 | style: TextStyle( 97 | fontSize: 20, 98 | fontWeight: FontWeight.bold, 99 | color: 100 | widget.isDarkMode ? Colors.white : Colors.black), 101 | ), 102 | subtitle: Text( 103 | "Hold the phone at eye level and look straight at the camera", 104 | style: TextStyle( 105 | color: 106 | widget.isDarkMode ? Colors.white : Colors.black), 107 | ), 108 | title: Text( 109 | "Straight Ahead View", 110 | style: TextStyle( 111 | fontSize: 20, 112 | fontWeight: FontWeight.bold, 113 | color: 114 | widget.isDarkMode ? Colors.white : Colors.black), 115 | ), 116 | ), 117 | ListTile( 118 | leading: Text( 119 | '3', 120 | style: TextStyle( 121 | fontSize: 20, 122 | fontWeight: FontWeight.bold, 123 | color: 124 | widget.isDarkMode ? Colors.white : Colors.black), 125 | ), 126 | subtitle: Text( 127 | "The time limit given for the liveness detection system verification process is ${widget.duration ?? 45} seconds", 128 | style: TextStyle( 129 | color: 130 | widget.isDarkMode ? Colors.white : Colors.black), 131 | ), 132 | title: Text( 133 | "Time Limit Verification", 134 | style: TextStyle( 135 | fontSize: 20, 136 | fontWeight: FontWeight.bold, 137 | color: 138 | widget.isDarkMode ? Colors.white : Colors.black), 139 | ), 140 | ) 141 | ], 142 | ), 143 | ), 144 | const SizedBox( 145 | height: 24, 146 | ), 147 | ElevatedButton.icon( 148 | style: ElevatedButton.styleFrom( 149 | backgroundColor: 150 | widget.isDarkMode ? Colors.black87 : Colors.white, 151 | foregroundColor: 152 | widget.isDarkMode ? Colors.white : Colors.black, 153 | shape: RoundedRectangleBorder( 154 | borderRadius: BorderRadius.circular(8), 155 | ), 156 | ), 157 | icon: const Icon(Icons.camera_alt_outlined), 158 | onPressed: () => widget.onStartTap(), 159 | label: const Text( 160 | "Start the Liveness Detection System", 161 | ), 162 | ), 163 | const SizedBox( 164 | height: 10, 165 | ), 166 | const Spacer(), 167 | const Row( 168 | mainAxisAlignment: MainAxisAlignment.center, 169 | children: [ 170 | Icon( 171 | Icons.info_outline_rounded, 172 | color: Colors.grey, 173 | size: 15, 174 | ), 175 | SizedBox( 176 | width: 10, 177 | ), 178 | Text( 179 | 'Package Version: 1.1.0', 180 | style: TextStyle(color: Colors.grey), 181 | ) 182 | ], 183 | ) 184 | ], 185 | ), 186 | ), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 2 | 3 | void main() { 4 | runApp( 5 | const MaterialApp(debugShowCheckedModeBanner: false, home: HomeView()), 6 | ); 7 | } 8 | 9 | class HomeView extends StatefulWidget { 10 | const HomeView({super.key}); 11 | 12 | @override 13 | State createState() => _HomeViewState(); 14 | } 15 | 16 | class _HomeViewState extends State { 17 | List capturedImages = []; 18 | String? imgPath; 19 | int livenessScenario = 0; 20 | final int totalScenarios = 8; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | body: Center( 31 | child: ListView( 32 | shrinkWrap: true, 33 | padding: const EdgeInsets.all(12), 34 | children: [ 35 | if (imgPath != null) ...[ 36 | const Text( 37 | 'Result Liveness Detection', 38 | textAlign: TextAlign.center, 39 | ), 40 | const SizedBox(height: 12), 41 | Align( 42 | child: SizedBox( 43 | height: 100, 44 | width: 100, 45 | child: ClipRRect( 46 | borderRadius: BorderRadius.circular(20), 47 | child: Image.file(File(imgPath!), fit: BoxFit.cover), 48 | ), 49 | ), 50 | ), 51 | const SizedBox(height: 12), 52 | ], 53 | Text( 54 | 'Liveness Scenario ${livenessScenario + 1}/$totalScenarios', 55 | style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 56 | textAlign: TextAlign.center, 57 | ), 58 | const SizedBox(height: 8), 59 | Text( 60 | _getScenarioDescription(), 61 | textAlign: TextAlign.center, 62 | style: const TextStyle(fontSize: 12), 63 | ), 64 | const SizedBox(height: 16), 65 | ElevatedButton.icon( 66 | icon: const Icon(Icons.camera_alt_rounded), 67 | onPressed: () async { 68 | final config = _getTestConfig(); 69 | final String? response = 70 | await FlutterLivenessDetectionRandomizedPlugin.instance 71 | .livenessDetection(context: context, config: config); 72 | if (mounted) { 73 | setState(() { 74 | imgPath = response; 75 | }); 76 | } 77 | }, 78 | label: const Text('Start Liveness Detection'), 79 | ), 80 | const SizedBox(height: 12), 81 | ElevatedButton( 82 | onPressed: () { 83 | setState(() { 84 | livenessScenario = (livenessScenario + 1) % totalScenarios; 85 | }); 86 | }, 87 | child: const Text('Next Liveness Scenario'), 88 | ), 89 | ], 90 | ), 91 | ), 92 | ); 93 | } 94 | 95 | String _getScenarioDescription() { 96 | switch (livenessScenario) { 97 | case 0: 98 | return 'Default: Smile always last + Info screen'; 99 | case 1: 100 | return 'Random shuffle: No smile priority'; 101 | case 2: 102 | return 'Dark mode + High resolution + No info'; 103 | case 3: 104 | return 'Custom labels: All steps with Indonesian'; 105 | case 4: 106 | return 'Skip steps: Only 3 challenges (blink, smile, lookUp)'; 107 | case 5: 108 | return 'Low quality + Duration timer + Cooldown enabled'; 109 | case 6: 110 | return 'Max brightness off + No snackbar + Hide steps'; 111 | case 7: 112 | return 'All features: Custom + Timer + Cooldown + Dark'; 113 | default: 114 | return ''; 115 | } 116 | } 117 | 118 | LivenessDetectionConfig _getTestConfig() { 119 | switch (livenessScenario) { 120 | case 0: // Default scenario 121 | return LivenessDetectionConfig( 122 | cameraResolution: ResolutionPreset.medium, 123 | imageQuality: 100, 124 | isEnableMaxBrightness: true, 125 | durationLivenessVerify: 45, 126 | showDurationUiText: false, 127 | startWithInfoScreen: true, 128 | useCustomizedLabel: false, 129 | enableCooldownOnFailure: false, 130 | isEnableSnackBar: true, 131 | shuffleListWithSmileLast: true, 132 | isDarkMode: false, 133 | showCurrentStep: true, 134 | ); 135 | case 1: // Random shuffle 136 | return LivenessDetectionConfig( 137 | cameraResolution: ResolutionPreset.medium, 138 | imageQuality: 85, 139 | isEnableMaxBrightness: true, 140 | durationLivenessVerify: 30, 141 | showDurationUiText: false, 142 | startWithInfoScreen: false, 143 | useCustomizedLabel: false, 144 | enableCooldownOnFailure: false, 145 | isEnableSnackBar: true, 146 | shuffleListWithSmileLast: false, 147 | isDarkMode: false, 148 | showCurrentStep: true, 149 | ); 150 | case 2: // Dark mode + High res 151 | return LivenessDetectionConfig( 152 | cameraResolution: ResolutionPreset.high, 153 | imageQuality: 100, 154 | isEnableMaxBrightness: true, 155 | durationLivenessVerify: 60, 156 | showDurationUiText: false, 157 | startWithInfoScreen: false, 158 | useCustomizedLabel: false, 159 | enableCooldownOnFailure: false, 160 | isEnableSnackBar: true, 161 | shuffleListWithSmileLast: true, 162 | isDarkMode: true, 163 | showCurrentStep: true, 164 | ); 165 | case 3: // Custom labels Indonesian 166 | return LivenessDetectionConfig( 167 | cameraResolution: ResolutionPreset.medium, 168 | imageQuality: 90, 169 | isEnableMaxBrightness: true, 170 | durationLivenessVerify: 45, 171 | showDurationUiText: false, 172 | startWithInfoScreen: true, 173 | useCustomizedLabel: true, 174 | enableCooldownOnFailure: false, 175 | isEnableSnackBar: true, 176 | shuffleListWithSmileLast: true, 177 | isDarkMode: false, 178 | showCurrentStep: true, 179 | customizedLabel: LivenessDetectionLabelModel( 180 | blink: 'Kedip 2-3 Kali', 181 | lookDown: 'Lihat ke Bawah', 182 | lookLeft: 'Lihat ke Kiri', 183 | lookRight: 'Lihat ke Kanan', 184 | lookUp: 'Lihat ke Atas', 185 | smile: 'Tersenyum Lebar', 186 | ), 187 | ); 188 | case 4: // Skip some steps 189 | return LivenessDetectionConfig( 190 | cameraResolution: ResolutionPreset.low, 191 | imageQuality: 70, 192 | isEnableMaxBrightness: true, 193 | durationLivenessVerify: 30, 194 | showDurationUiText: false, 195 | startWithInfoScreen: false, 196 | useCustomizedLabel: true, 197 | enableCooldownOnFailure: false, 198 | isEnableSnackBar: true, 199 | shuffleListWithSmileLast: false, 200 | isDarkMode: false, 201 | showCurrentStep: true, 202 | customizedLabel: LivenessDetectionLabelModel( 203 | blink: 'Blink Eyes', 204 | lookDown: '', // Skip 205 | lookLeft: '', // Skip 206 | lookRight: '', // Skip 207 | lookUp: 'Look Up Please', 208 | smile: 'Smile Wide', 209 | ), 210 | ); 211 | case 5: // Low quality + Timer + Cooldown 212 | return LivenessDetectionConfig( 213 | cameraResolution: ResolutionPreset.low, 214 | imageQuality: 50, 215 | isEnableMaxBrightness: true, 216 | durationLivenessVerify: 20, 217 | showDurationUiText: true, 218 | startWithInfoScreen: true, 219 | useCustomizedLabel: false, 220 | enableCooldownOnFailure: true, 221 | maxFailedAttempts: 2, 222 | cooldownMinutes: 5, 223 | isEnableSnackBar: true, 224 | shuffleListWithSmileLast: true, 225 | isDarkMode: false, 226 | showCurrentStep: true, 227 | ); 228 | case 6: // Minimal features 229 | return LivenessDetectionConfig( 230 | cameraResolution: ResolutionPreset.medium, 231 | imageQuality: 80, 232 | isEnableMaxBrightness: false, 233 | durationLivenessVerify: 40, 234 | showDurationUiText: false, 235 | startWithInfoScreen: false, 236 | useCustomizedLabel: false, 237 | enableCooldownOnFailure: false, 238 | isEnableSnackBar: false, 239 | shuffleListWithSmileLast: false, 240 | isDarkMode: false, 241 | showCurrentStep: false, 242 | ); 243 | case 7: // All features enabled 244 | return LivenessDetectionConfig( 245 | cameraResolution: ResolutionPreset.high, 246 | imageQuality: 95, 247 | isEnableMaxBrightness: true, 248 | durationLivenessVerify: 50, 249 | showDurationUiText: true, 250 | startWithInfoScreen: true, 251 | useCustomizedLabel: true, 252 | enableCooldownOnFailure: true, 253 | maxFailedAttempts: 3, 254 | cooldownMinutes: 10, 255 | isEnableSnackBar: true, 256 | shuffleListWithSmileLast: true, 257 | isDarkMode: true, 258 | showCurrentStep: true, 259 | customizedLabel: LivenessDetectionLabelModel( 260 | blink: '👁️ Kedipkan Mata', 261 | lookDown: '⬇️ Lihat Bawah', 262 | lookLeft: '⬅️ Lihat Kiri', 263 | lookRight: '➡️ Lihat Kanan', 264 | lookUp: '⬆️ Lihat Atas', 265 | smile: '😊 Senyum Manis', 266 | ), 267 | ); 268 | default: 269 | return LivenessDetectionConfig(); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Liveness Detection Randomized Plugin 2 | 3 | A Flutter plugin for liveness detection with randomized challenge response method with an interaction mechanism between the user and the system in the form of a movement challenge that indicates life is detected on the face. This plugin helps implement secure biometric authentication by detecting real human presence through dynamic facial verification challenges. 4 | 5 | [![pub package](https://img.shields.io/pub/v/flutter_liveness_detection_randomized_plugin.svg)](https://pub.dev/packages/flutter_liveness_detection_randomized_plugin) 6 | 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/50b64954ad654b65b0424d266399b026)](https://app.codacy.com/gh/bagussubagja/flutter-liveness-detection-randomized-plugin/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 8 | 9 | ## Author 10 | 11 | Crafted with love by **[Bagus Subagja](https://www.linkedin.com/in/bagussubagja/)** ❤️ 12 | 13 | Feel free to fork and modify this package to suit your needs - that's much more enjoyable than stealing or claiming my code 😊 14 | 15 | ## Preview 🪟 16 | ![Slide 16_9 - 1](https://github.com/user-attachments/assets/55e59d51-e0da-4562-879e-ae50adaced33) 17 | 18 | https://github.com/user-attachments/assets/f7266dc9-c4a2-4fba-8684-0ead2f678180 19 | 20 | ## Update 1.1.0 21 | - ⏱️ Added automatic cooldown feature after 3 failed verification attempts 22 | - 🔒 10-minute waiting period with persistent countdown (survives app restarts) 23 | - 🎯 Countdown only decreases when app is active (pauses when app is backgrounded) 24 | - 🔄 **API Refactor**: All parameters consolidated into `LivenessDetectionConfig` 25 | - 🎯 Simplified API - only requires `context` and `config` parameters 26 | - 🛠️ Fixed customizedLabel logic for proper skip challenge behavior 27 | - ✅ Added validation: `customizedLabel` must not be null when `useCustomizedLabel` is true 28 | 29 | ## Update 1.0.6 30 | ![Slide 16_9 - 9](https://github.com/user-attachments/assets/3a9b187a-ccfd-4542-a8d9-88b7ef7903a9) 31 | Face stretching already fixed on this version 32 | 33 | ## Features ✨ 34 | 35 | - 📱 Real-time face detection 36 | - 🎲 Randomized challenge sequence generation 37 | - 💫 Cross-platform support (iOS & Android) 38 | - 🎨 Light and dark mode support 39 | - ✅ High accuracy liveness verification 40 | - 🚀 Simple integration API 41 | - 🎭 Customizable liveness challenge labels 42 | - ⏳ Flexible security verification duration 43 | - 🎲 Adjustable number of liveness challenges 44 | - 🛠️ Adjustable image quality result 45 | - ⏱️ Automatic cooldown after failed attempts 46 | 47 | ## Getting Started 🌟 48 | 49 | Add this to your package's `pubspec.yaml` file: 50 | 51 | ```yaml 52 | dependencies: 53 | flutter_liveness_detection_randomized_plugin: ^1.1.0 54 | ``` 55 | 56 | ## Usage 🚀 57 | 58 | ```dart 59 | final String? response = await FlutterLivenessDetectionRandomizedPlugin.instance.livenessDetection( 60 | context: context, 61 | config: LivenessDetectionConfig( 62 | // Camera & Image Settings 63 | cameraResolution: ResolutionPreset.medium, // Camera resolution 64 | imageQuality: 100, // Image quality (0-100) 65 | isEnableMaxBrightness: true, // Auto brightness adjustment 66 | 67 | // Detection Settings 68 | durationLivenessVerify: 60, // Detection timeout in seconds 69 | showDurationUiText: false, // Show countdown timer 70 | startWithInfoScreen: true, // Show tutorial screen 71 | 72 | // UI Settings 73 | isDarkMode: false, // Dark/light theme 74 | showCurrentStep: true, // Show step counter 75 | isEnableSnackBar: true, // Show result notifications 76 | shuffleListWithSmileLast: true, // Randomize challenges with smile last 77 | 78 | // Customization 79 | useCustomizedLabel: false, // Enable custom labels 80 | customizedLabel: LivenessDetectionLabelModel( 81 | blink: '', // Empty string = skip challenge 82 | lookDown: '', // Skip this challenge 83 | lookLeft: null, // null = use default "Look LEFT" 84 | lookRight: 'Turn Right', // Custom label 85 | lookUp: 'Look Up Please', // Custom label 86 | smile: null, // null = use default "Smile" 87 | ), 88 | 89 | // Security Features 90 | enableCooldownOnFailure: true, // Enable cooldown after failures 91 | maxFailedAttempts: 3, // Failed attempts before cooldown 92 | cooldownMinutes: 10, // Cooldown duration 93 | ), 94 | ); 95 | ``` 96 | 97 | ## Configuration Parameters 📋 98 | 99 | ### Camera & Image Settings 100 | - `cameraResolution`: Camera quality (ResolutionPreset.low/medium/high) 101 | - `imageQuality`: Output image quality 0-100 (default: 100) 102 | - `isEnableMaxBrightness`: Auto brightness adjustment (default: true) 103 | 104 | ### Detection Settings 105 | - `durationLivenessVerify`: Detection timeout in seconds (default: 45) 106 | - `showDurationUiText`: Show countdown timer (default: false) 107 | - `startWithInfoScreen`: Show tutorial before detection (default: false) 108 | 109 | ### UI Settings 110 | - `isDarkMode`: Dark theme mode (default: true) 111 | - `showCurrentStep`: Show current step number (default: false) 112 | - `isEnableSnackBar`: Show success/failure notifications (default: true) 113 | - `shuffleListWithSmileLast`: Randomize challenges with smile at end (default: true) 114 | 115 | ### Customization 116 | - `useCustomizedLabel`: Enable custom challenge labels (default: false) 117 | - `customizedLabel`: Custom labels for each challenge type 118 | 119 | ### Security Features 120 | - `enableCooldownOnFailure`: Enable cooldown after failed attempts (default: true) 121 | - `maxFailedAttempts`: Number of failures before cooldown (default: 3) 122 | - `cooldownMinutes`: Cooldown duration in minutes (default: 10) 123 | 124 | ## Cooldown Feature 125 | The plugin includes an automatic cooldown mechanism to prevent brute force attempts: 126 | - Configurable number of failed attempts before cooldown 127 | - Configurable cooldown duration 128 | - Countdown timer only decreases when app is active 129 | - Cooldown state persists through app restarts 130 | - Users see a countdown screen during cooldown period 131 | 132 | ## Customized Steps Label 133 | You can customize challenge labels or skip certain challenges: 134 | - Use empty string `''` to skip a challenge 135 | - Use `null` to keep default label 136 | - Provide custom string for personalized labels 137 | - When `useCustomizedLabel: true`, `customizedLabel` must not be null 138 | 139 | ## Complete Example 💡 140 | 141 | ```dart 142 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 143 | 144 | class MyApp extends StatelessWidget { 145 | @override 146 | Widget build(BuildContext context) { 147 | return MaterialApp( 148 | home: Scaffold( 149 | body: Center( 150 | child: ElevatedButton( 151 | onPressed: () async { 152 | final result = await FlutterLivenessDetectionRandomizedPlugin.instance.livenessDetection( 153 | context: context, 154 | config: LivenessDetectionConfig( 155 | startWithInfoScreen: true, 156 | isDarkMode: false, 157 | showCurrentStep: true, 158 | isEnableSnackBar: true, 159 | ), 160 | ); 161 | 162 | if (result != null) { 163 | // Liveness detection successful 164 | print('Face captured: $result'); 165 | } else { 166 | // Detection failed or cancelled 167 | print('Detection failed'); 168 | } 169 | }, 170 | child: Text('Start Liveness Detection'), 171 | ), 172 | ), 173 | ), 174 | ); 175 | } 176 | } 177 | ``` 178 | 179 | ## Platform Setup 180 | 181 | ### Android 182 | Add camera permission to your `android/app/src/main/AndroidManifest.xml`: 183 | ```xml 184 | 185 | ``` 186 | Minimum SDK version: 23 187 | 188 | ### iOS 189 | Add camera usage description to `ios/Runner/Info.plist`: 190 | ```xml 191 | NSCameraUsageDescription 192 | Camera access is required for liveness detection 193 | ``` 194 | 195 | ## Testing Scenarios 🧪 196 | 197 | The example app includes 8 comprehensive liveness scenarios to test all features: 198 | 199 | ### Scenario 1: Default Configuration 200 | ```dart 201 | LivenessDetectionConfig( 202 | shuffleListWithSmileLast: true, 203 | startWithInfoScreen: true, 204 | // Standard settings 205 | ) 206 | ``` 207 | 208 | ### Scenario 2: Random Shuffle 209 | ```dart 210 | LivenessDetectionConfig( 211 | shuffleListWithSmileLast: false, 212 | durationLivenessVerify: 30, 213 | startWithInfoScreen: false, 214 | ) 215 | ``` 216 | 217 | ### Scenario 3: Dark Mode + High Resolution 218 | ```dart 219 | LivenessDetectionConfig( 220 | isDarkMode: true, 221 | cameraResolution: ResolutionPreset.high, 222 | durationLivenessVerify: 60, 223 | ) 224 | ``` 225 | 226 | ### Scenario 4: Custom Indonesian Labels 227 | ```dart 228 | LivenessDetectionConfig( 229 | useCustomizedLabel: true, 230 | customizedLabel: LivenessDetectionLabelModel( 231 | blink: 'Kedip 2-3 Kali', 232 | lookUp: 'Lihat ke Atas', 233 | smile: 'Tersenyum Lebar', 234 | ), 235 | ) 236 | ``` 237 | 238 | ### Scenario 5: Skip Steps (Minimal Challenges) 239 | ```dart 240 | LivenessDetectionConfig( 241 | useCustomizedLabel: true, 242 | customizedLabel: LivenessDetectionLabelModel( 243 | blink: 'Blink Eyes', 244 | lookDown: '', // Skip 245 | lookLeft: '', // Skip 246 | lookRight: '', // Skip 247 | lookUp: 'Look Up Please', 248 | smile: 'Smile Wide', 249 | ), 250 | ) 251 | ``` 252 | 253 | ### Scenario 6: Timer + Cooldown Features 254 | ```dart 255 | LivenessDetectionConfig( 256 | showDurationUiText: true, 257 | enableCooldownOnFailure: true, 258 | maxFailedAttempts: 2, 259 | cooldownMinutes: 5, 260 | ) 261 | ``` 262 | 263 | ### Scenario 7: Minimal Features 264 | ```dart 265 | LivenessDetectionConfig( 266 | isEnableMaxBrightness: false, 267 | isEnableSnackBar: false, 268 | showCurrentStep: false, 269 | ) 270 | ``` 271 | 272 | ### Scenario 8: All Features Enabled 273 | ```dart 274 | LivenessDetectionConfig( 275 | isDarkMode: true, 276 | cameraResolution: ResolutionPreset.high, 277 | showDurationUiText: true, 278 | enableCooldownOnFailure: true, 279 | useCustomizedLabel: true, 280 | customizedLabel: LivenessDetectionLabelModel( 281 | blink: '👁️ Kedipkan Mata', 282 | smile: '😊 Senyum Manis', 283 | ), 284 | ) 285 | ``` 286 | 287 | ## Migration Guide 🔄 288 | 289 | ### From v1.0.x to v1.1.0+ 290 | All parameters are now consolidated into the `LivenessDetectionConfig` object: 291 | 292 | **Before:** 293 | ```dart 294 | await plugin.livenessDetection( 295 | context: context, 296 | config: LivenessDetectionConfig(...), 297 | isEnableSnackBar: true, 298 | shuffleListWithSmileLast: true, 299 | showCurrentStep: true, 300 | isDarkMode: false, 301 | ); 302 | ``` 303 | 304 | **After:** 305 | ```dart 306 | await plugin.livenessDetection( 307 | context: context, 308 | config: LivenessDetectionConfig( 309 | isEnableSnackBar: true, 310 | shuffleListWithSmileLast: true, 311 | showCurrentStep: true, 312 | isDarkMode: false, 313 | // ... other parameters 314 | ), 315 | ); 316 | ``` -------------------------------------------------------------------------------- /lib/src/presentation/widgets/liveness_detection_step_overlay_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 3 | import 'package:flutter_liveness_detection_randomized_plugin/src/presentation/widgets/circular_progress_widget/circular_progress_widget.dart'; 4 | import 'package:lottie/lottie.dart'; 5 | 6 | class LivenessDetectionStepOverlayWidget extends StatefulWidget { 7 | final List steps; 8 | final VoidCallback onCompleted; 9 | final Widget camera; 10 | final CameraController? cameraController; 11 | final bool isFaceDetected; 12 | final bool showCurrentStep; 13 | final bool isDarkMode; 14 | final bool showDurationUiText; 15 | final int? duration; 16 | 17 | const LivenessDetectionStepOverlayWidget({ 18 | super.key, 19 | required this.steps, 20 | required this.onCompleted, 21 | required this.camera, 22 | required this.cameraController, 23 | required this.isFaceDetected, 24 | this.showCurrentStep = false, 25 | this.isDarkMode = true, 26 | this.showDurationUiText = false, 27 | this.duration, 28 | }); 29 | 30 | @override 31 | State createState() => 32 | LivenessDetectionStepOverlayWidgetState(); 33 | } 34 | 35 | class LivenessDetectionStepOverlayWidgetState 36 | extends State { 37 | int get currentIndex => _currentIndex; 38 | 39 | bool _isLoading = false; 40 | int _currentIndex = 0; 41 | double _currentStepIndicator = 0; 42 | late final PageController _pageController; 43 | late CircularProgressWidget _circularProgressWidget; 44 | 45 | bool _pageViewVisible = false; 46 | Timer? _countdownTimer; 47 | int _remainingDuration = 0; 48 | 49 | static const double _indicatorMaxStep = 100; 50 | static const double _heightLine = 25; 51 | 52 | double _getStepIncrement(int stepLength) { 53 | return 100 / stepLength; 54 | } 55 | 56 | String get stepCounter => "$_currentIndex/${widget.steps.length}"; 57 | 58 | @override 59 | void initState() { 60 | super.initState(); 61 | _initializeControllers(); 62 | _initializeTimer(); 63 | WidgetsBinding.instance.addPostFrameCallback((_) { 64 | setState(() { 65 | _pageViewVisible = true; 66 | }); 67 | }); 68 | debugPrint('showCurrentStep ${widget.showCurrentStep}'); 69 | } 70 | 71 | void _initializeControllers() { 72 | _pageController = PageController(initialPage: 0); 73 | _circularProgressWidget = _buildCircularIndicator(); 74 | } 75 | 76 | void _initializeTimer() { 77 | if (widget.duration != null && widget.showDurationUiText) { 78 | _remainingDuration = widget.duration!; 79 | _startCountdownTimer(); 80 | } 81 | } 82 | 83 | void _startCountdownTimer() { 84 | _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { 85 | if (_remainingDuration > 0) { 86 | setState(() { 87 | _remainingDuration--; 88 | }); 89 | } else { 90 | _countdownTimer?.cancel(); 91 | } 92 | }); 93 | } 94 | 95 | CircularProgressWidget _buildCircularIndicator() { 96 | double scale = 1.0; 97 | if (widget.cameraController != null && 98 | widget.cameraController!.value.isInitialized) { 99 | final cameraAspectRatio = widget.cameraController!.value.aspectRatio; 100 | const containerAspectRatio = 1.0; 101 | scale = cameraAspectRatio / containerAspectRatio; 102 | if (scale < 1.0) { 103 | scale = 1.0 / scale; 104 | } 105 | } 106 | 107 | return CircularProgressWidget( 108 | unselectedColor: Colors.grey, 109 | selectedColor: Colors.green, 110 | heightLine: _heightLine, 111 | current: _currentStepIndicator, 112 | maxStep: _indicatorMaxStep, 113 | child: Transform.scale( 114 | scale: scale, 115 | child: Center( 116 | child: widget.camera, 117 | ), 118 | ), 119 | ); 120 | } 121 | 122 | @override 123 | void dispose() { 124 | _pageController.dispose(); 125 | _countdownTimer?.cancel(); 126 | super.dispose(); 127 | } 128 | 129 | Future nextPage() async { 130 | if (_isLoading) return; 131 | 132 | if (_currentIndex + 1 <= widget.steps.length - 1) { 133 | await _handleNextStep(); 134 | } else { 135 | await _handleCompletion(); 136 | } 137 | } 138 | 139 | Future _handleNextStep() async { 140 | _showLoader(); 141 | await Future.delayed(const Duration(milliseconds: 100)); 142 | await _pageController.nextPage( 143 | duration: const Duration(milliseconds: 1), 144 | curve: Curves.easeIn, 145 | ); 146 | await Future.delayed(const Duration(seconds: 1)); 147 | _hideLoader(); 148 | _updateState(); 149 | } 150 | 151 | Future _handleCompletion() async { 152 | _updateState(); 153 | await Future.delayed(const Duration(milliseconds: 500)); 154 | widget.onCompleted(); 155 | } 156 | 157 | void _updateState() { 158 | if (mounted) { 159 | setState(() { 160 | _currentIndex++; 161 | _currentStepIndicator += _getStepIncrement(widget.steps.length); 162 | _circularProgressWidget = _buildCircularIndicator(); 163 | }); 164 | } 165 | } 166 | 167 | void reset() { 168 | _pageController.jumpToPage(0); 169 | if (mounted) { 170 | setState(() { 171 | _currentIndex = 0; 172 | _currentStepIndicator = 0; 173 | _circularProgressWidget = _buildCircularIndicator(); 174 | }); 175 | } 176 | } 177 | 178 | void _showLoader() { 179 | if (mounted) setState(() => _isLoading = true); 180 | } 181 | 182 | void _hideLoader() { 183 | if (mounted) setState(() => _isLoading = false); 184 | } 185 | 186 | @override 187 | Widget build(BuildContext context) { 188 | return SafeArea( 189 | minimum: const EdgeInsets.all(16), 190 | child: Container( 191 | margin: const EdgeInsets.all(12), 192 | height: double.infinity, 193 | width: double.infinity, 194 | color: Colors.transparent, 195 | child: Stack( 196 | children: [ 197 | GestureDetector( 198 | onTap: () => Navigator.pop(context), 199 | child: widget.showCurrentStep 200 | ? Row( 201 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 202 | children: [ 203 | Text( 204 | 'Back', 205 | style: TextStyle( 206 | color: widget.isDarkMode 207 | ? Colors.white 208 | : Colors.black), 209 | ), 210 | Visibility( 211 | replacement: const SizedBox.shrink(), 212 | visible: widget.showDurationUiText, 213 | child: Text( 214 | _getRemainingTimeText(_remainingDuration), 215 | style: TextStyle( 216 | color: widget.isDarkMode 217 | ? Colors.white 218 | : Colors.black, 219 | fontWeight: FontWeight.bold, 220 | ), 221 | ), 222 | ), 223 | Text( 224 | stepCounter, 225 | style: TextStyle( 226 | color: widget.isDarkMode 227 | ? Colors.white 228 | : Colors.black), 229 | ) 230 | ], 231 | ) 232 | : Text('Back', 233 | style: TextStyle( 234 | color: 235 | widget.isDarkMode ? Colors.white : Colors.black)), 236 | ), 237 | _buildBody(), 238 | ], 239 | ), 240 | ), 241 | ); 242 | } 243 | 244 | Widget _buildBody() { 245 | return Column( 246 | mainAxisAlignment: MainAxisAlignment.center, 247 | crossAxisAlignment: CrossAxisAlignment.center, 248 | mainAxisSize: MainAxisSize.max, 249 | children: [ 250 | _buildCircularCamera(), 251 | const SizedBox(height: 16), 252 | _buildFaceDetectionStatus(), 253 | const SizedBox(height: 16), 254 | Visibility( 255 | visible: _pageViewVisible, 256 | replacement: const CircularProgressIndicator.adaptive(), 257 | child: _buildStepPageView(), 258 | ), 259 | const SizedBox(height: 16), 260 | widget.isDarkMode ? _buildLoaderDarkMode() : _buildLoaderLightMode(), 261 | ], 262 | ); 263 | } 264 | 265 | Widget _buildCircularCamera() { 266 | return SizedBox( 267 | height: 300, 268 | width: 300, 269 | child: ClipRRect( 270 | borderRadius: BorderRadius.circular(1000), 271 | child: _circularProgressWidget, 272 | ), 273 | ); 274 | } 275 | 276 | String _getRemainingTimeText(int duration) { 277 | int minutes = duration ~/ 60; 278 | int seconds = duration % 60; 279 | return "${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}"; 280 | } 281 | 282 | Widget _buildFaceDetectionStatus() { 283 | return Row( 284 | mainAxisAlignment: MainAxisAlignment.center, 285 | children: [ 286 | SizedBox( 287 | child: widget.isDarkMode 288 | ? LottieBuilder.asset( 289 | widget.isFaceDetected 290 | ? 'packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-detected.json' 291 | : 'packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-id-anim.json', 292 | height: widget.isFaceDetected ? 32 : 22, 293 | width: widget.isFaceDetected ? 32 : 22, 294 | ) 295 | : ColorFiltered( 296 | colorFilter: ColorFilter.mode( 297 | widget.isFaceDetected ? Colors.green : Colors.black, 298 | BlendMode.modulate), 299 | child: LottieBuilder.asset( 300 | widget.isFaceDetected 301 | ? 'packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-detected.json' 302 | : 'packages/flutter_liveness_detection_randomized_plugin/src/core/assets/face-id-anim.json', 303 | height: widget.isFaceDetected ? 32 : 22, 304 | width: widget.isFaceDetected ? 32 : 22, 305 | )), 306 | ), 307 | const SizedBox(width: 16), 308 | Text( 309 | widget.isFaceDetected ? 'User Face Found' : 'User Face Not Found...', 310 | style: 311 | TextStyle(color: widget.isDarkMode ? Colors.white : Colors.black), 312 | ), 313 | ], 314 | ); 315 | } 316 | 317 | Widget _buildStepPageView() { 318 | return SizedBox( 319 | height: MediaQuery.of(context).size.height / 10, 320 | width: MediaQuery.of(context).size.width, 321 | child: AbsorbPointer( 322 | absorbing: true, 323 | child: PageView.builder( 324 | controller: _pageController, 325 | itemCount: widget.steps.length, 326 | itemBuilder: _buildStepItem, 327 | ), 328 | ), 329 | ); 330 | } 331 | 332 | Widget _buildStepItem(BuildContext context, int index) { 333 | return Padding( 334 | padding: const EdgeInsets.all(10), 335 | child: Container( 336 | decoration: BoxDecoration( 337 | color: widget.isDarkMode ? Colors.black : Colors.white, 338 | borderRadius: BorderRadius.circular(20), 339 | ), 340 | alignment: Alignment.center, 341 | margin: const EdgeInsets.symmetric(horizontal: 30), 342 | padding: const EdgeInsets.all(10), 343 | child: Text( 344 | widget.steps[index].title, 345 | textAlign: TextAlign.center, 346 | style: TextStyle( 347 | color: widget.isDarkMode ? Colors.white : Colors.black, 348 | fontSize: 24, 349 | fontWeight: FontWeight.w500, 350 | ), 351 | ), 352 | ), 353 | ); 354 | } 355 | 356 | Widget _buildLoaderDarkMode() { 357 | return Center( 358 | child: CupertinoActivityIndicator( 359 | color: !_isLoading ? Colors.transparent : Colors.white, 360 | ), 361 | ); 362 | } 363 | 364 | Widget _buildLoaderLightMode() { 365 | return Center( 366 | child: CupertinoActivityIndicator( 367 | color: _isLoading ? Colors.transparent : Colors.white, 368 | ), 369 | ); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "3.6.1" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | camera: 29 | dependency: transitive 30 | description: 31 | name: camera 32 | sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "0.10.6" 36 | camera_android: 37 | dependency: transitive 38 | description: 39 | name: camera_android 40 | sha256: "007c57cdcace4751014071e3d42f2eb8a64a519254abed35b714223d81d66234" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "0.10.10" 44 | camera_avfoundation: 45 | dependency: transitive 46 | description: 47 | name: camera_avfoundation 48 | sha256: "1eeb9ce7c9a397e312343fd7db337d95f35c3e65ad5a62ff637c8abce5102b98" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "0.9.18+8" 52 | camera_platform_interface: 53 | dependency: transitive 54 | description: 55 | name: camera_platform_interface 56 | sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.9.0" 60 | camera_web: 61 | dependency: transitive 62 | description: 63 | name: camera_web 64 | sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "0.3.5" 68 | characters: 69 | dependency: transitive 70 | description: 71 | name: characters 72 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.4.0" 76 | clock: 77 | dependency: transitive 78 | description: 79 | name: clock 80 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.1.2" 84 | collection: 85 | dependency: transitive 86 | description: 87 | name: collection 88 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.19.1" 92 | cross_file: 93 | dependency: transitive 94 | description: 95 | name: cross_file 96 | sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "0.3.4+2" 100 | crypto: 101 | dependency: transitive 102 | description: 103 | name: crypto 104 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.0.6" 108 | cupertino_icons: 109 | dependency: "direct main" 110 | description: 111 | name: cupertino_icons 112 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.0.8" 116 | equatable: 117 | dependency: transitive 118 | description: 119 | name: equatable 120 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.7" 124 | fake_async: 125 | dependency: transitive 126 | description: 127 | name: fake_async 128 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.3" 132 | ffi: 133 | dependency: transitive 134 | description: 135 | name: ffi 136 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.3" 140 | file: 141 | dependency: transitive 142 | description: 143 | name: file 144 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "7.0.0" 148 | flutter: 149 | dependency: "direct main" 150 | description: flutter 151 | source: sdk 152 | version: "0.0.0" 153 | flutter_driver: 154 | dependency: transitive 155 | description: flutter 156 | source: sdk 157 | version: "0.0.0" 158 | flutter_lints: 159 | dependency: "direct dev" 160 | description: 161 | name: flutter_lints 162 | sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" 163 | url: "https://pub.dev" 164 | source: hosted 165 | version: "4.0.0" 166 | flutter_liveness_detection_randomized_plugin: 167 | dependency: "direct main" 168 | description: 169 | path: ".." 170 | relative: true 171 | source: path 172 | version: "1.1.0" 173 | flutter_plugin_android_lifecycle: 174 | dependency: transitive 175 | description: 176 | name: flutter_plugin_android_lifecycle 177 | sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf" 178 | url: "https://pub.dev" 179 | source: hosted 180 | version: "2.0.26" 181 | flutter_test: 182 | dependency: "direct dev" 183 | description: flutter 184 | source: sdk 185 | version: "0.0.0" 186 | flutter_web_plugins: 187 | dependency: transitive 188 | description: flutter 189 | source: sdk 190 | version: "0.0.0" 191 | fuchsia_remote_debug_protocol: 192 | dependency: transitive 193 | description: flutter 194 | source: sdk 195 | version: "0.0.0" 196 | google_mlkit_commons: 197 | dependency: transitive 198 | description: 199 | name: google_mlkit_commons 200 | sha256: "8f40fbac10685cad4715d11e6a0d86837d9ad7168684dfcad29610282a88e67a" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "0.11.0" 204 | google_mlkit_face_detection: 205 | dependency: transitive 206 | description: 207 | name: google_mlkit_face_detection 208 | sha256: f336737d5b8a86797fd4368f42a5c26aeaa9c6dcc5243f0a16b5f6f663cfb70a 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "0.13.1" 212 | image: 213 | dependency: transitive 214 | description: 215 | name: image 216 | sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "4.3.0" 220 | integration_test: 221 | dependency: "direct dev" 222 | description: flutter 223 | source: sdk 224 | version: "0.0.0" 225 | leak_tracker: 226 | dependency: transitive 227 | description: 228 | name: leak_tracker 229 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "11.0.2" 233 | leak_tracker_flutter_testing: 234 | dependency: transitive 235 | description: 236 | name: leak_tracker_flutter_testing 237 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "3.0.10" 241 | leak_tracker_testing: 242 | dependency: transitive 243 | description: 244 | name: leak_tracker_testing 245 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "3.0.2" 249 | lints: 250 | dependency: transitive 251 | description: 252 | name: lints 253 | sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "4.0.0" 257 | lottie: 258 | dependency: transitive 259 | description: 260 | name: lottie 261 | sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 262 | url: "https://pub.dev" 263 | source: hosted 264 | version: "2.7.0" 265 | matcher: 266 | dependency: transitive 267 | description: 268 | name: matcher 269 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "0.12.17" 273 | material_color_utilities: 274 | dependency: transitive 275 | description: 276 | name: material_color_utilities 277 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "0.11.1" 281 | meta: 282 | dependency: transitive 283 | description: 284 | name: meta 285 | sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" 286 | url: "https://pub.dev" 287 | source: hosted 288 | version: "1.17.0" 289 | path: 290 | dependency: transitive 291 | description: 292 | name: path 293 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 294 | url: "https://pub.dev" 295 | source: hosted 296 | version: "1.9.1" 297 | path_provider: 298 | dependency: transitive 299 | description: 300 | name: path_provider 301 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 302 | url: "https://pub.dev" 303 | source: hosted 304 | version: "2.1.5" 305 | path_provider_android: 306 | dependency: transitive 307 | description: 308 | name: path_provider_android 309 | sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" 310 | url: "https://pub.dev" 311 | source: hosted 312 | version: "2.2.15" 313 | path_provider_foundation: 314 | dependency: transitive 315 | description: 316 | name: path_provider_foundation 317 | sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" 318 | url: "https://pub.dev" 319 | source: hosted 320 | version: "2.4.1" 321 | path_provider_linux: 322 | dependency: transitive 323 | description: 324 | name: path_provider_linux 325 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 326 | url: "https://pub.dev" 327 | source: hosted 328 | version: "2.2.1" 329 | path_provider_platform_interface: 330 | dependency: transitive 331 | description: 332 | name: path_provider_platform_interface 333 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 334 | url: "https://pub.dev" 335 | source: hosted 336 | version: "2.1.2" 337 | path_provider_windows: 338 | dependency: transitive 339 | description: 340 | name: path_provider_windows 341 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 342 | url: "https://pub.dev" 343 | source: hosted 344 | version: "2.3.0" 345 | petitparser: 346 | dependency: transitive 347 | description: 348 | name: petitparser 349 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 350 | url: "https://pub.dev" 351 | source: hosted 352 | version: "6.0.2" 353 | platform: 354 | dependency: transitive 355 | description: 356 | name: platform 357 | sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" 358 | url: "https://pub.dev" 359 | source: hosted 360 | version: "3.1.5" 361 | plugin_platform_interface: 362 | dependency: transitive 363 | description: 364 | name: plugin_platform_interface 365 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 366 | url: "https://pub.dev" 367 | source: hosted 368 | version: "2.1.8" 369 | process: 370 | dependency: transitive 371 | description: 372 | name: process 373 | sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" 374 | url: "https://pub.dev" 375 | source: hosted 376 | version: "5.0.2" 377 | screen_brightness: 378 | dependency: transitive 379 | description: 380 | name: screen_brightness 381 | sha256: eca7bd9d2c3c688bcad14855361cab7097839400b6b4a56f62b7ae511c709958 382 | url: "https://pub.dev" 383 | source: hosted 384 | version: "2.1.2" 385 | screen_brightness_android: 386 | dependency: transitive 387 | description: 388 | name: screen_brightness_android 389 | sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e" 390 | url: "https://pub.dev" 391 | source: hosted 392 | version: "2.1.1" 393 | screen_brightness_ios: 394 | dependency: transitive 395 | description: 396 | name: screen_brightness_ios 397 | sha256: bfd9bfd0ac852e7aa170e7e356cc27195b2a75037b72c8c6336cf6fb2115cffb 398 | url: "https://pub.dev" 399 | source: hosted 400 | version: "2.1.1" 401 | screen_brightness_macos: 402 | dependency: transitive 403 | description: 404 | name: screen_brightness_macos 405 | sha256: "4edf330ad21078686d8bfaf89413325fbaf571dcebe1e89254d675a3f288b5b9" 406 | url: "https://pub.dev" 407 | source: hosted 408 | version: "2.1.1" 409 | screen_brightness_platform_interface: 410 | dependency: transitive 411 | description: 412 | name: screen_brightness_platform_interface 413 | sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c" 414 | url: "https://pub.dev" 415 | source: hosted 416 | version: "2.1.0" 417 | screen_brightness_windows: 418 | dependency: transitive 419 | description: 420 | name: screen_brightness_windows 421 | sha256: d3518bf0f5d7a884cee2c14449ae0b36803802866de09f7ef74077874b6b2448 422 | url: "https://pub.dev" 423 | source: hosted 424 | version: "2.1.0" 425 | shared_preferences: 426 | dependency: transitive 427 | description: 428 | name: shared_preferences 429 | sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" 430 | url: "https://pub.dev" 431 | source: hosted 432 | version: "2.5.3" 433 | shared_preferences_android: 434 | dependency: transitive 435 | description: 436 | name: shared_preferences_android 437 | sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" 438 | url: "https://pub.dev" 439 | source: hosted 440 | version: "2.4.15" 441 | shared_preferences_foundation: 442 | dependency: transitive 443 | description: 444 | name: shared_preferences_foundation 445 | sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" 446 | url: "https://pub.dev" 447 | source: hosted 448 | version: "2.5.5" 449 | shared_preferences_linux: 450 | dependency: transitive 451 | description: 452 | name: shared_preferences_linux 453 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 454 | url: "https://pub.dev" 455 | source: hosted 456 | version: "2.4.1" 457 | shared_preferences_platform_interface: 458 | dependency: transitive 459 | description: 460 | name: shared_preferences_platform_interface 461 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 462 | url: "https://pub.dev" 463 | source: hosted 464 | version: "2.4.1" 465 | shared_preferences_web: 466 | dependency: transitive 467 | description: 468 | name: shared_preferences_web 469 | sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 470 | url: "https://pub.dev" 471 | source: hosted 472 | version: "2.4.3" 473 | shared_preferences_windows: 474 | dependency: transitive 475 | description: 476 | name: shared_preferences_windows 477 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 478 | url: "https://pub.dev" 479 | source: hosted 480 | version: "2.4.1" 481 | sky_engine: 482 | dependency: transitive 483 | description: flutter 484 | source: sdk 485 | version: "0.0.0" 486 | source_span: 487 | dependency: transitive 488 | description: 489 | name: source_span 490 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 491 | url: "https://pub.dev" 492 | source: hosted 493 | version: "1.10.0" 494 | stack_trace: 495 | dependency: transitive 496 | description: 497 | name: stack_trace 498 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 499 | url: "https://pub.dev" 500 | source: hosted 501 | version: "1.12.1" 502 | stream_channel: 503 | dependency: transitive 504 | description: 505 | name: stream_channel 506 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 507 | url: "https://pub.dev" 508 | source: hosted 509 | version: "2.1.4" 510 | stream_transform: 511 | dependency: transitive 512 | description: 513 | name: stream_transform 514 | sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 515 | url: "https://pub.dev" 516 | source: hosted 517 | version: "2.1.1" 518 | string_scanner: 519 | dependency: transitive 520 | description: 521 | name: string_scanner 522 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 523 | url: "https://pub.dev" 524 | source: hosted 525 | version: "1.2.0" 526 | sync_http: 527 | dependency: transitive 528 | description: 529 | name: sync_http 530 | sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" 531 | url: "https://pub.dev" 532 | source: hosted 533 | version: "0.3.1" 534 | term_glyph: 535 | dependency: transitive 536 | description: 537 | name: term_glyph 538 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 539 | url: "https://pub.dev" 540 | source: hosted 541 | version: "1.2.1" 542 | test_api: 543 | dependency: transitive 544 | description: 545 | name: test_api 546 | sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 547 | url: "https://pub.dev" 548 | source: hosted 549 | version: "0.7.7" 550 | typed_data: 551 | dependency: transitive 552 | description: 553 | name: typed_data 554 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 555 | url: "https://pub.dev" 556 | source: hosted 557 | version: "1.4.0" 558 | vector_math: 559 | dependency: transitive 560 | description: 561 | name: vector_math 562 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 563 | url: "https://pub.dev" 564 | source: hosted 565 | version: "2.2.0" 566 | vm_service: 567 | dependency: transitive 568 | description: 569 | name: vm_service 570 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 571 | url: "https://pub.dev" 572 | source: hosted 573 | version: "14.2.5" 574 | web: 575 | dependency: transitive 576 | description: 577 | name: web 578 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 579 | url: "https://pub.dev" 580 | source: hosted 581 | version: "1.1.1" 582 | webdriver: 583 | dependency: transitive 584 | description: 585 | name: webdriver 586 | sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" 587 | url: "https://pub.dev" 588 | source: hosted 589 | version: "3.0.3" 590 | xdg_directories: 591 | dependency: transitive 592 | description: 593 | name: xdg_directories 594 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 595 | url: "https://pub.dev" 596 | source: hosted 597 | version: "1.1.0" 598 | xml: 599 | dependency: transitive 600 | description: 601 | name: xml 602 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 603 | url: "https://pub.dev" 604 | source: hosted 605 | version: "6.5.0" 606 | sdks: 607 | dart: ">=3.10.0 <4.0.0" 608 | flutter: ">=3.38.1" 609 | -------------------------------------------------------------------------------- /lib/src/presentation/views/liveness_detection_view.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: depend_on_referenced_packages 2 | import 'package:flutter_liveness_detection_randomized_plugin/index.dart'; 3 | import 'package:flutter_liveness_detection_randomized_plugin/src/core/constants/liveness_detection_step_constant.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:screen_brightness/screen_brightness.dart'; 6 | import 'package:image/image.dart' as img; 7 | import 'package:path_provider/path_provider.dart'; 8 | 9 | List availableCams = []; 10 | 11 | class LivenessDetectionView extends StatefulWidget { 12 | final LivenessDetectionConfig config; 13 | 14 | const LivenessDetectionView({ 15 | super.key, 16 | required this.config, 17 | }); 18 | 19 | @override 20 | State createState() => _LivenessDetectionScreenState(); 21 | } 22 | 23 | class _LivenessDetectionScreenState extends State { 24 | // Camera related variables 25 | CameraController? _cameraController; 26 | int _cameraIndex = 0; 27 | bool _isBusy = false; 28 | bool _isTakingPicture = false; 29 | Timer? _timerToDetectFace; 30 | 31 | // Detection state variables 32 | late bool _isInfoStepCompleted; 33 | bool _isProcessingStep = false; 34 | bool _faceDetectedState = false; 35 | List _shuffledSteps = []; 36 | 37 | // Brightness Screen 38 | Future setApplicationBrightness(double brightness) async { 39 | try { 40 | await ScreenBrightness.instance.setApplicationScreenBrightness( 41 | brightness, 42 | ); 43 | } catch (e) { 44 | throw 'Failed to set application brightness'; 45 | } 46 | } 47 | 48 | Future resetApplicationBrightness() async { 49 | try { 50 | await ScreenBrightness.instance.resetApplicationScreenBrightness(); 51 | } catch (e) { 52 | throw 'Failed to reset application brightness'; 53 | } 54 | } 55 | 56 | // Steps related variables 57 | late final List steps; 58 | final GlobalKey _stepsKey = 59 | GlobalKey(); 60 | 61 | static void shuffleListLivenessChallenge({ 62 | required List list, 63 | required bool isSmileLast, 64 | }) { 65 | if (isSmileLast) { 66 | int? smileIndex = list.indexWhere( 67 | (item) => item.step == LivenessDetectionStep.smile, 68 | ); 69 | 70 | if (smileIndex != -1) { 71 | LivenessDetectionStepItem smileItem = list.removeAt(smileIndex); 72 | list.shuffle(Random()); 73 | list.add(smileItem); 74 | } else { 75 | list.shuffle(Random()); 76 | } 77 | } else { 78 | list.shuffle(Random()); 79 | } 80 | } 81 | 82 | Future _compressImage(XFile originalFile) async { 83 | final int quality = widget.config.imageQuality; 84 | 85 | if (quality >= 100) { 86 | return originalFile; 87 | } 88 | 89 | try { 90 | final bytes = await originalFile.readAsBytes(); 91 | 92 | final img.Image? originalImage = img.decodeImage(bytes); 93 | if (originalImage == null) { 94 | return originalFile; 95 | } 96 | 97 | final tempDir = await getTemporaryDirectory(); 98 | final String targetPath = 99 | '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; 100 | 101 | final compressedBytes = img.encodeJpg(originalImage, quality: quality); 102 | 103 | final File compressedFile = await File( 104 | targetPath, 105 | ).writeAsBytes(compressedBytes); 106 | 107 | return XFile(compressedFile.path); 108 | } catch (e) { 109 | debugPrint("Error compressing image: $e"); 110 | return originalFile; 111 | } 112 | } 113 | 114 | List manualRandomItemLiveness(List list) { 115 | final random = Random(); 116 | List shuffledList = List.from(list); 117 | for (int i = shuffledList.length - 1; i > 0; i--) { 118 | int j = random.nextInt(i + 1); 119 | 120 | T temp = shuffledList[i]; 121 | shuffledList[i] = shuffledList[j]; 122 | shuffledList[j] = temp; 123 | } 124 | return shuffledList; 125 | } 126 | 127 | List customizedLivenessLabel( 128 | LivenessDetectionLabelModel label, 129 | ) { 130 | List customizedSteps = []; 131 | 132 | // Add blink step if not explicitly skipped (empty string skips) 133 | if (label.blink != "") { 134 | customizedSteps.add( 135 | LivenessDetectionStepItem( 136 | step: LivenessDetectionStep.blink, 137 | title: label.blink ?? "Blink 2-3 Times", 138 | ), 139 | ); 140 | } 141 | 142 | // Add lookRight step if not explicitly skipped 143 | if (label.lookRight != "") { 144 | customizedSteps.add( 145 | LivenessDetectionStepItem( 146 | step: LivenessDetectionStep.lookRight, 147 | title: label.lookRight ?? "Look RIGHT", 148 | ), 149 | ); 150 | } 151 | 152 | // Add lookLeft step if not explicitly skipped 153 | if (label.lookLeft != "") { 154 | customizedSteps.add( 155 | LivenessDetectionStepItem( 156 | step: LivenessDetectionStep.lookLeft, 157 | title: label.lookLeft ?? "Look LEFT", 158 | ), 159 | ); 160 | } 161 | 162 | // Add lookUp step if not explicitly skipped 163 | if (label.lookUp != "") { 164 | customizedSteps.add( 165 | LivenessDetectionStepItem( 166 | step: LivenessDetectionStep.lookUp, 167 | title: label.lookUp ?? "Look UP", 168 | ), 169 | ); 170 | } 171 | 172 | // Add lookDown step if not explicitly skipped 173 | if (label.lookDown != "") { 174 | customizedSteps.add( 175 | LivenessDetectionStepItem( 176 | step: LivenessDetectionStep.lookDown, 177 | title: label.lookDown ?? "Look DOWN", 178 | ), 179 | ); 180 | } 181 | 182 | // Add smile step if not explicitly skipped 183 | if (label.smile != "") { 184 | customizedSteps.add( 185 | LivenessDetectionStepItem( 186 | step: LivenessDetectionStep.smile, 187 | title: label.smile ?? "Smile", 188 | ), 189 | ); 190 | } 191 | 192 | return customizedSteps; 193 | } 194 | 195 | @override 196 | void initState() { 197 | _preInitCallBack(); 198 | super.initState(); 199 | if (widget.config.enableCooldownOnFailure) { 200 | LivenessCooldownService.instance.configure( 201 | maxFailedAttempts: widget.config.maxFailedAttempts, 202 | cooldownMinutes: widget.config.cooldownMinutes, 203 | ); 204 | LivenessCooldownService.instance.initializeCooldownTimer(); 205 | } 206 | WidgetsBinding.instance.addPostFrameCallback((_) => _postFrameCallBack()); 207 | } 208 | 209 | @override 210 | void dispose() { 211 | _timerToDetectFace?.cancel(); 212 | _timerToDetectFace = null; 213 | _cameraController?.dispose(); 214 | 215 | if (widget.config.isEnableMaxBrightness) { 216 | resetApplicationBrightness(); 217 | } 218 | super.dispose(); 219 | } 220 | 221 | void _preInitCallBack() { 222 | _isInfoStepCompleted = !widget.config.startWithInfoScreen; 223 | 224 | // Initialize and shuffle steps fresh each time 225 | _initializeShuffledSteps(); 226 | 227 | if (widget.config.isEnableMaxBrightness) { 228 | setApplicationBrightness(1.0); 229 | } 230 | } 231 | 232 | void _postFrameCallBack() async { 233 | availableCams = await availableCameras(); 234 | if (availableCams.any( 235 | (element) => 236 | element.lensDirection == CameraLensDirection.front && 237 | element.sensorOrientation == 90, 238 | )) { 239 | _cameraIndex = availableCams.indexOf( 240 | availableCams.firstWhere( 241 | (element) => 242 | element.lensDirection == CameraLensDirection.front && 243 | element.sensorOrientation == 90, 244 | ), 245 | ); 246 | } else { 247 | _cameraIndex = availableCams.indexOf( 248 | availableCams.firstWhere( 249 | (element) => element.lensDirection == CameraLensDirection.front, 250 | ), 251 | ); 252 | } 253 | if (!widget.config.startWithInfoScreen) { 254 | _startLiveFeed(); 255 | } 256 | 257 | // Steps are shuffled fresh in _preInitCallBack 258 | } 259 | 260 | void _startLiveFeed() async { 261 | final camera = availableCams[_cameraIndex]; 262 | _cameraController = CameraController( 263 | camera, 264 | widget.config.cameraResolution, 265 | enableAudio: false, 266 | imageFormatGroup: Platform.isAndroid 267 | ? ImageFormatGroup.nv21 268 | : ImageFormatGroup.bgra8888, 269 | ); 270 | 271 | _cameraController?.initialize().then((_) { 272 | if (!mounted) return; 273 | _cameraController?.startImageStream(_processCameraImage); 274 | setState(() {}); 275 | }); 276 | _startFaceDetectionTimer(); 277 | } 278 | 279 | void _startFaceDetectionTimer() { 280 | _timerToDetectFace = Timer( 281 | Duration(seconds: widget.config.durationLivenessVerify ?? 45), 282 | () => _onDetectionCompleted(imgToReturn: null), 283 | ); 284 | } 285 | 286 | Future _processCameraImage(CameraImage cameraImage) async { 287 | final camera = availableCams[_cameraIndex]; 288 | final imageRotation = InputImageRotationValue.fromRawValue( 289 | camera.sensorOrientation, 290 | ); 291 | if (imageRotation == null) return; 292 | 293 | InputImage? inputImage; 294 | 295 | if (Platform.isAndroid) { 296 | if (cameraImage.format.group == ImageFormatGroup.nv21) { 297 | inputImage = InputImage.fromBytes( 298 | bytes: cameraImage.planes[0].bytes, 299 | metadata: InputImageMetadata( 300 | size: Size( 301 | cameraImage.width.toDouble(), 302 | cameraImage.height.toDouble(), 303 | ), 304 | rotation: imageRotation, 305 | format: InputImageFormat.nv21, 306 | bytesPerRow: cameraImage.planes[0].bytesPerRow, 307 | ), 308 | ); 309 | } 310 | } else if (Platform.isIOS) { 311 | if (cameraImage.format.group == ImageFormatGroup.bgra8888) { 312 | inputImage = InputImage.fromBytes( 313 | bytes: cameraImage.planes[0].bytes, 314 | metadata: InputImageMetadata( 315 | size: Size( 316 | cameraImage.width.toDouble(), 317 | cameraImage.height.toDouble(), 318 | ), 319 | rotation: imageRotation, 320 | format: InputImageFormat.bgra8888, 321 | bytesPerRow: cameraImage.planes[0].bytesPerRow, 322 | ), 323 | ); 324 | } 325 | } 326 | 327 | if (inputImage != null) { 328 | _processImage(inputImage); 329 | } 330 | } 331 | 332 | Future _processImage(InputImage inputImage) async { 333 | if (_isBusy) return; 334 | _isBusy = true; 335 | 336 | final faces = await MachineLearningKitHelper.instance.processInputImage( 337 | inputImage, 338 | ); 339 | 340 | if (inputImage.metadata?.size != null && 341 | inputImage.metadata?.rotation != null) { 342 | if (faces.isEmpty) { 343 | _resetSteps(); 344 | if (mounted) setState(() => _faceDetectedState = false); 345 | } else { 346 | if (mounted) setState(() => _faceDetectedState = true); 347 | final currentIndex = _stepsKey.currentState?.currentIndex ?? 0; 348 | List currentSteps = _getStepsToUse(); 349 | if (currentIndex < currentSteps.length) { 350 | _detectFace( 351 | face: faces.first, 352 | step: currentSteps[currentIndex].step, 353 | ); 354 | } 355 | } 356 | } else { 357 | _resetSteps(); 358 | } 359 | 360 | _isBusy = false; 361 | if (mounted) setState(() {}); 362 | } 363 | 364 | void _detectFace({ 365 | required Face face, 366 | required LivenessDetectionStep step, 367 | }) async { 368 | if (_isProcessingStep) return; 369 | 370 | debugPrint('Current Step: $step'); 371 | 372 | switch (step) { 373 | case LivenessDetectionStep.blink: 374 | await _handlingBlinkStep(face: face, step: step); 375 | break; 376 | 377 | case LivenessDetectionStep.lookRight: 378 | await _handlingTurnRight(face: face, step: step); 379 | break; 380 | 381 | case LivenessDetectionStep.lookLeft: 382 | await _handlingTurnLeft(face: face, step: step); 383 | break; 384 | 385 | case LivenessDetectionStep.lookUp: 386 | await _handlingLookUp(face: face, step: step); 387 | break; 388 | 389 | case LivenessDetectionStep.lookDown: 390 | await _handlingLookDown(face: face, step: step); 391 | break; 392 | 393 | case LivenessDetectionStep.smile: 394 | await _handlingSmile(face: face, step: step); 395 | break; 396 | } 397 | } 398 | 399 | Future _completeStep({required LivenessDetectionStep step}) async { 400 | if (mounted) setState(() {}); 401 | await _stepsKey.currentState?.nextPage(); 402 | _stopProcessing(); 403 | } 404 | 405 | void _takePicture() async { 406 | try { 407 | if (_cameraController == null || _isTakingPicture) return; 408 | 409 | if (mounted) setState(() => _isTakingPicture = true); 410 | await _cameraController?.stopImageStream(); 411 | 412 | final XFile? clickedImage = await _cameraController?.takePicture(); 413 | if (clickedImage == null) { 414 | _startLiveFeed(); 415 | if (mounted) setState(() => _isTakingPicture = false); 416 | return; 417 | } 418 | 419 | final XFile? finalImage = await _compressImage(clickedImage); 420 | 421 | debugPrint('Final image path: ${finalImage?.path}'); 422 | _onDetectionCompleted(imgToReturn: finalImage); 423 | } catch (e) { 424 | debugPrint('Error taking picture: $e'); 425 | if (mounted) setState(() => _isTakingPicture = false); 426 | _startLiveFeed(); 427 | } 428 | } 429 | 430 | void _onDetectionCompleted({XFile? imgToReturn}) async { 431 | final String? imgPath = imgToReturn?.path; 432 | 433 | if (imgPath != null) { 434 | final File imageFile = File(imgPath); 435 | final int fileSizeInBytes = await imageFile.length(); 436 | final double sizeInKb = fileSizeInBytes / 1024; 437 | debugPrint('Image result size : ${sizeInKb.toStringAsFixed(2)} KB'); 438 | } 439 | if (widget.config.isEnableSnackBar) { 440 | final snackBar = SnackBar( 441 | content: Text( 442 | imgToReturn == null 443 | ? 'Verification of liveness detection failed, please try again. (Exceeds time limit ${widget.config.durationLivenessVerify ?? 45} second.)' 444 | : 'Verification of liveness detection success!', 445 | ), 446 | ); 447 | if (!mounted) return; 448 | ScaffoldMessenger.of(context).showSnackBar(snackBar); 449 | } 450 | if (!mounted) return; 451 | Navigator.of(context).pop(imgPath); 452 | } 453 | 454 | void _resetSteps() { 455 | List currentSteps = _getStepsToUse(); 456 | 457 | for (var step in currentSteps) { 458 | final index = currentSteps.indexWhere((p1) => p1.step == step.step); 459 | if (index != -1) { 460 | currentSteps[index] = currentSteps[index].copyWith(); 461 | } 462 | } 463 | 464 | if (_stepsKey.currentState?.currentIndex != 0) { 465 | _stepsKey.currentState?.reset(); 466 | } 467 | 468 | if (mounted) setState(() {}); 469 | } 470 | 471 | void _startProcessing() { 472 | if (!mounted) return; 473 | if (mounted) setState(() => _isProcessingStep = true); 474 | } 475 | 476 | void _stopProcessing() { 477 | if (!mounted) return; 478 | if (mounted) setState(() => _isProcessingStep = false); 479 | } 480 | 481 | /// Initialize and shuffle steps fresh each time 482 | void _initializeShuffledSteps() { 483 | List baseSteps; 484 | 485 | if (widget.config.useCustomizedLabel && widget.config.customizedLabel != null) { 486 | baseSteps = customizedLivenessLabel(widget.config.customizedLabel!); 487 | } else { 488 | baseSteps = List.from(stepLiveness); // Create a copy to avoid modifying the original 489 | } 490 | 491 | shuffleListLivenessChallenge( 492 | list: baseSteps, 493 | isSmileLast: widget.config.useCustomizedLabel 494 | ? false 495 | : widget.config.shuffleListWithSmileLast, 496 | ); 497 | 498 | _shuffledSteps = baseSteps; 499 | } 500 | 501 | /// Helper method to get the shuffled steps list 502 | List _getStepsToUse() { 503 | return _shuffledSteps; 504 | } 505 | 506 | @override 507 | Widget build(BuildContext context) { 508 | return Scaffold( 509 | backgroundColor: widget.config.isDarkMode ? Colors.black : Colors.white, 510 | body: _buildBody(), 511 | ); 512 | } 513 | 514 | Widget _buildBody() { 515 | return Stack( 516 | children: [ 517 | _isInfoStepCompleted 518 | ? _buildDetectionBody() 519 | : LivenessDetectionTutorialScreen( 520 | duration: widget.config.durationLivenessVerify ?? 45, 521 | isDarkMode: widget.config.isDarkMode, 522 | onStartTap: () { 523 | if (mounted) setState(() => _isInfoStepCompleted = true); 524 | _startLiveFeed(); 525 | }, 526 | ), 527 | ], 528 | ); 529 | } 530 | 531 | Widget _buildDetectionBody() { 532 | if (_cameraController == null || 533 | _cameraController?.value.isInitialized == false) { 534 | return const Center(child: CircularProgressIndicator.adaptive()); 535 | } 536 | 537 | return Stack( 538 | children: [ 539 | Container( 540 | height: MediaQuery.of(context).size.height, 541 | width: MediaQuery.of(context).size.width, 542 | color: widget.config.isDarkMode ? Colors.black : Colors.white, 543 | ), 544 | LivenessDetectionStepOverlayWidget( 545 | cameraController: _cameraController, 546 | duration: widget.config.durationLivenessVerify, 547 | showDurationUiText: widget.config.showDurationUiText, 548 | isDarkMode: widget.config.isDarkMode, 549 | isFaceDetected: _faceDetectedState, 550 | camera: CameraPreview(_cameraController!), 551 | key: _stepsKey, 552 | steps: _getStepsToUse(), 553 | showCurrentStep: widget.config.showCurrentStep, 554 | onCompleted: () => Future.delayed( 555 | const Duration(milliseconds: 500), 556 | () => _takePicture(), 557 | ), 558 | ), 559 | ], 560 | ); 561 | } 562 | 563 | Future _handlingBlinkStep({ 564 | required Face face, 565 | required LivenessDetectionStep step, 566 | }) async { 567 | final blinkThreshold = 568 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 569 | .firstWhereOrNull((p0) => p0 is LivenessThresholdBlink) 570 | as LivenessThresholdBlink?; 571 | 572 | if ((face.leftEyeOpenProbability ?? 1.0) < 573 | (blinkThreshold?.leftEyeProbability ?? 0.25) && 574 | (face.rightEyeOpenProbability ?? 1.0) < 575 | (blinkThreshold?.rightEyeProbability ?? 0.25)) { 576 | _startProcessing(); 577 | await _completeStep(step: step); 578 | } 579 | } 580 | 581 | Future _handlingTurnRight({ 582 | required Face face, 583 | required LivenessDetectionStep step, 584 | }) async { 585 | if (Platform.isAndroid) { 586 | final headTurnThreshold = 587 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 588 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 589 | as LivenessThresholdHead?; 590 | if ((face.headEulerAngleY ?? 0) < 591 | (headTurnThreshold?.rotationAngle ?? -30)) { 592 | _startProcessing(); 593 | await _completeStep(step: step); 594 | } 595 | } else if (Platform.isIOS) { 596 | final headTurnThreshold = 597 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 598 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 599 | as LivenessThresholdHead?; 600 | if ((face.headEulerAngleY ?? 0) > 601 | (headTurnThreshold?.rotationAngle ?? 30)) { 602 | _startProcessing(); 603 | await _completeStep(step: step); 604 | } 605 | } 606 | } 607 | 608 | Future _handlingTurnLeft({ 609 | required Face face, 610 | required LivenessDetectionStep step, 611 | }) async { 612 | if (Platform.isAndroid) { 613 | final headTurnThreshold = 614 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 615 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 616 | as LivenessThresholdHead?; 617 | if ((face.headEulerAngleY ?? 0) > 618 | (headTurnThreshold?.rotationAngle ?? 30)) { 619 | _startProcessing(); 620 | await _completeStep(step: step); 621 | } 622 | } else if (Platform.isIOS) { 623 | final headTurnThreshold = 624 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 625 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 626 | as LivenessThresholdHead?; 627 | if ((face.headEulerAngleY ?? 0) < 628 | (headTurnThreshold?.rotationAngle ?? -30)) { 629 | _startProcessing(); 630 | await _completeStep(step: step); 631 | } 632 | } 633 | } 634 | 635 | Future _handlingLookUp({ 636 | required Face face, 637 | required LivenessDetectionStep step, 638 | }) async { 639 | final headTurnThreshold = 640 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 641 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 642 | as LivenessThresholdHead?; 643 | if ((face.headEulerAngleX ?? 0) > 644 | (headTurnThreshold?.rotationAngle ?? 20)) { 645 | _startProcessing(); 646 | await _completeStep(step: step); 647 | } 648 | } 649 | 650 | Future _handlingLookDown({ 651 | required Face face, 652 | required LivenessDetectionStep step, 653 | }) async { 654 | final headTurnThreshold = 655 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 656 | .firstWhereOrNull((p0) => p0 is LivenessThresholdHead) 657 | as LivenessThresholdHead?; 658 | if ((face.headEulerAngleX ?? 0) < 659 | (headTurnThreshold?.rotationAngle ?? -15)) { 660 | _startProcessing(); 661 | await _completeStep(step: step); 662 | } 663 | } 664 | 665 | Future _handlingSmile({ 666 | required Face face, 667 | required LivenessDetectionStep step, 668 | }) async { 669 | final smileThreshold = 670 | FlutterLivenessDetectionRandomizedPlugin.instance.thresholdConfig 671 | .firstWhereOrNull((p0) => p0 is LivenessThresholdSmile) 672 | as LivenessThresholdSmile?; 673 | 674 | if ((face.smilingProbability ?? 0) > 675 | (smileThreshold?.probability ?? 0.65)) { 676 | _startProcessing(); 677 | await _completeStep(step: step); 678 | } 679 | } 680 | } 681 | --------------------------------------------------------------------------------