├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── EkycIdFlutterPlugin.h │ ├── EkycIdFlutterPlugin.m │ ├── SwiftEkycIdFlutterPlugin.swift │ ├── FaceScanner │ │ ├── FaceScannerViewFactory.swift │ │ └── FlutterFaceScanner.swift │ ├── DocumentScanner │ │ ├── DocumentScannerViewFactory.swift │ │ └── FlutterDocumentScanner.swift │ ├── LivenessDetection │ │ └── LivenessDetectionViewFactory.swift │ └── Core │ │ ├── FlutterFaceDetection.swift │ │ └── FlutterDocumentDetection.swift ├── .gitignore └── ekyc_id_flutter.podspec ├── android ├── .idea │ ├── .name │ ├── .gitignore │ ├── compiler.xml │ ├── kotlinc.xml │ ├── vcs.xml │ ├── migrations.xml │ ├── misc.xml │ ├── gradle.xml │ ├── modules.xml │ └── jarRepositories.xml ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── layout │ │ │ ├── document_scanner_viewfinder.xml │ │ │ ├── face_scanner_viewfinder.xml │ │ │ └── liveness_detection_viewfinder.xml │ │ └── kotlin │ │ └── com │ │ └── ekycsolutions │ │ └── ekyc_id_flutter │ │ ├── FaceScanner │ │ ├── FaceScannerViewFactory.kt │ │ └── FlutterFaceScanner.kt │ │ ├── DocumentScanner │ │ └── DocumentScannerViewFactory.kt │ │ ├── LivenessDetection │ │ └── LivenessDetectionViewFactory.kt │ │ └── EkycIdFlutterPlugin.kt ├── build.gradle ├── gradlew.bat └── gradlew ├── .tool-versions ├── example ├── .tool-versions ├── .fvm │ ├── flutter_sdk │ └── fvm_config.json ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── Podfile │ └── Podfile.lock ├── android │ ├── 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 │ │ │ │ │ │ └── ekycsolutions │ │ │ │ │ │ └── ekyc_id_flutter_example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── pubspec.yaml └── lib │ └── main.dart ├── .fvm ├── flutter_sdk └── fvm_config.json ├── LICENSE ├── .gitignore ├── lib ├── assets │ ├── blink_en.mp3 │ ├── blink_kh.mp3 │ ├── look_left_en.mp3 │ ├── look_left_kh.mp3 │ ├── look_right_en.mp3 │ ├── look_right_kh.mp3 │ ├── scan_back_en.mp3 │ ├── scan_back_kh.mp3 │ ├── scan_front_en.mp3 │ └── scan_front_kh.mp3 ├── src │ ├── models │ │ ├── language.dart │ │ ├── vehicle_registration.dart │ │ ├── api_result.dart │ │ ├── driver_license.dart │ │ └── national_id.dart │ ├── core │ │ ├── models │ │ │ ├── object_detection_object_group.dart │ │ │ ├── object_detection_object_type.dart │ │ │ └── frame_status.dart │ │ ├── face_detection.dart │ │ └── document_detection.dart │ ├── face_scanner │ │ ├── face_scanner_values.dart │ │ ├── face_scanner.dart │ │ ├── face_scanner_controller.dart │ │ └── face_scanner_view.dart │ ├── document_scanner │ │ ├── document_scanner.dart │ │ ├── document_scanner_controller.dart │ │ ├── document_scanner_values.dart │ │ └── document_scanner_view.dart │ └── liveness_detection │ │ ├── liveness_detection.dart │ │ ├── liveness_detection_controller.dart │ │ ├── liveness_detection_view.dart │ │ └── liveness_detection_values.dart └── ekyc_id_flutter.dart ├── doc └── build_settings_01.png ├── .idea ├── vcs.xml ├── runConfigurations │ └── example_lib_main_dart.xml ├── modules.xml ├── libraries │ ├── Flutter_Plugins.xml │ └── Dart_SDK.xml └── workspace.xml ├── .vscode └── settings.json ├── CHANGELOG.md ├── .metadata ├── publish.sh ├── pubspec.yaml └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/.idea/.name: -------------------------------------------------------------------------------- 1 | ekyc_id_flutter -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | flutter 3.16.8-stable 2 | -------------------------------------------------------------------------------- /example/.tool-versions: -------------------------------------------------------------------------------- 1 | flutter 3.16.8-stable 2 | -------------------------------------------------------------------------------- /.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | C:/Users/Raksa/fvm/versions/3.3.10 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | © 2022 EKYC Solutions Co, Ltd. All rights reserved. -------------------------------------------------------------------------------- /example/.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | C:/Users/Raksa/fvm/versions/2.2.1 -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ekyc_id_flutter' -------------------------------------------------------------------------------- /android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.3.10", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "2.2.1", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /lib/assets/blink_en.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/blink_en.mp3 -------------------------------------------------------------------------------- /lib/assets/blink_kh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/blink_kh.mp3 -------------------------------------------------------------------------------- /doc/build_settings_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/doc/build_settings_01.png -------------------------------------------------------------------------------- /lib/src/models/language.dart: -------------------------------------------------------------------------------- 1 | /// Enum indicating the language used. 2 | enum Language { 3 | EN, 4 | KH, 5 | } 6 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4608m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /lib/assets/look_left_en.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/look_left_en.mp3 -------------------------------------------------------------------------------- /lib/assets/look_left_kh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/look_left_kh.mp3 -------------------------------------------------------------------------------- /lib/assets/look_right_en.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/look_right_en.mp3 -------------------------------------------------------------------------------- /lib/assets/look_right_kh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/look_right_kh.mp3 -------------------------------------------------------------------------------- /lib/assets/scan_back_en.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/scan_back_en.mp3 -------------------------------------------------------------------------------- /lib/assets/scan_back_kh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/scan_back_kh.mp3 -------------------------------------------------------------------------------- /lib/assets/scan_front_en.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/scan_front_en.mp3 -------------------------------------------------------------------------------- /lib/assets/scan_front_kh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/lib/assets/scan_front_kh.mp3 -------------------------------------------------------------------------------- /ios/Classes/EkycIdFlutterPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface EkycIdFlutterPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /android/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EKYCSolutions/ekyc-id-flutter/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/EKYCSolutions/ekyc-id-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx5120M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/flutter_sdk", 3 | "search.exclude": { 4 | "**/.fvm": true 5 | }, 6 | // Remove from file watching 7 | "files.watcherExclude": { 8 | "**/.fvm": true 9 | } 10 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 26 15:44:02 ICT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 24 15:51:23 ICT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 1.0.26 3 | - Upgrade android sdk 4 | ## 1.0.20 5 | - Bug fixes 6 | ## 1.0.19 7 | - Raise gradle build tool, compile sdk and min sdk version 8 | ## 1.0.18 9 | - Fix android camera view overlap on viewfinder ui 10 | ## 1.0.16 11 | - Fix memory leak issue. -------------------------------------------------------------------------------- /android/src/main/res/layout/document_scanner_viewfinder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/res/layout/face_scanner_viewfinder.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/res/layout/liveness_detection_viewfinder.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.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: 02c026b03cd31dd3f867e5faeb7e104cce174c5f 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/.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: 02c026b03cd31dd3f867e5faeb7e104cce174c5f 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /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/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @UIApplicationMain 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 | -------------------------------------------------------------------------------- /android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | # set environment variables 2 | export VERSION="$1" &&\ 3 | export CWD="$(pwd)" &&\ 4 | export PUBSPEC="pubspec.yaml" 5 | 6 | # update version in .podspec file 7 | sed "s#\(.*version.*\):.*#\1: ${VERSION}#" $PUBSPEC > $PUBSPEC.bak &&\ 8 | mv $PUBSPEC.bak $PUBSPEC &&\ 9 | 10 | # sync git changes 11 | git add . &&\ 12 | git commit -m "released version ${VERSION}" | true &&\ 13 | git tag "${VERSION}" | true &&\ 14 | git push --tags | true &&\ 15 | git push origin main | true &&\ 16 | 17 | # publish 18 | flutter packages pub publish -------------------------------------------------------------------------------- /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/.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 -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/core/models/object_detection_object_group.dart: -------------------------------------------------------------------------------- 1 | enum ObjectDetectionObjectGroup { 2 | COVID_19_VACCINATION_CARD, 3 | DRIVER_LICENSE, 4 | DRIVER_LICENSE_FRONT, 5 | DRIVER_LICENSE_BACK, 6 | LICENSE_PLATE, 7 | NATIONAL_ID, 8 | NATIONAL_ID_FRONT, 9 | NATIONAL_ID_BACK, 10 | VEHICLE_REGISTRATION, 11 | VEHICLE_REGISTRATION_FRONT, 12 | VEHICLE_REGISTRATION_BACK, 13 | PASSPORT, 14 | PASSPORT_TOP, 15 | PASSPORT_BOTTOM, 16 | OTHERS, 17 | } 18 | 19 | extension ObjectDetectionObjectGroupToString on ObjectDetectionObjectGroup { 20 | String toShortString() { 21 | return this.toString().split('.').last; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ekyc_id_flutter_example 2 | 3 | Demonstrates how to use the ekyc_id_flutter 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://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | 13 | //include ':ekyc-id' 14 | //project(':ekyc-id').projectDir = new File('/Users/socretlee/CodingDrive/ekyc/ekyc-id-android/ekyc-id') -------------------------------------------------------------------------------- /ios/Classes/EkycIdFlutterPlugin.m: -------------------------------------------------------------------------------- 1 | #import "EkycIdFlutterPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "ekyc_id_flutter-Swift.h" 9 | #endif 10 | 11 | @implementation EkycIdFlutterPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftEkycIdFlutterPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/SwiftEkycIdFlutterPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftEkycIdFlutterPlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | registrar.register(DocumentScannerViewFactory(messenger: registrar.messenger()), withId: "DocumentScanner") 7 | registrar.register(LivenessDetectionViewFactory(messenger: registrar.messenger()), withId: "LivenessDetection") 8 | registrar.register(FaceScannerViewFactory(messenger: registrar.messenger()), withId: "FaceScanner") 9 | // 10 | FlutterFaceDetection.register(with: registrar) 11 | FlutterDocumentDetection.register(with: registrar) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter/FaceScanner/FaceScannerViewFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter.FaceScanner 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.StandardMessageCodec 6 | import io.flutter.plugin.platform.PlatformView 7 | import io.flutter.plugin.platform.PlatformViewFactory 8 | 9 | 10 | class FaceScannerViewFactory( 11 | private var binding: FlutterPlugin.FlutterPluginBinding, 12 | private var context: Context 13 | ): PlatformViewFactory(StandardMessageCodec.INSTANCE) { 14 | 15 | override fun create(_s: Context?, viewId: Int, args: Any?): PlatformView { 16 | return FlutterFaceScanner(binding, context, viewId) 17 | } 18 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter/DocumentScanner/DocumentScannerViewFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter.DocumentScanner 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.StandardMessageCodec 6 | import io.flutter.plugin.platform.PlatformView 7 | import io.flutter.plugin.platform.PlatformViewFactory 8 | 9 | 10 | class DocumentScannerViewFactory( 11 | private var binding: FlutterPlugin.FlutterPluginBinding, 12 | private var context: Context 13 | ): PlatformViewFactory(StandardMessageCodec.INSTANCE) { 14 | 15 | override fun create(_s: Context?, viewId: Int, args: Any?): PlatformView { 16 | return FlutterDocumentScanner(binding, context, viewId) 17 | } 18 | } -------------------------------------------------------------------------------- /android/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter/LivenessDetection/LivenessDetectionViewFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter.LivenessDetection 2 | 3 | import com.ekycsolutions.ekyc_id_flutter.DocumentScanner.FlutterDocumentScanner 4 | 5 | import android.content.Context 6 | import io.flutter.embedding.engine.plugins.FlutterPlugin 7 | import io.flutter.plugin.common.StandardMessageCodec 8 | import io.flutter.plugin.platform.PlatformView 9 | import io.flutter.plugin.platform.PlatformViewFactory 10 | 11 | 12 | class LivenessDetectionViewFactory( 13 | private var binding: FlutterPlugin.FlutterPluginBinding, 14 | private var context: Context 15 | ): PlatformViewFactory(StandardMessageCodec.INSTANCE) { 16 | 17 | override fun create(_s: Context?, viewId: Int, args: Any?): PlatformView { 18 | return FlutterLivenessDetection(binding, context, viewId) 19 | } 20 | } -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /lib/src/models/vehicle_registration.dart: -------------------------------------------------------------------------------- 1 | class VehicleRegistration { 2 | String? ADDRESS; 3 | String? BRAND; 4 | String? COLOR; 5 | String? ISSUE_DATE; 6 | String? NAME_ENG; 7 | String? NAME_KHM; 8 | String? PLATE_NUMBER; 9 | String? TYPE; 10 | String? VEHICLE_TYPE; 11 | 12 | VehicleRegistration({ 13 | this.ADDRESS, 14 | this.BRAND, 15 | this.COLOR, 16 | this.ISSUE_DATE, 17 | this.NAME_ENG, 18 | this.NAME_KHM, 19 | this.PLATE_NUMBER, 20 | this.TYPE, 21 | this.VEHICLE_TYPE, 22 | }); 23 | 24 | VehicleRegistration.fromJson(Map json) { 25 | this.ADDRESS = json["ADDRESS"]; 26 | this.BRAND = json["BRAND"]; 27 | this.COLOR = json["COLOR"]; 28 | this.ISSUE_DATE = json["ISSUE_DATE"]; 29 | this.NAME_ENG = json["NAME_ENG"]; 30 | this.NAME_KHM = json["NAME_KHM"]; 31 | this.PLATE_NUMBER = json["PLATE_NUMBER"]; 32 | this.TYPE = json["TYPE"]; 33 | this.VEHICLE_TYPE = json["VEHICLE_TYPE"]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ios/Classes/FaceScanner/FaceScannerViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FaceScannerViewFactory.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 3/16/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 10.0, *) 11 | public class FaceScannerViewFactory: NSObject, FlutterPlatformViewFactory { 12 | let messenger: FlutterBinaryMessenger 13 | 14 | init(messenger: FlutterBinaryMessenger) { 15 | self.messenger = messenger 16 | super.init() 17 | } 18 | 19 | public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { 20 | return FlutterFaceScanner(frame: frame, viewId: viewId, messenger: messenger, args: args) 21 | } 22 | 23 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 24 | return FlutterJSONMessageCodec() 25 | } 26 | 27 | public func applicationDidEnterBackground() {} 28 | 29 | public func applicationWillEnterForeground() {} 30 | } 31 | -------------------------------------------------------------------------------- /ios/Classes/DocumentScanner/DocumentScannerViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentScannerViewFactory.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 6/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 10.0, *) 11 | public class DocumentScannerViewFactory: NSObject, FlutterPlatformViewFactory { 12 | let messenger: FlutterBinaryMessenger 13 | 14 | init(messenger: FlutterBinaryMessenger) { 15 | self.messenger = messenger 16 | super.init() 17 | } 18 | 19 | public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { 20 | return FlutterDocumentScanner(frame: frame, viewId: viewId, messenger: messenger, args: args) 21 | } 22 | 23 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 24 | return FlutterJSONMessageCodec() 25 | } 26 | 27 | public func applicationDidEnterBackground() {} 28 | 29 | public func applicationWillEnterForeground() {} 30 | } 31 | -------------------------------------------------------------------------------- /ios/Classes/LivenessDetection/LivenessDetectionViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LivenessDetectionViewFactory.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 6/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 10.0, *) 11 | public class LivenessDetectionViewFactory: NSObject, FlutterPlatformViewFactory { 12 | let messenger: FlutterBinaryMessenger 13 | 14 | init(messenger: FlutterBinaryMessenger) { 15 | self.messenger = messenger 16 | super.init() 17 | } 18 | 19 | public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { 20 | return FlutterLivenessDetection(frame: frame, viewId: viewId, messenger: messenger, args: args) 21 | } 22 | 23 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 24 | return FlutterJSONMessageCodec() 25 | } 26 | 27 | public func applicationDidEnterBackground() {} 28 | 29 | public func applicationWillEnterForeground() {} 30 | } 31 | -------------------------------------------------------------------------------- /ios/ekyc_id_flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint ekyc_id_flutter.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'ekyc_id_flutter' 7 | s.version = '1.0.0' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'https://ekycsolutions.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Socret Lee' => 'ml-developer@ekycsolutions.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '11.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 | s.dependency "EkycID", "~> 2.0.8" 25 | end 26 | -------------------------------------------------------------------------------- /lib/src/core/face_detection.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import '../liveness_detection/liveness_detection_values.dart'; 4 | 5 | /// Class for controlling the document scanner functionalites. 6 | class FaceDetectionController { 7 | late MethodChannel _methodChannel; 8 | 9 | FaceDetectionController() { 10 | _methodChannel = new MethodChannel('FaceDetection_MethodChannel'); 11 | } 12 | 13 | Future> detect(Uint8List image) async { 14 | List result = await _methodChannel.invokeMethod('detect', image); 15 | return result 16 | .map((e) => LivenessFace.fromMap(Map.from(e))) 17 | .toList(); 18 | } 19 | 20 | /// Initializes the camera and starts the scanning process. 21 | /// 22 | Future initialize() async { 23 | await _methodChannel.invokeMethod('initialize'); 24 | } 25 | 26 | /// Stops the scanning process and dispose the camera object. 27 | Future dispose() async { 28 | await _methodChannel.invokeMethod('dispose'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/core/models/object_detection_object_type.dart: -------------------------------------------------------------------------------- 1 | enum ObjectDetectionObjectType { 2 | COVID_19_VACCINATION_CARD_0, 3 | COVID_19_VACCINATION_CARD_0_BACK, 4 | COVID_19_VACCINATION_CARD_1, 5 | COVID_19_VACCINATION_CARD_1_BACK, 6 | DRIVER_LICENSE_0, 7 | DRIVER_LICENSE_0_BACK, 8 | DRIVER_LICENSE_1, 9 | DRIVER_LICENSE_1_BACK, 10 | LICENSE_PLATE_0_0, 11 | LICENSE_PLATE_0_1, 12 | LICENSE_PLATE_1_0, 13 | LICENSE_PLATE_2_0, 14 | LICENSE_PLATE_3_0, 15 | LICENSE_PLATE_3_1, 16 | NATIONAL_ID_0, 17 | NATIONAL_ID_0_BACK, 18 | NATIONAL_ID_1, 19 | NATIONAL_ID_1_BACK, 20 | NATIONAL_ID_2, 21 | NATIONAL_ID_2_BACK, 22 | PASSPORT_0, 23 | PASSPORT_0_TOP, 24 | SUPERFIT_0, 25 | SUPERFIT_0_BACK, 26 | VEHICLE_REGISTRATION_0, 27 | VEHICLE_REGISTRATION_0_BACK, 28 | VEHICLE_REGISTRATION_1, 29 | VEHICLE_REGISTRATION_1_BACK, 30 | VEHICLE_REGISTRATION_2, 31 | } 32 | 33 | extension ObjectDetectionObjectTypeToString on ObjectDetectionObjectType { 34 | String toShortString() { 35 | return this.toString().split('.').last; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/ekyc_id_flutter.dart: -------------------------------------------------------------------------------- 1 | export 'src/core/face_detection.dart'; 2 | export 'src/core/document_detection.dart'; 3 | export 'src/core/models/frame_status.dart'; 4 | export 'src/core/models/object_detection_object_type.dart'; 5 | export 'src/core/models/object_detection_object_group.dart'; 6 | // 7 | export 'src/models/language.dart'; 8 | export 'src/models/national_id.dart'; 9 | // 10 | export 'src/document_scanner/document_scanner.dart'; 11 | export 'src/document_scanner/document_scanner_view.dart'; 12 | export 'src/document_scanner/document_scanner_values.dart'; 13 | export 'src/document_scanner/document_scanner_controller.dart'; 14 | // 15 | export 'src/face_scanner/face_scanner.dart'; 16 | export 'src/face_scanner/face_scanner_view.dart'; 17 | export 'src/face_scanner/face_scanner_values.dart'; 18 | export 'src/face_scanner/face_scanner_controller.dart'; 19 | // 20 | export 'src/liveness_detection/liveness_detection.dart'; 21 | export 'src/liveness_detection/liveness_detection_view.dart'; 22 | export 'src/liveness_detection/liveness_detection_values.dart'; 23 | export 'src/liveness_detection/liveness_detection_controller.dart'; 24 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.23' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.2.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | 20 | // This code is where all the magic happens and fixes the error. 21 | subprojects { 22 | afterEvaluate { project -> 23 | if (project.hasProperty('android')) { 24 | project.android { 25 | if (namespace == null) { 26 | namespace project.group 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | rootProject.buildDir = '../build' 35 | subprojects { 36 | project.buildDir = "${rootProject.buildDir}/${project.name}" 37 | project.evaluationDependsOn(':app') 38 | } 39 | 40 | tasks.register("clean", Delete) { 41 | delete rootProject.buildDir 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/core/document_detection.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'models/object_detection_object_type.dart'; 4 | import '../document_scanner/document_scanner_values.dart'; 5 | 6 | /// Class for controlling the document scanner functionalites. 7 | class DocumentDetectionController { 8 | late MethodChannel _methodChannel; 9 | 10 | DocumentDetectionController() { 11 | _methodChannel = new MethodChannel('DocumentDetection_MethodChannel'); 12 | } 13 | 14 | Future> detect(Uint8List image) async { 15 | List result = await _methodChannel.invokeMethod('detect', image); 16 | return result 17 | .map((e) => DocumentScannerResult.fromMap(Map.from(e))) 18 | .toList(); 19 | } 20 | 21 | Future initialize() async { 22 | await _methodChannel.invokeMethod('initialize'); 23 | } 24 | 25 | Future setWhiteList(List whiteList) async { 26 | await _methodChannel.invokeMethod( 27 | 'setWhiteList', whiteList.map((e) => e.toShortString()).toList()); 28 | } 29 | 30 | Future dispose() async { 31 | await _methodChannel.invokeMethod('dispose'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.ekycsolutions.ekyc_id_flutter' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.6.0' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.2.1' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven { url 'https://jitpack.io' } 22 | mavenLocal() 23 | } 24 | } 25 | 26 | apply plugin: 'com.android.library' 27 | apply plugin: 'kotlin-android' 28 | 29 | android { 30 | compileSdkVersion 33 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | minSdkVersion 21 47 | } 48 | } 49 | 50 | dependencies { 51 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 52 | api 'com.github.EKYCSolutions:ekyc-id-android:1.0.33' 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/face_scanner/face_scanner_values.dart: -------------------------------------------------------------------------------- 1 | class FaceScannerCameraOptions { 2 | final double roiSize; 3 | final double faceCropScale; 4 | final int captureDurationCountDown; 5 | final double minFaceWidthPercentage; 6 | final double maxFaceWidthPercentage; 7 | 8 | const FaceScannerCameraOptions({ 9 | this.roiSize = 250, 10 | this.faceCropScale = 1.4, 11 | this.maxFaceWidthPercentage = 1, 12 | this.minFaceWidthPercentage = 0.7, 13 | this.captureDurationCountDown = 3, 14 | }); 15 | 16 | Map toMap() { 17 | return { 18 | "roiSize": roiSize, 19 | "faceCropScale": faceCropScale, 20 | "maxFaceWidthPercentage": maxFaceWidthPercentage, 21 | "minFaceWidthPercentage": minFaceWidthPercentage, 22 | "captureDurationCountDown": captureDurationCountDown, 23 | }; 24 | } 25 | } 26 | 27 | /// Class representing configurations of the [DocumentScanner]. 28 | class FaceScannerOptions { 29 | final bool useFrontCamera; 30 | final FaceScannerCameraOptions cameraOptions; 31 | 32 | const FaceScannerOptions({ 33 | this.useFrontCamera = false, 34 | this.cameraOptions = const FaceScannerCameraOptions(), 35 | }); 36 | 37 | Map toMap() { 38 | return { 39 | "useFrontCamera": useFrontCamera, 40 | "cameraOptions": cameraOptions.toMap(), 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ekyc_id_flutter 2 | description: A Flutter Plugin to interact with EkycID. 3 | version: 1.0.34 4 | homepage: https://ekycsolutions.com 5 | repository: https://github.com/EKYCSolutions/ekyc-id-flutter 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | flutter: ">=2.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | dio: ^5.4.1 15 | vibration: ^1.7.3 16 | just_audio: ^0.9.24 17 | http_parser: ^4.0.1 18 | flutter_beep: ^1.0.0 19 | flutter_spinkit: ^5.1.0 20 | flutter_font_icons: ^2.2.4 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | 26 | flutter: 27 | assets: 28 | - packages/ekyc_id_flutter/assets/blink_en.mp3 29 | - packages/ekyc_id_flutter/assets/look_left_en.mp3 30 | - packages/ekyc_id_flutter/assets/look_right_en.mp3 31 | - packages/ekyc_id_flutter/assets/scan_back_en.mp3 32 | - packages/ekyc_id_flutter/assets/scan_front_en.mp3 33 | - packages/ekyc_id_flutter/assets/blink_kh.mp3 34 | - packages/ekyc_id_flutter/assets/look_left_kh.mp3 35 | - packages/ekyc_id_flutter/assets/look_right_kh.mp3 36 | - packages/ekyc_id_flutter/assets/scan_back_kh.mp3 37 | - packages/ekyc_id_flutter/assets/scan_front_kh.mp3 38 | plugin: 39 | platforms: 40 | android: 41 | package: com.ekycsolutions.ekyc_id_flutter 42 | pluginClass: EkycIdFlutterPlugin 43 | ios: 44 | pluginClass: EkycIdFlutterPlugin 45 | -------------------------------------------------------------------------------- /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! :linkage => :static 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /lib/src/core/models/frame_status.dart: -------------------------------------------------------------------------------- 1 | /// Enum indicating the status of a frame coming from the device's camera. 2 | enum FrameStatus { 3 | /// The camera is currently initializing. 4 | INITIALIZING, 5 | 6 | /// The camera is preparing to capture the frame. 7 | PREPARING, 8 | 9 | /// The camera is currently processing the frame. 10 | PROCESSING, 11 | 12 | /// There is no document found inside the frame. 13 | DOCUMENT_NOT_FOUND, 14 | 15 | /// There is a document found in the frame. 16 | DOCUMENT_FOUND, 17 | 18 | /// The size of the document is too large within the frame. 19 | DOCUMENT_TOO_BIG, 20 | 21 | /// The size of the document is too small within the frame. 22 | DOCUMENT_TOO_SMALL, 23 | 24 | /// The document is not in the center of frame. 25 | DOCUMENT_NOT_IN_CENTER, 26 | 27 | /// The processor cannot grab face from the document. 28 | CANNOT_GRAB_DOCUMENT, 29 | 30 | /// There is no face found inside the frame. 31 | NO_FACE_FOUND, 32 | 33 | /// There is a face found inside the frame. 34 | FACE_FOUND, 35 | 36 | /// The size of the face is too large within the frame. 37 | FACE_TOO_BIG, 38 | 39 | /// The size of the face is too small within the frame. 40 | FACE_TOO_SMALL, 41 | 42 | /// The face is not in the center of frame. 43 | FACE_NOT_IN_CENTER, 44 | 45 | /// There are multiple faces found inside the frame. 46 | MULTIPLE_FACES_FOUND, 47 | 48 | /// The processor cannot grab the face from the frame. 49 | CANNOT_GRAB_FACE, 50 | 51 | /// The face is blocked 52 | FACE_BLOCKAGE_FOUND, 53 | 54 | DOCUMENT_BLUR, 55 | } 56 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/models/api_result.dart: -------------------------------------------------------------------------------- 1 | /// Class representing the response from EkyID services. (e.g face-compare, ocr) 2 | class ApiResult { 3 | 4 | /// The response from EkycID service. Depending on API, the response can be face-compare score or document ocr response. 5 | Map? data; 6 | 7 | /// The error message from the api call. 8 | String? message; 9 | 10 | /// The errorCode returned from the server. 11 | String? errorCode; 12 | 13 | /// Boolean indicating if the api call was a success. 14 | bool? isSuccess; 15 | 16 | /// The end time of the api call. 17 | String? endTime; 18 | 19 | /// The start time of the api call. 20 | String? startTime; 21 | 22 | /// The duration of the api call. 23 | int? timeElapsedAsSec; 24 | 25 | ApiResult({ 26 | this.data, 27 | this.endTime, 28 | this.message, 29 | this.startTime, 30 | this.isSuccess, 31 | this.errorCode, 32 | this.timeElapsedAsSec, 33 | }); 34 | 35 | /// Creates an instance of ApiResult from a [json] response. 36 | ApiResult.fromJson(Map? json) { 37 | if (json != null) { 38 | this.data = 39 | json["data"] != null ? Map.from(json["data"]) : null; 40 | this.message = null; 41 | this.errorCode = json["errorCode"] ?? null; 42 | this.isSuccess = json["isSuccess"] ?? null; 43 | this.endTime = json["endTime"] ?? null; 44 | this.startTime = json["startTime"] ?? null; 45 | this.timeElapsedAsSec = json["timeElapsedAsSec"] ?? null; 46 | } else { 47 | this.data = null; 48 | this.message = null; 49 | this.errorCode = null; 50 | this.isSuccess = null; 51 | this.endTime = null; 52 | this.startTime = null; 53 | this.timeElapsedAsSec = null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /lib/src/models/driver_license.dart: -------------------------------------------------------------------------------- 1 | class DriverLicense { 2 | String? ADDRESS; 3 | String? ADDRESS_ENG; 4 | String? CARD_NUMBER; 5 | String? CATEGORIES; 6 | String? DATE_OF_BIRTH; 7 | String? ENGLISH_NAME; 8 | String? EXPIRY_DATE; 9 | String? ID_NUMBER; 10 | String? ISSUE_DATE; 11 | String? KHMER_NAME; 12 | String? NATIONALITY_ENG; 13 | String? NATIONALITY_KHM; 14 | String? PLACE_OF_BIRTH; 15 | String? PLACE_OF_BIRTH_KHM; 16 | String? SEX; 17 | String? SPECIAL_CONDITION_ENG; 18 | String? SPECIAL_CONDITION_KHM; 19 | 20 | DriverLicense({ 21 | this.ADDRESS, 22 | this.ADDRESS_ENG, 23 | this.CARD_NUMBER, 24 | this.CATEGORIES, 25 | this.DATE_OF_BIRTH, 26 | this.ENGLISH_NAME, 27 | this.EXPIRY_DATE, 28 | this.ID_NUMBER, 29 | this.ISSUE_DATE, 30 | this.KHMER_NAME, 31 | this.NATIONALITY_ENG, 32 | this.NATIONALITY_KHM, 33 | this.PLACE_OF_BIRTH, 34 | this.PLACE_OF_BIRTH_KHM, 35 | this.SEX, 36 | this.SPECIAL_CONDITION_ENG, 37 | this.SPECIAL_CONDITION_KHM, 38 | }); 39 | 40 | DriverLicense.fromJson(Map json) { 41 | this.ADDRESS = json["ADDRESS"]; 42 | this.ADDRESS_ENG = json["ADDRESS_ENG"]; 43 | this.CARD_NUMBER = json["CARD_NUMBER"]; 44 | this.CATEGORIES = json["CATEGORIES"]; 45 | this.DATE_OF_BIRTH = json["DATE_OF_BIRTH"]; 46 | this.ENGLISH_NAME = json["ENGLISH_NAME"]; 47 | this.EXPIRY_DATE = json["EXPIRY_DATE"]; 48 | this.ID_NUMBER = json["ID_NUMBER"]; 49 | this.ISSUE_DATE = json["ISSUE_DATE"]; 50 | this.KHMER_NAME = json["KHMER_NAME"]; 51 | this.NATIONALITY_ENG = json["NATIONALITY_ENG"]; 52 | this.NATIONALITY_KHM = json["NATIONALITY_KHM"]; 53 | this.PLACE_OF_BIRTH = json["PLACE_OF_BIRTH"]; 54 | this.PLACE_OF_BIRTH_KHM = json["PLACE_OF_BIRTH_KHM"]; 55 | this.SEX = json["SEX"]; 56 | this.SPECIAL_CONDITION_ENG = json["SPECIAL_CONDITION_ENG"]; 57 | this.SPECIAL_CONDITION_KHM = json["SPECIAL_CONDITION_KHM"]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ekyc_id_flutter_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | NSCameraUsageDescription 45 | Need Camera Access 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | NSPhotoLibraryUsageDescription 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 34 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | compileOptions { 36 | sourceCompatibility JavaVersion.VERSION_1_8 37 | targetCompatibility JavaVersion.VERSION_1_8 38 | } 39 | 40 | kotlinOptions { 41 | jvmTarget = '1.8' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.ekycsolutions.ekyc_id_flutter_example" 47 | minSdkVersion 21 48 | targetSdkVersion 33 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | namespace 'com.ekycsolutions.ekyc_id_flutter_example' 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 69 | } 70 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter/EkycIdFlutterPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter 2 | 3 | import androidx.annotation.NonNull 4 | import com.ekycsolutions.ekycid.EkycID 5 | import com.ekycsolutions.ekyc_id_flutter.DocumentScanner.DocumentScannerViewFactory 6 | import com.ekycsolutions.ekyc_id_flutter.FaceScanner.FaceScannerViewFactory 7 | import com.ekycsolutions.ekyc_id_flutter.LivenessDetection.LivenessDetectionViewFactory 8 | 9 | import io.flutter.embedding.engine.plugins.FlutterPlugin 10 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 11 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 12 | 13 | /** EkycIdFlutterPlugin */ 14 | class EkycIdFlutterPlugin: FlutterPlugin, ActivityAware { 15 | /// The MethodChannel that will the communication between Flutter and native Android 16 | /// 17 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 18 | /// when the Flutter Engine is detached from the Activity 19 | 20 | private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null 21 | 22 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 23 | this.flutterPluginBinding = flutterPluginBinding 24 | EkycID.initialize(flutterPluginBinding.applicationContext) 25 | } 26 | 27 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 28 | this.flutterPluginBinding = null 29 | } 30 | 31 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 32 | this.flutterPluginBinding!!.platformViewRegistry.registerViewFactory( 33 | "DocumentScanner", 34 | DocumentScannerViewFactory(this.flutterPluginBinding!!, binding.activity) 35 | ) 36 | this.flutterPluginBinding!!.platformViewRegistry.registerViewFactory( 37 | "FaceScanner", 38 | FaceScannerViewFactory(this.flutterPluginBinding!!, binding.activity) 39 | ) 40 | this.flutterPluginBinding!!.platformViewRegistry.registerViewFactory( 41 | "LivenessDetection", 42 | LivenessDetectionViewFactory(this.flutterPluginBinding!!, binding.activity) 43 | ) 44 | } 45 | 46 | 47 | override fun onDetachedFromActivityForConfigChanges() { 48 | onDetachedFromActivity() 49 | } 50 | 51 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 52 | onAttachedToActivity(binding); 53 | } 54 | 55 | override fun onDetachedFromActivity() { 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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/models/national_id.dart: -------------------------------------------------------------------------------- 1 | class NationalID { 2 | NationalIDEng? ENG; 3 | NationalIDKhm? KHM; 4 | 5 | NationalID({ 6 | this.ENG, 7 | this.KHM, 8 | }); 9 | 10 | NationalID.fromJson(Map json) { 11 | this.ENG = NationalIDEng.fromJson(json["ENG"]); 12 | this.KHM = NationalIDKhm.fromJson(json["KHM"]); 13 | } 14 | 15 | static String parsedDate(String? date) { 16 | if (date == null) { 17 | return "--"; 18 | } 19 | 20 | String day = date.substring(date.length - 2); 21 | String month = date.substring(date.length - 4, date.length - 2); 22 | String year = date.substring(0, date.length - 4); 23 | 24 | return "$day/$month/$year"; 25 | } 26 | } 27 | 28 | class NationalIDEng { 29 | String? FH; 30 | String? H1; 31 | String? H2; 32 | String? H3; 33 | String? doc; 34 | String? sex; 35 | String? country; 36 | String? fullName; 37 | String? lastName; 38 | String? birthDate; 39 | String? firstName; 40 | String? middleName; 41 | String? expiryDate; 42 | String? nationality; 43 | String? optionalData1; 44 | String? optionalData2; 45 | String? documentNumber; 46 | 47 | NationalIDEng.fromJson(Map json) { 48 | this.FH = json["FH"]; 49 | this.H1 = json["H1"]; 50 | this.H2 = json["H2"]; 51 | this.H3 = json["H3"]; 52 | this.sex = json["sex"]; 53 | this.doc = json["doc"]; 54 | this.country = json["country"]; 55 | this.fullName = json["full_name"]; 56 | this.lastName = json["last_name"]; 57 | this.birthDate = json["birth_date"]; 58 | this.firstName = json["first_name"]; 59 | this.expiryDate = json["expiry_date"]; 60 | this.middleName = json["middle_name"]; 61 | this.nationality = json["nationality"]; 62 | this.optionalData1 = json["optional_data_1"]; 63 | this.optionalData2 = json["optional_data_2"]; 64 | this.documentNumber = json["document_number"]; 65 | } 66 | } 67 | 68 | class NationalIDKhm { 69 | String? MRZ_ENG; 70 | String? NAME_KH; 71 | String? HEIGHT_KH; 72 | String? ADDRESS_KH; 73 | String? IDENTIFY_KH; 74 | String? ADDRESS_1_KH; 75 | String? ADDRESS_2_KH; 76 | String? VALIDITY_END_KH; 77 | String? VALIDITY_START_KH; 78 | 79 | NationalIDKhm.fromJson(Map json) { 80 | this.ADDRESS_1_KH = json["ADDRESS_1_KH"]; 81 | this.ADDRESS_2_KH = json["ADDRESS_2_KH"]; 82 | this.MRZ_ENG = json["MRZ_ENG"]; 83 | this.NAME_KH = json["NAME_KH"]; 84 | this.HEIGHT_KH = json["HEIGHT_KH"]; 85 | this.ADDRESS_KH = json["ADDRESS_KH"]; 86 | this.IDENTIFY_KH = json["IDENTIFY_KH"]; 87 | this.VALIDITY_END_KH = json["VALIDITY_END_KH"]; 88 | this.VALIDITY_START_KH = json["VALIDITY_START_KH"]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/face_scanner/face_scanner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'face_scanner_controller.dart'; 9 | 10 | /// Class representing the native DocumentScanner view 11 | class FaceScanner extends StatefulWidget { 12 | const FaceScanner({ 13 | Key? key, 14 | required this.onCreated, 15 | }) : super(key: key); 16 | 17 | final void Function(FaceScannerController) onCreated; 18 | 19 | @override 20 | State createState() => _FaceScannerState(); 21 | } 22 | 23 | class _FaceScannerState extends State { 24 | Future onPlatformViewCreated(id) async { 25 | widget.onCreated(FaceScannerController(id)); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | StandardMessageCodec decorder = const StandardMessageCodec(); 31 | 32 | if (Platform.isAndroid) { 33 | final Map creationParams = const {}; 34 | 35 | return PlatformViewLink( 36 | viewType: "FaceScanner", 37 | surfaceFactory: ( 38 | BuildContext context, 39 | PlatformViewController controller, 40 | ) { 41 | return AndroidViewSurface( 42 | controller: controller as AndroidViewController, 43 | gestureRecognizers: const >{}, 44 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 45 | ); 46 | }, 47 | onCreatePlatformView: (PlatformViewCreationParams params) { 48 | final ExpensiveAndroidViewController controller = 49 | PlatformViewsService.initExpensiveAndroidView( 50 | id: params.id, 51 | viewType: "FaceScanner", 52 | layoutDirection: TextDirection.ltr, 53 | creationParams: creationParams, 54 | creationParamsCodec: const StandardMessageCodec(), 55 | onFocus: () => params.onFocusChanged(true), 56 | ); 57 | controller 58 | ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) 59 | ..addOnPlatformViewCreatedListener(onPlatformViewCreated) 60 | ..create(); 61 | return controller; 62 | }, 63 | ); 64 | } 65 | 66 | if (Platform.isIOS) { 67 | return UiKitView( 68 | viewType: "FaceScanner", 69 | onPlatformViewCreated: onPlatformViewCreated, 70 | creationParamsCodec: decorder, 71 | ); 72 | } 73 | 74 | return Container( 75 | child: Text("Platform not Supported."), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /lib/src/document_scanner/document_scanner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'document_scanner_controller.dart'; 9 | 10 | /// Class representing the native DocumentScanner view 11 | class DocumentScanner extends StatefulWidget { 12 | const DocumentScanner({ 13 | Key? key, 14 | required this.onCreated, 15 | }) : super(key: key); 16 | 17 | final void Function(DocumentScannerController) onCreated; 18 | 19 | @override 20 | State createState() => _DocumentScannerState(); 21 | } 22 | 23 | class _DocumentScannerState extends State { 24 | Future onPlatformViewCreated(id) async { 25 | widget.onCreated(DocumentScannerController(id)); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | StandardMessageCodec decorder = const StandardMessageCodec(); 31 | 32 | if (Platform.isAndroid) { 33 | final Map creationParams = const {}; 34 | 35 | return PlatformViewLink( 36 | viewType: "DocumentScanner", 37 | surfaceFactory: ( 38 | BuildContext context, 39 | PlatformViewController controller, 40 | ) { 41 | return AndroidViewSurface( 42 | controller: controller as AndroidViewController, 43 | gestureRecognizers: const >{}, 44 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 45 | ); 46 | }, 47 | onCreatePlatformView: (PlatformViewCreationParams params) { 48 | final ExpensiveAndroidViewController controller = 49 | PlatformViewsService.initExpensiveAndroidView( 50 | id: params.id, 51 | viewType: "DocumentScanner", 52 | layoutDirection: TextDirection.ltr, 53 | creationParams: creationParams, 54 | creationParamsCodec: const StandardMessageCodec(), 55 | onFocus: () => params.onFocusChanged(true), 56 | ); 57 | controller 58 | ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) 59 | ..addOnPlatformViewCreatedListener(onPlatformViewCreated) 60 | ..create(); 61 | return controller; 62 | }, 63 | ); 64 | } 65 | 66 | if (Platform.isIOS) { 67 | return UiKitView( 68 | viewType: "DocumentScanner", 69 | onPlatformViewCreated: onPlatformViewCreated, 70 | creationParamsCodec: decorder, 71 | ); 72 | } 73 | 74 | return Container( 75 | child: Text("Platform not Supported."), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/liveness_detection/liveness_detection.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'liveness_detection_values.dart'; 9 | import 'liveness_detection_controller.dart'; 10 | 11 | /// Class representing the native LivenessDetection view 12 | class LivenessDetection extends StatefulWidget { 13 | const LivenessDetection({ 14 | Key? key, 15 | required this.onCreated, 16 | }) : super(key: key); 17 | 18 | final LivenessDetectionCreatedCallback onCreated; 19 | 20 | @override 21 | State createState() => _LivenessDetectionState(); 22 | } 23 | 24 | class _LivenessDetectionState extends State { 25 | Future onPlatformViewCreated(id) async { 26 | widget.onCreated(LivenessDetectionController(id)); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | StandardMessageCodec decorder = const StandardMessageCodec(); 32 | 33 | if (Platform.isAndroid) { 34 | final Map creationParams = const {}; 35 | 36 | return PlatformViewLink( 37 | viewType: "LivenessDetection", 38 | surfaceFactory: ( 39 | BuildContext context, 40 | PlatformViewController controller, 41 | ) { 42 | return AndroidViewSurface( 43 | controller: controller as AndroidViewController, 44 | gestureRecognizers: const >{}, 45 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 46 | ); 47 | }, 48 | onCreatePlatformView: (PlatformViewCreationParams params) { 49 | final ExpensiveAndroidViewController controller = 50 | PlatformViewsService.initExpensiveAndroidView( 51 | id: params.id, 52 | viewType: "LivenessDetection", 53 | layoutDirection: TextDirection.ltr, 54 | creationParams: creationParams, 55 | creationParamsCodec: const StandardMessageCodec(), 56 | onFocus: () => params.onFocusChanged(true), 57 | ); 58 | controller 59 | ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) 60 | ..addOnPlatformViewCreatedListener(onPlatformViewCreated) 61 | ..create(); 62 | return controller; 63 | }, 64 | ); 65 | } 66 | 67 | if (Platform.isIOS) { 68 | return UiKitView( 69 | viewType: "LivenessDetection", 70 | onPlatformViewCreated: onPlatformViewCreated, 71 | creationParamsCodec: decorder, 72 | ); 73 | } 74 | 75 | return Container( 76 | child: Text("Platform not Supported."), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ekyc_id_flutter_example 2 | description: Demonstrates how to use the ekyc_id_flutter plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.0 <4.0.0" 10 | 11 | dependencies: 12 | modal_bottom_sheet: 2.0.1 13 | flutter: 14 | sdk: flutter 15 | 16 | ekyc_id_flutter: 17 | # When depending on this package from a real application you should use: 18 | # ekyc_id_flutter: ^x.y.z 19 | # See https://dart.dev/tools/pub/dependencies#version-constraints 20 | # The example app is bundled with the plugin so we use a path dependency on 21 | # the parent directory to use the current plugin's version. 22 | path: ../ 23 | 24 | # The following adds the Cupertino Icons font to your application. 25 | # Use with the CupertinoIcons class for iOS style icons. 26 | image_picker: ^1.0.7 27 | cupertino_icons: ^1.0.2 28 | permission_handler: ^10.2.0 29 | json_view: ^0.4.2 30 | 31 | dev_dependencies: 32 | flutter_test: 33 | sdk: flutter 34 | 35 | # For information on the generic Dart part of this file, see the 36 | # following page: https://dart.dev/tools/pub/pubspec 37 | 38 | # The following section is specific to Flutter. 39 | flutter: 40 | # The following line ensures that the Material Icons font is 41 | # included with your application, so that you can use the icons in 42 | # the material Icons class. 43 | uses-material-design: true 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /lib/src/face_scanner/face_scanner_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import '../core/models/frame_status.dart'; 4 | import '../liveness_detection/liveness_detection_values.dart'; 5 | import 'face_scanner_values.dart'; 6 | 7 | /// Class for controlling the document scanner functionalites. 8 | class FaceScannerController { 9 | late MethodChannel _methodChannel; 10 | late EventChannel _eventChannel; 11 | 12 | FaceScannerController(int id) { 13 | _methodChannel = new MethodChannel('FaceScanner_MethodChannel_$id'); 14 | _eventChannel = new EventChannel('FaceScanner_EventChannel_$id'); 15 | } 16 | 17 | /// Initializes the camera and starts the scanning process. 18 | /// 19 | Future start({ 20 | required void Function() onInitialized, 21 | required void Function(FrameStatus) onFrameStatusChanged, 22 | required Future Function(LivenessFace) onFaceScanned, 23 | required void Function(int, int) onCaptureCountDownChanged, 24 | required FaceScannerOptions options, 25 | }) async { 26 | await _methodChannel.invokeMethod('start', options.toMap()); 27 | _registerEventListener( 28 | onInitialized: onInitialized, 29 | onFaceScanned: onFaceScanned, 30 | onFrameStatusChanged: onFrameStatusChanged, 31 | onCaptureCountDownChanged: onCaptureCountDownChanged, 32 | ); 33 | } 34 | 35 | /// Stops the scanning process and dispose the camera object. 36 | Future dispose() async { 37 | await _methodChannel.invokeMethod('dispose'); 38 | } 39 | 40 | /// Allows the camera to start processing the next frame. 41 | Future nextImage() async { 42 | await _methodChannel.invokeMethod('nextImage'); 43 | } 44 | 45 | void _registerEventListener({ 46 | required void Function() onInitialized, 47 | required void Function(FrameStatus) onFrameStatusChanged, 48 | required Future Function(LivenessFace) onFaceScanned, 49 | required void Function(int, int) onCaptureCountDownChanged, 50 | }) { 51 | _eventChannel.receiveBroadcastStream().listen((event) async { 52 | if (event["type"] == "onFaceScanned") { 53 | Map values = 54 | Map.from(event["values"]); 55 | LivenessFace face = LivenessFace.fromMap(values); 56 | onFaceScanned(face); 57 | return; 58 | } 59 | 60 | if (event["type"] == "onInitialized") { 61 | onInitialized(); 62 | return; 63 | } 64 | 65 | if (event["type"] == "onFrameStatusChanged") { 66 | FrameStatus frameStatus = FrameStatus.values.firstWhere( 67 | (e) => e.toString() == "FrameStatus.${event['values']}"); 68 | onFrameStatusChanged(frameStatus); 69 | return; 70 | } 71 | 72 | if (event["type"] == "onCaptureCountDownChanged") { 73 | onCaptureCountDownChanged( 74 | event["values"]["current"], 75 | event["values"]["max"], 76 | ); 77 | return; 78 | } 79 | }); 80 | } 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/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/src/document_scanner/document_scanner_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import '../core/models/frame_status.dart'; 4 | import 'document_scanner_values.dart'; 5 | 6 | /// Class for controlling the document scanner functionalites. 7 | class DocumentScannerController { 8 | late MethodChannel _methodChannel; 9 | late EventChannel _eventChannel; 10 | 11 | DocumentScannerController(int id) { 12 | _methodChannel = new MethodChannel('DocumentScanner_MethodChannel_$id'); 13 | _eventChannel = new EventChannel('DocumentScanner_EventChannel_$id'); 14 | } 15 | 16 | /// Initializes the camera and starts the scanning process. 17 | /// 18 | Future start({ 19 | required void Function() onInitialized, 20 | required void Function(DocumentSide) onCurrentSideChanged, 21 | required void Function(FrameStatus) onFrameStatusChanged, 22 | required void Function(int, int) onCaptureCountDownChanged, 23 | required Future Function( 24 | DocumentScannerResult, DocumentScannerResult?) 25 | onDocumentScanned, 26 | required DocumentScannerOptions options, 27 | }) async { 28 | await _methodChannel.invokeMethod('start', options.toMap()); 29 | _registerEventListener( 30 | onInitialized: onInitialized, 31 | onDocumentScanned: onDocumentScanned, 32 | onFrameStatusChanged: onFrameStatusChanged, 33 | onCurrentSideChanged: onCurrentSideChanged, 34 | onCaptureCountDownChanged: onCaptureCountDownChanged); 35 | } 36 | 37 | /// Stops the scanning process and dispose the camera object. 38 | Future dispose() async { 39 | await _methodChannel.invokeMethod('dispose'); 40 | } 41 | 42 | /// Allows the camera to start processing the next frame. 43 | Future nextImage() async { 44 | await _methodChannel.invokeMethod('nextImage'); 45 | } 46 | 47 | Future reset() async { 48 | await _methodChannel.invokeMethod('reset'); 49 | } 50 | 51 | void _registerEventListener({ 52 | required void Function() onInitialized, 53 | required void Function(DocumentSide) onCurrentSideChanged, 54 | required void Function(FrameStatus) onFrameStatusChanged, 55 | required void Function(int, int) onCaptureCountDownChanged, 56 | required Future Function( 57 | DocumentScannerResult, DocumentScannerResult?) 58 | onDocumentScanned, 59 | }) { 60 | _eventChannel.receiveBroadcastStream().listen((event) async { 61 | if (event["type"] == "onDocumentScanned") { 62 | DocumentScannerResult mainSide = DocumentScannerResult.fromMap( 63 | Map.from(event["values"]["mainSide"])); 64 | DocumentScannerResult? secondarySide = 65 | event["values"]["secondarySide"] != null 66 | ? DocumentScannerResult.fromMap( 67 | Map.from(event["values"]["secondarySide"])) 68 | : null; 69 | onDocumentScanned(mainSide, secondarySide); 70 | return; 71 | } 72 | 73 | if (event["type"] == "onInitialized") { 74 | onInitialized(); 75 | return; 76 | } 77 | 78 | if (event["type"] == "onCurrentSideChanged") { 79 | DocumentSide side = DocumentSide.values.firstWhere( 80 | (e) => e.toString() == "DocumentSide.${event['values']}"); 81 | onCurrentSideChanged(side); 82 | return; 83 | } 84 | 85 | if (event["type"] == "onFrameStatusChanged") { 86 | FrameStatus frameStatus = FrameStatus.values.firstWhere( 87 | (e) => e.toString() == "FrameStatus.${event['values']}"); 88 | onFrameStatusChanged(frameStatus); 89 | return; 90 | } 91 | 92 | if (event["type"] == "onCaptureCountDownChanged") { 93 | onCaptureCountDownChanged( 94 | event["values"]["current"], 95 | event["values"]["max"], 96 | ); 97 | return; 98 | } 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/document_scanner/document_scanner_values.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import '../core/models/object_detection_object_group.dart'; 4 | import '../core/models/object_detection_object_type.dart'; 5 | 6 | /// Enum indicating the side of the document. 7 | enum DocumentSide { MAIN, SECONDARY } 8 | 9 | /// Enum indicating the supported document type of of the document. 10 | enum DocumentScannerDocType { NATIONAL_ID, DRIVER_LICENSE } 11 | 12 | /// Class representing the result of the document scanning process. 13 | class DocumentScannerResult { 14 | /// The type of document detected. 15 | late ObjectDetectionObjectType documentType; 16 | 17 | /// The group that the document belongs to. 18 | late ObjectDetectionObjectGroup documentGroup; 19 | 20 | /// The image of the frame when the document is detected. 21 | late Uint8List fullImage; 22 | 23 | /// The warped image of the detected document. 24 | late Uint8List documentImage; 25 | 26 | /// The face image existing in the document (If document has a face). 27 | late Uint8List? faceImage; 28 | 29 | DocumentScannerResult({ 30 | required this.documentType, 31 | required this.documentGroup, 32 | required this.fullImage, 33 | required this.documentImage, 34 | this.faceImage, 35 | }); 36 | 37 | /// Creates an instance of DocumentScannerResult from a [json] response. 38 | DocumentScannerResult.fromMap(Map json) { 39 | ObjectDetectionObjectGroup documentGroup = ObjectDetectionObjectGroup.values 40 | .firstWhere((e) => 41 | e.toString() == 42 | "ObjectDetectionObjectGroup.${json['documentGroup']}"); 43 | this.documentGroup = documentGroup; 44 | 45 | ObjectDetectionObjectType documentType = ObjectDetectionObjectType.values 46 | .firstWhere((e) => 47 | e.toString() == 48 | "ObjectDetectionObjectType.${json['documentType']}"); 49 | this.documentType = documentType; 50 | 51 | this.fullImage = Uint8List.fromList(json["fullImage"]); 52 | this.documentImage = Uint8List.fromList(json["documentImage"]); 53 | 54 | if (json["faceImage"] != null) { 55 | this.faceImage = Uint8List.fromList(json["faceImage"]); 56 | } 57 | } 58 | } 59 | 60 | class DocumentScannerCameraOptions { 61 | final double roiSize; 62 | final double faceCropScale; 63 | final int captureDurationCountDown; 64 | final double minDocWidthPercentage; 65 | final double maxDocWidthPercentage; 66 | 67 | const DocumentScannerCameraOptions({ 68 | this.roiSize = 100, 69 | this.faceCropScale = 1.4, 70 | this.maxDocWidthPercentage = 1, 71 | this.minDocWidthPercentage = 0.7, 72 | this.captureDurationCountDown = 3, 73 | }); 74 | 75 | Map toMap() { 76 | return { 77 | "roiSize": roiSize, 78 | "faceCropScale": faceCropScale, 79 | "maxDocWidthPercentage": maxDocWidthPercentage, 80 | "minDocWidthPercentage": minDocWidthPercentage, 81 | "captureDurationCountDown": captureDurationCountDown, 82 | }; 83 | } 84 | } 85 | 86 | class ScannableDocument { 87 | final ObjectDetectionObjectType mainSide; 88 | final ObjectDetectionObjectType? secondarySide; 89 | 90 | const ScannableDocument({ 91 | required this.mainSide, 92 | this.secondarySide, 93 | }); 94 | 95 | Map toMap() { 96 | return { 97 | "mainSide": this.mainSide.toShortString(), 98 | "secondarySide": this.secondarySide?.toShortString(), 99 | }; 100 | } 101 | } 102 | 103 | /// Class representing configurations of the [DocumentScanner]. 104 | class DocumentScannerOptions { 105 | final DocumentScannerCameraOptions cameraOptions; 106 | final List scannableDocuments; 107 | 108 | const DocumentScannerOptions({ 109 | this.cameraOptions = const DocumentScannerCameraOptions(), 110 | this.scannableDocuments = const [], 111 | }); 112 | 113 | Map toMap() { 114 | return { 115 | "cameraOptions": cameraOptions.toMap(), 116 | "scannableDocuments": scannableDocuments.map((e) => e.toMap()).toList(), 117 | }; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ios/Classes/Core/FlutterFaceDetection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlutterFaceDetection.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 3/17/24. 6 | // 7 | 8 | import Foundation 9 | import EkycID 10 | 11 | public class FlutterFaceDetection: NSObject, FlutterPlugin { 12 | var detector: FaceDetection? = nil 13 | 14 | public static func register(with registrar: any FlutterPluginRegistrar) { 15 | let channel = FlutterMethodChannel(name: "FaceDetection_MethodChannel", binaryMessenger: registrar.messenger()) 16 | let instance = FlutterFaceDetection() 17 | registrar.addMethodCallDelegate(instance, channel: channel) 18 | } 19 | 20 | private func initialize(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 21 | self.detector = FaceDetection.Builder().setModelType(modelType: .GOOGLE).build() 22 | result(true) 23 | } 24 | 25 | private func dispose(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 26 | if (self.detector != nil) { 27 | self.detector = nil 28 | } 29 | result(true) 30 | } 31 | 32 | public func detect(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 33 | let imageBytes = call.arguments as! FlutterStandardTypedData 34 | let image = UIImage(data:Data(imageBytes.data)) 35 | let img = UIImage(cgImage: image!.cgImage!) 36 | 37 | let detections = self.detector!.detect(image: img) 38 | if detections == nil || detections?.count ?? 0 <= 0 { 39 | result([]) 40 | } 41 | 42 | result(detections!.map({ face in 43 | 44 | var faceImageCG = ImageUtils.cropBoundingBox( 45 | image: img.cgImage!, 46 | bbox: face.bbox, 47 | scale: 1.2 48 | ) 49 | 50 | if (image?.imageOrientation == .left) { 51 | faceImageCG = faceImageCG.rotate(orienation: .right)! 52 | } else if (image?.imageOrientation == .up) { 53 | faceImageCG = faceImageCG.rotate(orienation: .up)! 54 | } else if (image?.imageOrientation == .down) { 55 | faceImageCG = faceImageCG.rotate(orienation: .down)! 56 | } else if (image?.imageOrientation == .right) { 57 | faceImageCG = faceImageCG.rotate(orienation: .left)! 58 | } 59 | 60 | let faceImage = UIImage(cgImage: faceImageCG) 61 | 62 | return EkycID.LivenessFace( 63 | image: faceImage, 64 | leftEyeOpenProbability: face.leftEyeOpenProbability, 65 | rightEyeOpenProbability: face.rightEyeOpenProbability, 66 | headEulerAngleX: face.headEulerAngleX, 67 | headEulerAngleY: face.headEulerAngleY, 68 | headEulerAngleZ: face.headEulerAngleZ, 69 | headDirection: face.headDirection, 70 | eyesStatus: face.eyesStatus 71 | ).toFlutterMap() 72 | })) 73 | } 74 | 75 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 76 | switch (call.method) { 77 | case "initialize": 78 | do { 79 | try self.initialize(call: call, result: result) 80 | } catch { 81 | result(FlutterError(code: "initialize Error", message: nil, details: nil)) 82 | } 83 | break 84 | case "dispose": 85 | do { 86 | try self.dispose(call: call, result: result) 87 | } catch { 88 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 89 | } 90 | break 91 | case "detect": 92 | do { 93 | try self.detect(call: call, result: result) 94 | } catch { 95 | result(FlutterError(code: "detect Error", message: nil, details: nil)) 96 | } 97 | break 98 | default: 99 | result(FlutterMethodNotImplemented) 100 | break 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:ekyc_id_flutter/ekyc_id_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:image_picker/image_picker.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | import 'package:ekyc_id_flutter/src/core/document_detection.dart'; 6 | import 'package:ekyc_id_flutter/src/document_scanner/document_scanner_values.dart'; 7 | import 'package:ekyc_id_flutter/src/core/models/object_detection_object_type.dart'; 8 | 9 | void main() { 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | runApp(MyApp()); 12 | } 13 | 14 | class MyApp extends StatefulWidget { 15 | @override 16 | _MyAppState createState() => _MyAppState(); 17 | } 18 | 19 | class _MyAppState extends State { 20 | @override 21 | void initState() { 22 | Permission.camera.request(); 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return MaterialApp( 29 | debugShowCheckedModeBanner: false, 30 | home: HomeScreen(), 31 | ); 32 | } 33 | } 34 | 35 | class HomeScreen extends StatefulWidget { 36 | const HomeScreen({Key? key}) : super(key: key); 37 | @override 38 | State createState() => _HomeScreenState(); 39 | } 40 | 41 | class _HomeScreenState extends State { 42 | // var faceDetector = FaceDetectionController(); 43 | // var documentDetector = DocumentDetectionController(); 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | // documentDetector.initialize(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | body: Center( 55 | child: TextButton( 56 | onPressed: () async { 57 | // final ImagePicker picker = ImagePicker(); 58 | 59 | // final XFile? image = 60 | // await picker.pickImage(source: ImageSource.gallery); 61 | // var bytes = await image?.readAsBytes(); 62 | 63 | // if (bytes != null) { 64 | // await documentDetector.setWhiteList([ 65 | // ObjectDetectionObjectType.NATIONAL_ID_0, 66 | // ]); 67 | // List detections = 68 | // await documentDetector.detect(bytes); 69 | 70 | // if (detections.isNotEmpty) { 71 | // showDialog( 72 | // context: context, 73 | // builder: (BuildContext context) { 74 | // return Dialog( 75 | // child: Image.memory(detections[0].documentImage), 76 | // ); 77 | // }, 78 | // ); 79 | // } 80 | // } 81 | await showModalBottomSheet( 82 | context: context, 83 | isScrollControlled: true, 84 | builder: (BuildContext context) { 85 | return FaceScannerView( 86 | options: FaceScannerOptions( 87 | useFrontCamera: true, 88 | ), 89 | onFaceScanned: (face) async { 90 | print(face.toString()); 91 | }, 92 | ); 93 | 94 | return DocumentScannerView( 95 | onDocumentScanned: (mainSide, secondarySide) async { 96 | print(mainSide.toString()); 97 | print(secondarySide?.toString()); 98 | }, 99 | options: DocumentScannerOptions(scannableDocuments: [ 100 | ScannableDocument( 101 | mainSide: ObjectDetectionObjectType.NATIONAL_ID_0, 102 | ) 103 | ]), 104 | overlayBuilder: (BuildContext context, 105 | FrameStatus frameStatus, 106 | DocumentSide side, 107 | int countDown) { 108 | return Container( 109 | child: Center( 110 | child: Text("$frameStatus, $side, $countDown")), 111 | ); 112 | }, 113 | ); 114 | }, 115 | ); 116 | }, 117 | child: Text("Start KYC"), 118 | ), 119 | ), 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/liveness_detection/liveness_detection_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import '../core/models/frame_status.dart'; 4 | import 'liveness_detection_values.dart'; 5 | 6 | class LivenessDetectionController { 7 | late MethodChannel _methodChannel; 8 | late EventChannel _eventChannel; 9 | 10 | LivenessDetectionController(int id) { 11 | _methodChannel = new MethodChannel('LivenessDetection_MethodChannel_$id'); 12 | _eventChannel = new EventChannel('LivenessDetection_EventChannel_$id'); 13 | } 14 | 15 | /// Initializes the camera and starts the scanning process. 16 | /// 17 | /// When the camera is initialized [onInitialized] is called. 18 | /// After the initialization process, the scanning process begins and on every frame [onFrame] is called. 19 | /// When the camera detects a face in the center of frame, [onFocus] is called. 20 | /// After the user completed each prompt, [onPromptCompleted] is called. 21 | Future start({ 22 | required LivenessDetectionOnFocusCallback onFocus, 23 | required LivenessDetectionOnFrameCallback onFrame, 24 | required LivenessDetectionOnInitializedCallback onInitialized, 25 | required LivenessDetectionOnFocusDroppedCallback onFocusDropped, 26 | required LivenessDetectionOnPromptCompletedCallback onPromptCompleted, 27 | required LivenessDetectionOnCountDownChangedCallback onCountDownChanged, 28 | required LivenessDetectionOnAllPromptsCompletedCallback 29 | onAllPromptsCompleted, 30 | required LivenessDetectionOptions options, 31 | }) async { 32 | await _methodChannel.invokeMethod('start', options.toMap()); 33 | 34 | _registerEventListener( 35 | onFocus: onFocus, 36 | onFrame: onFrame, 37 | onInitialized: onInitialized, 38 | onFocusDropped: onFocusDropped, 39 | onPromptCompleted: onPromptCompleted, 40 | onCountDownChanged: onCountDownChanged, 41 | onAllPromptsCompleted: onAllPromptsCompleted, 42 | ); 43 | } 44 | 45 | /// Stops the scanning process and dispose the camera object. 46 | Future dispose() async { 47 | await _methodChannel.invokeMethod('dispose'); 48 | } 49 | 50 | /// Allows the camera to start processing the next frame. 51 | Future nextImage() async { 52 | await _methodChannel.invokeMethod('nextImage'); 53 | } 54 | 55 | void _registerEventListener({ 56 | required LivenessDetectionOnFocusCallback onFocus, 57 | required LivenessDetectionOnFrameCallback onFrame, 58 | required LivenessDetectionOnInitializedCallback onInitialized, 59 | required LivenessDetectionOnFocusDroppedCallback onFocusDropped, 60 | required LivenessDetectionOnPromptCompletedCallback onPromptCompleted, 61 | required LivenessDetectionOnCountDownChangedCallback onCountDownChanged, 62 | required LivenessDetectionOnAllPromptsCompletedCallback 63 | onAllPromptsCompleted, 64 | }) { 65 | _eventChannel.receiveBroadcastStream().listen((event) async { 66 | if (event["type"] == "onFocus") { 67 | onFocus(); 68 | } else if (event["type"] == "onFrame") { 69 | FrameStatus frameStatus = FrameStatus.values.firstWhere( 70 | (e) => e.toString() == "FrameStatus.${event['values']}"); 71 | onFrame(frameStatus); 72 | } else if (event["type"] == "onInitialized") { 73 | onInitialized(); 74 | } else if (event["type"] == "onFocusDropped") { 75 | onFocusDropped(); 76 | } else if (event["type"] == "onPromptCompleted") { 77 | print(event); 78 | Map values = 79 | Map.from(event["values"]); 80 | onPromptCompleted( 81 | completedPromptIndex: values["completedPromptIndex"], 82 | success: values["success"], 83 | progress: values["progress"], 84 | ); 85 | } else if (event["type"] == "onAllPromptsCompleted") { 86 | Map values = Map.from( 87 | event["values"], 88 | ); 89 | LivenessDetectionResult result = 90 | LivenessDetectionResult.fromMap(values); 91 | onAllPromptsCompleted(result); 92 | } else if (event["type"] == "onCountDownChanged") { 93 | Map values = 94 | Map.from(event["values"]); 95 | onCountDownChanged( 96 | current: values["current"], 97 | max: values["max"], 98 | ); 99 | } 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/face_scanner/face_scanner_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:vibration/vibration.dart'; 4 | import 'package:just_audio/just_audio.dart'; 5 | import 'package:flutter_beep/flutter_beep.dart'; 6 | 7 | import 'package:ekyc_id_flutter/src/models/language.dart'; 8 | 9 | import '../core/models/frame_status.dart'; 10 | import '../liveness_detection/liveness_detection_values.dart'; 11 | import 'face_scanner.dart'; 12 | import 'face_scanner_values.dart'; 13 | import 'face_scanner_controller.dart'; 14 | 15 | /// The Camera View for Document Scanning 16 | class FaceScannerView extends StatefulWidget { 17 | const FaceScannerView({ 18 | Key? key, 19 | required this.onFaceScanned, 20 | this.options = const FaceScannerOptions(), 21 | this.language = Language.EN, 22 | this.cameraPlaceholder, 23 | this.overlayBuilder, 24 | this.fullScreen = true, 25 | }) : super(key: key); 26 | 27 | /// The language for the audio and text in the DocumentScannerView. 28 | final Language language; 29 | 30 | /// The option for the DocumentScanner 31 | final FaceScannerOptions options; 32 | 33 | /// The callback for when the document scanning process is completed. 34 | final Future Function(LivenessFace) onFaceScanned; 35 | 36 | final bool fullScreen; 37 | final Widget? cameraPlaceholder; 38 | final Widget Function(BuildContext, FrameStatus, int)? overlayBuilder; 39 | 40 | @override 41 | State createState() => FaceScannerViewState(); 42 | } 43 | 44 | class FaceScannerViewState extends State { 45 | AudioPlayer? player; 46 | // 47 | int countDown = 0; 48 | bool shouldRenderCamera = false; 49 | bool allowFrameStatusUpdate = true; 50 | 51 | late FaceScannerController controller; 52 | FrameStatus frameStatus = FrameStatus.INITIALIZING; 53 | 54 | Future onPlatformViewCreated( 55 | FaceScannerController controller, 56 | ) async { 57 | this.controller = controller; 58 | await start(); 59 | } 60 | 61 | Future start() async { 62 | await this.controller.start( 63 | options: widget.options, 64 | onInitialized: onInitialized, 65 | onFaceScanned: onFaceScanned, 66 | onFrameStatusChanged: onFrameStatusChanged, 67 | onCaptureCountDownChanged: onCaptureCountDownChanged, 68 | ); 69 | } 70 | 71 | Future pause() async { 72 | this.controller.dispose(); 73 | } 74 | 75 | @override 76 | void initState() { 77 | player = AudioPlayer(); 78 | 79 | if (widget.fullScreen) { 80 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); 81 | } 82 | 83 | Future.delayed(const Duration(milliseconds: 500), () { 84 | setState(() { 85 | shouldRenderCamera = true; 86 | }); 87 | }); 88 | super.initState(); 89 | } 90 | 91 | @override 92 | void dispose() { 93 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); 94 | this.player?.dispose(); 95 | this.controller.dispose(); 96 | super.dispose(); 97 | } 98 | 99 | Future onFaceScanned(LivenessFace face) async { 100 | try { 101 | await widget.onFaceScanned(face); 102 | } catch (e) { 103 | print(e); 104 | } finally { 105 | this.controller.nextImage(); 106 | } 107 | } 108 | 109 | void onInitialized() { 110 | playBeep(); 111 | playInstruction(); 112 | } 113 | 114 | void onFrameStatusChanged(FrameStatus f) { 115 | if (mounted) { 116 | setState(() { 117 | frameStatus = f; 118 | }); 119 | } 120 | } 121 | 122 | void onCaptureCountDownChanged(int current, int max) { 123 | if (mounted) { 124 | setState(() { 125 | countDown = current; 126 | }); 127 | } 128 | } 129 | 130 | void playBeep() { 131 | try { 132 | FlutterBeep.beep(); 133 | Vibration.hasVibrator().then((value) { 134 | if (value != null && value) { 135 | Vibration.vibrate(); 136 | } 137 | }); 138 | 139 | } catch (e) {} 140 | } 141 | 142 | void playInstruction() {} 143 | 144 | @override 145 | Widget build(BuildContext context) { 146 | return Stack( 147 | children: [ 148 | Positioned.fill( 149 | child: shouldRenderCamera 150 | ? FaceScanner(onCreated: onPlatformViewCreated) 151 | : widget.cameraPlaceholder ?? Container(), 152 | ), 153 | if (widget.overlayBuilder != null) 154 | Positioned.fill( 155 | child: widget.overlayBuilder!(context, frameStatus, countDown)), 156 | ], 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 40 | { 41 | "keyToString": { 42 | "RunOnceActivity.cidr.known.project.marker": "true", 43 | "cidr.known.project.marker": "true", 44 | "dart.analysis.tool.window.visible": "false", 45 | "io.flutter.reload.alreadyRun": "true", 46 | "last_opened_file_path": "/Users/bunphy/Developer/ekyc-id-flutter", 47 | "settings.editor.selected.configurable": "editor.preferences.fonts.default", 48 | "show.migrate.to.gradle.popup": "false" 49 | } 50 | } 51 | 52 | 53 | $USER_HOME$/.subversion 54 | 55 | 56 | 57 | 58 | 1673940313123 59 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 86 | 87 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /lib/src/document_scanner/document_scanner_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:vibration/vibration.dart'; 4 | import 'package:just_audio/just_audio.dart'; 5 | import 'package:flutter_beep/flutter_beep.dart'; 6 | 7 | import 'package:ekyc_id_flutter/src/models/language.dart'; 8 | import 'package:ekyc_id_flutter/src/document_scanner/document_scanner_values.dart'; 9 | 10 | import '../core/models/frame_status.dart'; 11 | import 'document_scanner.dart'; 12 | import 'document_scanner_controller.dart'; 13 | 14 | /// The Camera View for Document Scanning 15 | class DocumentScannerView extends StatefulWidget { 16 | const DocumentScannerView({ 17 | Key? key, 18 | required this.onDocumentScanned, 19 | this.options = const DocumentScannerOptions(), 20 | this.language = Language.EN, 21 | this.overlayBuilder, 22 | this.fullScreen = true, 23 | }) : super(key: key); 24 | 25 | /// The language for the audio and text in the DocumentScannerView. 26 | final Language language; 27 | 28 | /// The option for the DocumentScanner 29 | final DocumentScannerOptions options; 30 | 31 | /// The callback for when the document scanning process is completed. 32 | final Future Function(DocumentScannerResult, DocumentScannerResult?) 33 | onDocumentScanned; 34 | 35 | final Widget Function(BuildContext, FrameStatus, DocumentSide, int)? 36 | overlayBuilder; 37 | 38 | final bool fullScreen; 39 | 40 | @override 41 | State createState() => DocumentScannerViewState(); 42 | } 43 | 44 | class DocumentScannerViewState extends State { 45 | AudioPlayer? player; 46 | bool shouldRenderCamera = false; 47 | bool showFlippingAnimation = false; 48 | bool allowFrameStatusUpdate = true; 49 | 50 | late DocumentScannerController controller; 51 | int countDown = 0; 52 | DocumentSide currentSide = DocumentSide.MAIN; 53 | FrameStatus frameStatus = FrameStatus.INITIALIZING; 54 | 55 | DocumentScannerResult? mainSide; 56 | DocumentScannerResult? secondarySide; 57 | 58 | Future onPlatformViewCreated( 59 | DocumentScannerController controller, 60 | ) async { 61 | this.controller = controller; 62 | await this.start(); 63 | } 64 | 65 | Future start() async { 66 | await this.controller.start( 67 | options: widget.options, 68 | onInitialized: onInitialized, 69 | onDocumentScanned: onDocumentScanned, 70 | onFrameStatusChanged: onFrameStatusChanged, 71 | onCurrentSideChanged: onCurrentSideChanged, 72 | onCaptureCountDownChanged: onCaptureCountDownChanged, 73 | ); 74 | } 75 | 76 | Future pause() async { 77 | this.controller.dispose(); 78 | } 79 | 80 | @override 81 | void initState() { 82 | player = AudioPlayer(); 83 | if (widget.fullScreen) { 84 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); 85 | } 86 | Future.delayed(const Duration(milliseconds: 500), () { 87 | setState(() { 88 | shouldRenderCamera = true; 89 | }); 90 | }); 91 | super.initState(); 92 | } 93 | 94 | @override 95 | void dispose() { 96 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); 97 | this.player?.dispose(); 98 | this.controller.dispose(); 99 | super.dispose(); 100 | } 101 | 102 | Future onDocumentScanned(DocumentScannerResult mainSide, 103 | DocumentScannerResult? secondarySide) async { 104 | try { 105 | await widget.onDocumentScanned(mainSide, secondarySide); 106 | } catch (e) { 107 | print(e); 108 | } finally { 109 | this.controller.reset(); 110 | this.controller.nextImage(); 111 | } 112 | } 113 | 114 | void onInitialized() { 115 | playBeep(); 116 | playInstruction(); 117 | } 118 | 119 | void onFrameStatusChanged(FrameStatus f) { 120 | if (mounted) { 121 | setState(() { 122 | frameStatus = f; 123 | }); 124 | } 125 | } 126 | 127 | void onCurrentSideChanged(DocumentSide side) { 128 | if (mounted) { 129 | setState(() { 130 | currentSide = side; 131 | }); 132 | playBeep(); 133 | playInstruction(); 134 | } 135 | } 136 | 137 | void onCaptureCountDownChanged(int current, int max) { 138 | if (mounted) { 139 | setState(() { 140 | countDown = current; 141 | }); 142 | } 143 | } 144 | 145 | void playBeep() { 146 | try { 147 | FlutterBeep.beep(); 148 | Vibration.hasVibrator().then((value) { 149 | if (value != null && value) { 150 | Vibration.vibrate(); 151 | } 152 | }); 153 | } catch (e) {} 154 | } 155 | 156 | void playInstruction() { 157 | String source = "packages/ekyc_id_flutter/assets"; 158 | String side = "scan_${currentSide == DocumentSide.MAIN ? "front" : "back"}"; 159 | String language = widget.language == Language.KH ? "kh" : "en"; 160 | try { 161 | player?.setAsset("$source/${side}_$language.mp3").then((value) { 162 | player?.play(); 163 | }); 164 | } catch (e) { 165 | print(e); 166 | } 167 | } 168 | 169 | @override 170 | Widget build(BuildContext context) { 171 | return Stack( 172 | children: [ 173 | Positioned.fill( 174 | child: shouldRenderCamera 175 | ? DocumentScanner( 176 | onCreated: onPlatformViewCreated, 177 | ) 178 | : Container(), 179 | ), 180 | if (widget.overlayBuilder != null) 181 | Positioned.fill( 182 | child: widget.overlayBuilder!( 183 | context, 184 | frameStatus, 185 | currentSide, 186 | countDown, 187 | ), 188 | ), 189 | ], 190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /ios/Classes/Core/FlutterDocumentDetection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlutterDocumentDetection.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 3/18/24. 6 | // 7 | 8 | import Foundation 9 | import EkycID 10 | 11 | public class FlutterDocumentDetection: NSObject, FlutterPlugin { 12 | private var faceDetector: FaceDetection? 13 | private var edgeDetector: EdgeDetection? 14 | private var objectDetector: ObjectDetection? 15 | 16 | public static func register(with registrar: any FlutterPluginRegistrar) { 17 | let channel = FlutterMethodChannel(name: "DocumentDetection_MethodChannel", binaryMessenger: registrar.messenger()) 18 | let instance = FlutterDocumentDetection() 19 | registrar.addMethodCallDelegate(instance, channel: channel) 20 | } 21 | 22 | private func initialize(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 23 | objectDetector = try ObjectDetection.Builder().setModelType(modelType: .MOBILENET_SSD).build() 24 | edgeDetector = try EdgeDetection.Builder().setModelType(modelType: EdgeDetectionModelType.SKSEG) 25 | .build() 26 | faceDetector = FaceDetection.Builder() 27 | .setFaceSize(faceSize: 0.05) 28 | .setModelType(modelType: FaceDetectionModelType.GOOGLE) 29 | .build() 30 | result(true) 31 | } 32 | 33 | private func setWhiteList(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 34 | let args = call.arguments as! [String] 35 | objectDetector?.setWhiteList(whiteList: args.map { e in 36 | return StringToObjectDetectionObjectTypeMapping[e]! 37 | }) 38 | result(true) 39 | } 40 | 41 | private func dispose(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 42 | if (self.objectDetector != nil) { 43 | self.objectDetector = nil 44 | } 45 | 46 | if (self.edgeDetector != nil) { 47 | self.edgeDetector = nil 48 | } 49 | 50 | if (self.faceDetector != nil) { 51 | self.faceDetector = nil 52 | } 53 | result(true) 54 | } 55 | 56 | public func detect(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 57 | let imageBytes = call.arguments as! FlutterStandardTypedData 58 | let tImg = UIImage(data:Data(imageBytes.data)) 59 | var tImgCG: CGImage = tImg!.cgImage! 60 | 61 | if (tImg?.imageOrientation == .left) { 62 | tImgCG = tImgCG.rotate(orienation: .right)! 63 | } else if (tImg?.imageOrientation == .up) { 64 | tImgCG = tImgCG.rotate(orienation: .up)! 65 | } else if (tImg?.imageOrientation == .down) { 66 | tImgCG = tImgCG.rotate(orienation: .down)! 67 | } else if (tImg?.imageOrientation == .right) { 68 | tImgCG = tImgCG.rotate(orienation: .left)! 69 | } 70 | 71 | let image = UIImage(cgImage: tImgCG) 72 | 73 | let detections = self.objectDetector!.detect(image: image.cgImage!) 74 | if detections == nil || detections?.count ?? 0 <= 0 { 75 | result([]) 76 | } 77 | 78 | var results: [DocumentScannerResult] = [] 79 | 80 | 81 | for detection in detections! { 82 | let documentEdge = edgeDetector!.detect(image: image.cgImage!) 83 | if documentEdge == nil { 84 | continue 85 | } 86 | 87 | let corners: [Float] = [ 88 | documentEdge!.topLeft.x, 89 | documentEdge!.topLeft.y, 90 | documentEdge!.topRight.x, 91 | documentEdge!.topRight.y, 92 | documentEdge!.bottomRight.x, 93 | documentEdge!.bottomRight.y, 94 | documentEdge!.bottomLeft.x, 95 | documentEdge!.bottomLeft.y, 96 | ] 97 | 98 | let fullImageUI = UIImage(cgImage: image.cgImage!) 99 | 100 | let warpedImage = OpenCVWrapper.warpImage(fullImageUI, corners) 101 | 102 | var faceImage: UIImage? 103 | 104 | if DOCUMENTS_WITH_FACE.contains(detection.objectType) { 105 | let faces: [FaceDetectionResult]? = faceDetector!.detect(image: warpedImage) 106 | 107 | if !(faces == nil || faces?.count == 0) { 108 | let face = faces![0] 109 | faceImage = UIImage( 110 | cgImage: ImageUtils.cropBoundingBox( 111 | image: warpedImage.cgImage!, 112 | bbox: face.bbox, 113 | scale: 1.4 114 | ) 115 | ) 116 | } 117 | } 118 | 119 | results.append(DocumentScannerResult( 120 | documentType: detection.objectType, 121 | documentGroup: ObjectDetectionObjectTypeToObjectGroupMapping["\(detection.objectType)"]!, 122 | fullImage: fullImageUI, 123 | documentImage: warpedImage, 124 | faceImage: faceImage 125 | )) 126 | } 127 | 128 | result(results.map{e in 129 | return e.toFlutterMap() 130 | }) 131 | } 132 | 133 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 134 | switch (call.method) { 135 | case "initialize": 136 | do { 137 | try self.initialize(call: call, result: result) 138 | } catch { 139 | result(FlutterError(code: "initialize Error", message: nil, details: nil)) 140 | } 141 | break 142 | case "setWhiteList": 143 | do { 144 | try self.setWhiteList(call: call, result: result) 145 | } catch { 146 | result(FlutterError(code: "setWhiteList Error", message: nil, details: nil)) 147 | } 148 | break 149 | case "dispose": 150 | do { 151 | try self.dispose(call: call, result: result) 152 | } catch { 153 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 154 | } 155 | break 156 | case "detect": 157 | do { 158 | try self.detect(call: call, result: result) 159 | } catch { 160 | result(FlutterError(code: "detect Error", message: nil, details: nil)) 161 | } 162 | break 163 | default: 164 | result(FlutterMethodNotImplemented) 165 | break 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/src/liveness_detection/liveness_detection_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:ekyc_id_flutter/src/models/language.dart'; 4 | import 'package:ekyc_id_flutter/src/liveness_detection/liveness_detection.dart'; 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:vibration/vibration.dart'; 8 | import 'package:just_audio/just_audio.dart'; 9 | 10 | import '../core/models/frame_status.dart'; 11 | import 'liveness_detection_values.dart'; 12 | import 'liveness_detection_controller.dart'; 13 | 14 | /// The Camera View for Liveness Detection 15 | class LivenessDetectionView extends StatefulWidget { 16 | LivenessDetectionView({ 17 | Key? key, 18 | required this.onLivenessTestCompleted, 19 | this.language = Language.EN, 20 | this.options = const LivenessDetectionOptions( 21 | promptTimerCountDownSec: 5, 22 | prompts: [ 23 | LivenessPromptType.LOOK_LEFT, 24 | LivenessPromptType.LOOK_RIGHT, 25 | LivenessPromptType.BLINKING 26 | ], 27 | ), 28 | this.overlayBuilder, 29 | }); 30 | 31 | final Widget Function(BuildContext, bool)? overlayBuilder; 32 | 33 | /// The language for the audio and text in the LivenessDetectionView. 34 | final Language language; 35 | 36 | /// The option for the LivenessDetection 37 | final LivenessDetectionOptions options; 38 | 39 | /// The callback for when the liveness test is completed. 40 | final OnLivenessTestCompletedCallback onLivenessTestCompleted; 41 | 42 | @override 43 | _LivenessDetectionViewState createState() => _LivenessDetectionViewState(); 44 | } 45 | 46 | class _LivenessDetectionViewState extends State 47 | with SingleTickerProviderStateMixin { 48 | AudioPlayer? player; 49 | bool hasFailed = false; 50 | bool isFocusing = false; 51 | 52 | int promptTimer = 0; 53 | int? activePromptIndex; 54 | 55 | late LivenessDetectionOptions options; 56 | 57 | LivenessPrompt? prompt; 58 | late LivenessDetectionController controller; 59 | FrameStatus frameStatus = FrameStatus.INITIALIZING; 60 | 61 | late Animation progress; 62 | late AnimationController progressController; 63 | 64 | Future onPlatformViewCreated( 65 | LivenessDetectionController controller, 66 | ) async { 67 | this.controller = controller; 68 | setState(() { 69 | promptTimer = widget.options.promptTimerCountDownSec; 70 | }); 71 | 72 | await this.controller.start( 73 | options: options, 74 | onFocus: onFocus, 75 | onFrame: onFrame, 76 | onInitialized: onInitialized, 77 | onFocusDropped: onFocusDropped, 78 | onPromptCompleted: onPromptCompleted, 79 | onCountDownChanged: onCountDownChanged, 80 | onAllPromptsCompleted: onAllPromptsCompleted, 81 | ); 82 | } 83 | 84 | @override 85 | void initState() { 86 | super.initState(); 87 | setState(() { 88 | options = widget.options; 89 | }); 90 | player = AudioPlayer(); 91 | progressController = AnimationController( 92 | vsync: this, 93 | duration: const Duration(seconds: 5), 94 | )..addListener(() => setState(() {})); 95 | progress = Tween(begin: 0.0, end: 1.0).animate( 96 | CurvedAnimation( 97 | parent: progressController, 98 | curve: Interval(0.0, 1.0, curve: Curves.easeInOutQuad), 99 | ), 100 | ); 101 | } 102 | 103 | @override 104 | void dispose() { 105 | this.player?.dispose(); 106 | this.controller.dispose(); 107 | this.progressController.dispose(); 108 | super.dispose(); 109 | } 110 | 111 | void onCountDownChanged({ 112 | required int current, 113 | required int max, 114 | }) { 115 | if (this.mounted) { 116 | setState(() { 117 | promptTimer = current; 118 | }); 119 | } 120 | } 121 | 122 | void onPromptCompleted({ 123 | required int completedPromptIndex, 124 | required bool success, 125 | required double progress, 126 | }) { 127 | if (this.mounted) { 128 | vibrate(); 129 | 130 | setState(() { 131 | activePromptIndex = activePromptIndex! + 1; 132 | }); 133 | 134 | playInstruction(); 135 | 136 | progressController.animateTo(progress).then((value) { 137 | this.controller.nextImage(); 138 | }); 139 | } 140 | } 141 | 142 | void onFocus() { 143 | if (this.mounted) { 144 | setState(() { 145 | activePromptIndex = 0; 146 | isFocusing = true; 147 | }); 148 | 149 | playInstruction(); 150 | } 151 | } 152 | 153 | void onFocusDropped() { 154 | if (this.mounted) { 155 | progressController.value = 0; 156 | 157 | setState(() { 158 | activePromptIndex = null; 159 | isFocusing = false; 160 | }); 161 | } 162 | } 163 | 164 | void onAllPromptsCompleted(LivenessDetectionResult result) { 165 | if (this.mounted) { 166 | vibrate(); 167 | 168 | progressController.animateTo(1).then((value) { 169 | widget.onLivenessTestCompleted(result).then((value) { 170 | progressController.value = 0; 171 | this.controller.nextImage(); 172 | }); 173 | }); 174 | } 175 | } 176 | 177 | void onFrame(FrameStatus f) { 178 | if (this.mounted) { 179 | setState(() { 180 | frameStatus = f; 181 | }); 182 | } 183 | } 184 | 185 | void onInitialized() {} 186 | 187 | void playInstruction() { 188 | String? type; 189 | if (options.prompts[activePromptIndex!] == LivenessPromptType.BLINKING) { 190 | type = "blink"; 191 | } else if (options.prompts[activePromptIndex!] == 192 | LivenessPromptType.LOOK_LEFT) { 193 | type = "look_left"; 194 | } else if (options.prompts[activePromptIndex!] == 195 | LivenessPromptType.LOOK_RIGHT) { 196 | type = "look_right"; 197 | } 198 | 199 | if (type != null) { 200 | String source = "packages/ekyc_id_flutter/assets"; 201 | String language = widget.language == Language.KH ? "kh" : "en"; 202 | try { 203 | player?.setAsset("$source/${type}_$language.mp3").then((value) { 204 | player?.play(); 205 | }); 206 | } catch (e) { 207 | print(e); 208 | } 209 | } 210 | } 211 | 212 | void vibrate() { 213 | Vibration.hasVibrator().then((value) { 214 | if (value != null && value) { 215 | Vibration.vibrate(); 216 | } 217 | }); 218 | } 219 | 220 | @override 221 | Widget build(BuildContext context) { 222 | return Stack( 223 | children: [ 224 | Positioned.fill( 225 | child: LivenessDetection(onCreated: onPlatformViewCreated), 226 | ), 227 | if (widget.overlayBuilder != null) 228 | Positioned.fill(child: widget.overlayBuilder!(context, isFocusing)), 229 | ], 230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/81238558/175767662-be4dc9ba-a6bd-459d-aaa3-f8ad0c96aa37.png) 2 | 3 | # EkycID SDK for Flutter 4 | ![](https://img.shields.io/badge/platform-flutter-blue) ![](https://img.shields.io/github/v/tag/EKYCSolutions/ekyc-id-flutter?label=version) 5 | 6 | The EkycID Flutter SDK lets you build fantastic OCR and Face Recognition experienced in your Flutter app. 7 | 8 | With one quick scan, your users will be able to extract information from thier identity cards, passports, driver licenses, license plate, vehicle registration, covid-19 vaccinate card, and any other document by government-issued. 9 | 10 | 11 | EkycID is: 12 | * Easy to integrate into existing ecosystems and solutions through the use of SDKs that supported both native and hybrid applications. 13 | * Better for user experience being that the document detections and liveness checks are done directly offline on the device. 14 | * Great for cutting down operations cost and increasing efficiency by decreasing reliance on human labor and time needed for manual data entry. 15 | 16 | 17 | EkycID can: 18 | * Extract information from identity documents through document recognition & OCR. 19 | * Verify whether an individual is real or fake through liveness detection, and face recognition. 20 | * Verify the authenticity of the identity documents by combining the power of document detection, OCR, liveness detection, and face recognition. 21 | 22 | This Flutter SDK is a wrapper around our Native SDK ([Android](https://github.com/EKYCSolutions/ekyc-id-android) & [iOS](https://github.com/EKYCSolutions/ekyc-id-ios)). 23 | 24 | ## 1. Requirements 25 | 26 | ### iOS 27 | - Minimum iOS Deployment Target: 10.0 28 | - Xcode 13 or newer 29 | - Swift 5 30 | - EkycID only supports 64-bit architectures (x86_64 and arm64). 31 | 32 | ### Android 33 | - minSdkVersion: 21 34 | - targetSdkVersion: 32 35 | - compileSdkVersion 32 36 | 37 | ## 2. Installation 38 | 39 | ### 2.1. Flutter Setup 40 | To use this plugin, add `ekyc_id_flutter` as a dependency in your pubspec.yaml file. 41 | ```yaml 42 | dependencies: 43 | ... 44 | ekyc_id_flutter: 45 | ``` 46 | 47 | ### 2.2 iOS Setup 48 | **Step 1:** On iOS add the following to your Info.plist 49 | ```xml 50 | 51 | NSCameraUsageDescription 52 | Camera Access for Scanning 53 | 54 | 55 | NSMicrophoneUsageDescription 56 | Microphone for playing instructions audio. 57 | ``` 58 | 59 | **Step 2:** Go to Project > Runner > Building Settings > Excluded Architectures > Any SDK > armv7 60 | ![](doc/build_settings_01.png) 61 | 62 | **Step 3:** Make adjustments to your Podfile as shown below. 63 | ```ruby 64 | # add this line: 65 | $iOSVersion = '10.0' 66 | 67 | post_install do |installer| 68 | # add these lines: 69 | installer.pods_project.build_configurations.each do |config| 70 | config.build_settings["EXCLUDED_ARCHS[sdk=*]"] = "armv7" 71 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion 72 | end 73 | 74 | installer.pods_project.targets.each do |target| 75 | flutter_additional_ios_build_settings(target) 76 | 77 | # add these lines: 78 | target.build_configurations.each do |config| 79 | if Gem::Version.new($iOSVersion) > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']) 80 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion 81 | end 82 | end 83 | 84 | end 85 | end 86 | ``` 87 | 88 | ### 2.3. Android Setup 89 | No extra setup is needed. 90 | 91 | ## 3. Usage 92 | 93 | **Step 1:** Setup URL to server. 94 | 95 | ```dart 96 | void main() { 97 | WidgetsFlutterBinding.ensureInitialized(); 98 | EkycIDServices.instance.setURL("YOUR_SERVER_URL"); 99 | runApp(MyApp()); 100 | } 101 | ``` 102 | 103 | **Step 2:** Setup EkycIDExpress Widget 104 | 105 | We expose an easy to use widget called `EkycIDExpress`. It handles the logic behind identity document scanning and liveness detections for you. You only expects to provide a simple callback to handle the result. Below is how you can use it. 106 | 107 | ```dart 108 | class HomeScreen extends StatefulWidget { 109 | const HomeScreen({Key? key}) : super(key: key); 110 | 111 | @override 112 | State createState() => _HomeScreenState(); 113 | } 114 | 115 | class _HomeScreenState extends State { 116 | 117 | Future onKYCCompleted({ 118 | required LivenessDetectionResult liveness, 119 | required DocumentScannerResult mainSide, 120 | DocumentScannerResult? secondarySide, 121 | }) async { 122 | print("== ACCESS RESULTS HERE =="); 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return Scaffold( 128 | body: Center( 129 | child: TextButton( 130 | onPressed: () async { 131 | await showCupertinoModalBottomSheet( 132 | context: context, 133 | builder: (BuildContext context) { 134 | return EkycIDExpress( 135 | language: Language.KH, 136 | onKYCCompleted: onKYCCompleted, 137 | ); 138 | }, 139 | ); 140 | }, 141 | child: Text("Start KYC"), 142 | ), 143 | ), 144 | ); 145 | } 146 | } 147 | ``` 148 | 149 | **Step 3:** Handle the results 150 | 151 | In the `onKYCCompleted` callback, you will have access to an instance of `LivenessDetectionResult`, and two instances of `DocumentScannerResult` (One for each side of the document). 152 | 153 | **Step 3:** Match Faces 154 | 155 | You can perform a face compare between the face in the document and the face from liveness detection like below. 156 | 157 | ```dart 158 | 159 | Future onKYCCompleted({ 160 | required LivenessDetectionResult liveness, 161 | required DocumentScannerResult mainSide, 162 | DocumentScannerResult? secondarySide, 163 | }) async { 164 | 165 | ApiResult response = await EkycIDServices.instance.faceCompare( 166 | faceImage1: mainSide.faceImage, 167 | faceImage2: liveness.frontFace?.image, 168 | ); 169 | 170 | print(response.data?) // match score 171 | } 172 | 173 | ``` 174 | 175 | **Step 4:** Perform OCR 176 | 177 | You can perform OCR on the document image like below. 178 | 179 | ```dart 180 | 181 | Future onKYCCompleted({ 182 | required LivenessDetectionResult liveness, 183 | required DocumentScannerResult mainSide, 184 | DocumentScannerResult? secondarySide, 185 | }) async { 186 | 187 | ApiResult response = await EkycIDServices.instance.ocr( 188 | image: mainSide.documentImage, 189 | objectType: mainSide.documentType 190 | ); 191 | 192 | print(response.data?); // response object based on document type 193 | } 194 | 195 | ``` 196 | 197 | ## 4. License 198 | 199 | © 2022 EKYC Solutions Co, Ltd. All rights reserved. 200 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audio_session (0.0.1): 3 | - Flutter 4 | - device_info_plus (0.0.1): 5 | - Flutter 6 | - ekyc_id_flutter (1.0.0): 7 | - EkycID (~> 2.0.7) 8 | - Flutter 9 | - EkycID (2.0.7): 10 | - GoogleMLKit/FaceDetection (= 2.5.0) 11 | - OpenCV (= 4.3.0) 12 | - TensorFlowLiteSwift (~> 2.12.0) 13 | - Flutter (1.0.0) 14 | - flutter_beep (0.0.1): 15 | - Flutter 16 | - GoogleDataTransport (9.4.1): 17 | - GoogleUtilities/Environment (~> 7.7) 18 | - nanopb (< 2.30911.0, >= 2.30908.0) 19 | - PromisesObjC (< 3.0, >= 1.2) 20 | - GoogleMLKit/FaceDetection (2.5.0): 21 | - GoogleMLKit/MLKitCore 22 | - MLKitFaceDetection (~> 1.5.0) 23 | - GoogleMLKit/MLKitCore (2.5.0): 24 | - MLKitCommon (~> 5.0.0) 25 | - GoogleToolboxForMac/DebugUtils (2.3.2): 26 | - GoogleToolboxForMac/Defines (= 2.3.2) 27 | - GoogleToolboxForMac/Defines (2.3.2) 28 | - GoogleToolboxForMac/Logger (2.3.2): 29 | - GoogleToolboxForMac/Defines (= 2.3.2) 30 | - "GoogleToolboxForMac/NSData+zlib (2.3.2)": 31 | - GoogleToolboxForMac/Defines (= 2.3.2) 32 | - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": 33 | - GoogleToolboxForMac/DebugUtils (= 2.3.2) 34 | - GoogleToolboxForMac/Defines (= 2.3.2) 35 | - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" 36 | - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" 37 | - GoogleUtilities/Environment (7.13.0): 38 | - GoogleUtilities/Privacy 39 | - PromisesObjC (< 3.0, >= 1.2) 40 | - GoogleUtilities/Logger (7.13.0): 41 | - GoogleUtilities/Environment 42 | - GoogleUtilities/Privacy 43 | - GoogleUtilities/Privacy (7.13.0) 44 | - GoogleUtilities/UserDefaults (7.13.0): 45 | - GoogleUtilities/Logger 46 | - GoogleUtilities/Privacy 47 | - GoogleUtilitiesComponents (1.1.0): 48 | - GoogleUtilities/Logger 49 | - GTMSessionFetcher/Core (1.7.2) 50 | - image_picker_ios (0.0.1): 51 | - Flutter 52 | - just_audio (0.0.1): 53 | - Flutter 54 | - MLImage (1.0.0-beta2) 55 | - MLKitCommon (5.0.0): 56 | - GoogleDataTransport (~> 9.0) 57 | - GoogleToolboxForMac/Logger (~> 2.1) 58 | - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" 59 | - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" 60 | - GoogleUtilities/UserDefaults (~> 7.0) 61 | - GoogleUtilitiesComponents (~> 1.0) 62 | - GTMSessionFetcher/Core (~> 1.1) 63 | - Protobuf (~> 3.12) 64 | - MLKitFaceDetection (1.5.0): 65 | - MLKitCommon (~> 5.0) 66 | - MLKitVision (~> 3.0) 67 | - MLKitVision (3.0.0): 68 | - GoogleToolboxForMac/Logger (~> 2.1) 69 | - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" 70 | - GTMSessionFetcher/Core (~> 1.1) 71 | - MLImage (= 1.0.0-beta2) 72 | - MLKitCommon (~> 5.0) 73 | - Protobuf (~> 3.12) 74 | - nanopb (2.30910.0): 75 | - nanopb/decode (= 2.30910.0) 76 | - nanopb/encode (= 2.30910.0) 77 | - nanopb/decode (2.30910.0) 78 | - nanopb/encode (2.30910.0) 79 | - OpenCV (4.3.0) 80 | - path_provider_foundation (0.0.1): 81 | - Flutter 82 | - FlutterMacOS 83 | - permission_handler_apple (9.1.1): 84 | - Flutter 85 | - PromisesObjC (2.4.0) 86 | - Protobuf (3.26.0) 87 | - TensorFlowLiteC (2.12.0): 88 | - TensorFlowLiteC/Core (= 2.12.0) 89 | - TensorFlowLiteC/Core (2.12.0) 90 | - TensorFlowLiteSwift (2.12.0): 91 | - TensorFlowLiteSwift/Core (= 2.12.0) 92 | - TensorFlowLiteSwift/Core (2.12.0): 93 | - TensorFlowLiteC (= 2.12.0) 94 | - vibration (1.7.5): 95 | - Flutter 96 | 97 | DEPENDENCIES: 98 | - audio_session (from `.symlinks/plugins/audio_session/ios`) 99 | - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 100 | - ekyc_id_flutter (from `.symlinks/plugins/ekyc_id_flutter/ios`) 101 | - Flutter (from `Flutter`) 102 | - flutter_beep (from `.symlinks/plugins/flutter_beep/ios`) 103 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) 104 | - just_audio (from `.symlinks/plugins/just_audio/ios`) 105 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 106 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 107 | - vibration (from `.symlinks/plugins/vibration/ios`) 108 | 109 | SPEC REPOS: 110 | trunk: 111 | - EkycID 112 | - GoogleDataTransport 113 | - GoogleMLKit 114 | - GoogleToolboxForMac 115 | - GoogleUtilities 116 | - GoogleUtilitiesComponents 117 | - GTMSessionFetcher 118 | - MLImage 119 | - MLKitCommon 120 | - MLKitFaceDetection 121 | - MLKitVision 122 | - nanopb 123 | - OpenCV 124 | - PromisesObjC 125 | - Protobuf 126 | - TensorFlowLiteC 127 | - TensorFlowLiteSwift 128 | 129 | EXTERNAL SOURCES: 130 | audio_session: 131 | :path: ".symlinks/plugins/audio_session/ios" 132 | device_info_plus: 133 | :path: ".symlinks/plugins/device_info_plus/ios" 134 | ekyc_id_flutter: 135 | :path: ".symlinks/plugins/ekyc_id_flutter/ios" 136 | Flutter: 137 | :path: Flutter 138 | flutter_beep: 139 | :path: ".symlinks/plugins/flutter_beep/ios" 140 | image_picker_ios: 141 | :path: ".symlinks/plugins/image_picker_ios/ios" 142 | just_audio: 143 | :path: ".symlinks/plugins/just_audio/ios" 144 | path_provider_foundation: 145 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 146 | permission_handler_apple: 147 | :path: ".symlinks/plugins/permission_handler_apple/ios" 148 | vibration: 149 | :path: ".symlinks/plugins/vibration/ios" 150 | 151 | SPEC CHECKSUMS: 152 | audio_session: 4f3e461722055d21515cf3261b64c973c062f345 153 | device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 154 | ekyc_id_flutter: 2038336f44c62c8f6d4ba705ee39a26d61547da5 155 | EkycID: b80f3abed7196f4ef9e5099e48b299cf636c4aa5 156 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 157 | flutter_beep: 54fb393b22dfa0f0e4573c81b1c74dd71c4e5af8 158 | GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a 159 | GoogleMLKit: 0d7f5aa2f8a2f2ea9d849a05abdbe80974b0ec83 160 | GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 161 | GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 162 | GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe 163 | GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba 164 | image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 165 | just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa 166 | MLImage: a454f9f8ecfd537783a12f9488f5be1a68820829 167 | MLKitCommon: 3bc17c6f7d25ce3660f030350b46ae7ec9ebca6e 168 | MLKitFaceDetection: 617cb847441868a8bfd4b48d751c9b33c1104948 169 | MLKitVision: e87dc3f2e456a6ab32361ebd985e078dd2746143 170 | nanopb: 438bc412db1928dac798aa6fd75726007be04262 171 | OpenCV: 681e0451c1e54c5930a8922277f817ed5d5ab459 172 | path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c 173 | permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 174 | PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 175 | Protobuf: 5685c66a07eaad9d18ce5ab618e9ac01fd04b5aa 176 | TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a 177 | TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793 178 | vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 179 | 180 | PODFILE CHECKSUM: 4904b4cd90c159c1930745e215e1cba88dd8ec5e 181 | 182 | COCOAPODS: 1.15.2 183 | -------------------------------------------------------------------------------- /lib/src/liveness_detection/liveness_detection_values.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import '../core/models/frame_status.dart'; 4 | import 'liveness_detection_controller.dart'; 5 | 6 | /// Callback for when LivenessDetection native view is created. 7 | typedef void LivenessDetectionCreatedCallback( 8 | LivenessDetectionController controller, 9 | ); 10 | 11 | /// Callback on every frame during the liveness detection process. 12 | typedef void LivenessDetectionOnFrameCallback(FrameStatus frameStatus); 13 | 14 | /// Callback for when all prompts is completed. 15 | typedef void LivenessDetectionOnAllPromptsCompletedCallback( 16 | LivenessDetectionResult result, 17 | ); 18 | 19 | /// Callback for when a prompt is completed. 20 | typedef void LivenessDetectionOnPromptCompletedCallback({ 21 | required int completedPromptIndex, 22 | required bool success, 23 | required double progress, 24 | }); 25 | 26 | /// Callback for when the count down changed. 27 | typedef void LivenessDetectionOnCountDownChangedCallback({ 28 | required int current, 29 | required int max, 30 | }); 31 | 32 | /// Callback for when liveness detection is initialized. 33 | typedef void LivenessDetectionOnInitializedCallback(); 34 | 35 | /// Callback for when the liveness detection starts focusing on the user. 36 | typedef void LivenessDetectionOnFocusCallback(); 37 | 38 | /// Callback for when the liveness detection focus dropped. 39 | typedef void LivenessDetectionOnFocusDroppedCallback(); 40 | 41 | /// Callback for when the liveness test is completed. 42 | typedef Future OnLivenessTestCompletedCallback( 43 | LivenessDetectionResult result, 44 | ); 45 | 46 | /// Enum indicating the liveness prompt type for the user to complete. 47 | enum LivenessPromptType { 48 | BLINKING, 49 | LOOK_LEFT, 50 | LOOK_RIGHT, 51 | } 52 | 53 | /// Enum indicating the head direction of the user face. 54 | enum FaceDetectionHeadDirection { 55 | FRONT, 56 | LEFT, 57 | RIGHT, 58 | } 59 | 60 | /// Enum indicating the eye status of the user face. 61 | enum FaceDetectionEyesStatus { 62 | OPEN, 63 | CLOSED, 64 | } 65 | 66 | extension LivenessPromptTypeToString on LivenessPromptType { 67 | String toShortString() { 68 | return this.toString().split('.').last; 69 | } 70 | } 71 | 72 | class LivenessDetectionOptions { 73 | /// Duration in seconds for user to complete a prompt. 74 | final int promptTimerCountDownSec; 75 | 76 | /// List of prompts for the user to complete. 77 | final List prompts; 78 | 79 | const LivenessDetectionOptions({ 80 | this.promptTimerCountDownSec = 5, 81 | this.prompts = const [ 82 | LivenessPromptType.LOOK_LEFT, 83 | LivenessPromptType.LOOK_RIGHT, 84 | LivenessPromptType.BLINKING 85 | ], 86 | }); 87 | 88 | Map toMap() { 89 | return { 90 | "promptTimerCountDownSec": promptTimerCountDownSec, 91 | "prompts": this.prompts.map((e) { 92 | var s = e.toString(); 93 | return s.substring(s.indexOf(".") + 1); 94 | }).toList() 95 | }; 96 | } 97 | } 98 | 99 | class LivenessDetectionResult { 100 | /// The front face of the user 101 | late LivenessFace? frontFace; 102 | 103 | /// The front right of the user 104 | late LivenessFace? rightFace; 105 | 106 | /// The front left of the user 107 | late LivenessFace? leftFace; 108 | 109 | /// List of prompts that the user has encountered 110 | late List prompts; 111 | 112 | LivenessDetectionResult({ 113 | this.frontFace, 114 | this.rightFace, 115 | this.leftFace, 116 | }); 117 | 118 | /// Creates an instance of LivenessDetectionResult from a [json] response. 119 | LivenessDetectionResult.fromMap(Map json) { 120 | if (json["frontFace"] != null) { 121 | this.frontFace = 122 | LivenessFace.fromMap(Map.from(json["frontFace"])); 123 | } else { 124 | this.frontFace = null; 125 | } 126 | 127 | if (json["rightFace"] != null) { 128 | this.rightFace = 129 | LivenessFace.fromMap(Map.from(json["rightFace"])); 130 | } else { 131 | this.rightFace = null; 132 | } 133 | 134 | if (json["leftFace"] != null) { 135 | this.leftFace = 136 | LivenessFace.fromMap(Map.from(json["leftFace"])); 137 | } else { 138 | this.leftFace = null; 139 | } 140 | 141 | this.prompts = List.from(json["prompts"]).map((e) { 142 | return LivenessPrompt.fromMap(Map.from(e)); 143 | }).toList(); 144 | } 145 | } 146 | 147 | /// Class representing the face detection by the liveness detection camera. 148 | class LivenessFace { 149 | /// The face image 150 | late Uint8List image; 151 | 152 | /// The probability that the left eye is open 153 | late double? leftEyeOpenProbability; 154 | 155 | /// The probability that the right eye is open 156 | late double? rightEyeOpenProbability; 157 | 158 | /// The head angle in the X direction 159 | late double? headEulerAngleX; 160 | 161 | /// The head angle in the Y direction 162 | late double? headEulerAngleY; 163 | 164 | /// The head angle in the Z direction 165 | late double? headEulerAngleZ; 166 | 167 | /// The head direction 168 | late FaceDetectionHeadDirection? headDirection; 169 | 170 | /// The eye status 171 | late FaceDetectionEyesStatus? eyesStatus; 172 | 173 | /// Creates an instance of LivenessFace from a [json] response. 174 | LivenessFace.fromMap(Map json) { 175 | this.image = Uint8List.fromList(json["image"]); 176 | this.leftEyeOpenProbability = json["leftEyeOpenProbability"]; 177 | this.rightEyeOpenProbability = json["rightEyeOpenProbability"]; 178 | this.headEulerAngleX = json["headEulerAngleX"]; 179 | this.headEulerAngleY = json["headEulerAngleY"]; 180 | this.headEulerAngleZ = json["headEulerAngleZ"]; 181 | 182 | if (json["headDirection"] != null) { 183 | FaceDetectionHeadDirection d = FaceDetectionHeadDirection.values 184 | .firstWhere((e) => 185 | e.toString() == 186 | "FaceDetectionHeadDirection.${json['headDirection']}"); 187 | this.headDirection = d; 188 | } else { 189 | this.headDirection = null; 190 | } 191 | 192 | if (json["eyesStatus"] != null) { 193 | FaceDetectionEyesStatus d = FaceDetectionEyesStatus.values.firstWhere( 194 | (e) => 195 | e.toString() == "FaceDetectionEyesStatus.${json['eyesStatus']}"); 196 | this.eyesStatus = d; 197 | } else { 198 | this.eyesStatus = null; 199 | } 200 | } 201 | } 202 | 203 | /// Class representing the liveness prompt that the user encounters. 204 | class LivenessPrompt { 205 | /// The liveness prompt type 206 | late LivenessPromptType prompt; 207 | 208 | /// Boolean indicate if the user successfully completed the prompt. 209 | late bool? success; 210 | 211 | LivenessPrompt({ 212 | required this.prompt, 213 | this.success, 214 | }); 215 | 216 | /// Creates an instance of LivenessPrompt from a [json] response. 217 | LivenessPrompt.fromMap(Map json) { 218 | LivenessPromptType prompt = LivenessPromptType.values.firstWhere( 219 | (e) => e.toString() == "LivenessPromptType.${json['prompt']}"); 220 | this.prompt = prompt; 221 | this.success = json["success"]; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /ios/Classes/FaceScanner/FlutterFaceScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlutterFaceScanner.swift 3 | // ekyc_id_flutter 4 | // 5 | // Created by Socret Lee on 3/16/24. 6 | // 7 | 8 | import EkycID 9 | import Foundation 10 | import AVFoundation 11 | 12 | public class FlutterFaceScanner: NSObject, FlutterPlatformView, FaceScannerEventListener { 13 | let viewId: Int64 14 | var flutterScannerView: UIView? 15 | var scanner: FaceScannerView! 16 | var methodChannel: FlutterMethodChannel? 17 | var eventChannel: FlutterEventChannel? 18 | var eventStreamHandler: FaceScannerEventStreamHandler? 19 | 20 | init(frame: CGRect, viewId: Int64, messenger: FlutterBinaryMessenger, args: Any?) { 21 | let screenSize = UIScreen.main.bounds 22 | let screenWidth = screenSize.width 23 | let screenHeight = screenSize.height 24 | 25 | self.viewId = viewId 26 | 27 | super.init() 28 | 29 | self.flutterScannerView = UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)) 30 | self.methodChannel = FlutterMethodChannel(name: "FaceScanner_MethodChannel_" + String(viewId), binaryMessenger: messenger) 31 | self.eventChannel = FlutterEventChannel(name: "FaceScanner_EventChannel_" + String(viewId), binaryMessenger: messenger) 32 | self.eventStreamHandler = FaceScannerEventStreamHandler() 33 | self.methodChannel!.setMethodCallHandler(self.onMethodCall) 34 | self.eventChannel!.setStreamHandler(self.eventStreamHandler) 35 | } 36 | 37 | public func view() -> UIView { 38 | return self.flutterScannerView! 39 | } 40 | 41 | private func start(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 42 | let args = call.arguments as! [String: Any?] 43 | let useFrontCamera = args["useFrontCamera"] as! Bool 44 | self.scanner = FaceScannerView( 45 | frame: self.flutterScannerView!.frame, 46 | useFrontCamera: useFrontCamera 47 | ) 48 | self.scanner.addListener(self) 49 | self.scanner.autoresizingMask = [.flexibleWidth, .flexibleHeight] 50 | self.flutterScannerView!.addSubview(self.scanner) 51 | self.scanner.start(options: self.argsToFaceScannerOptions(args)) 52 | result(true) 53 | } 54 | 55 | private func argsToFaceScannerOptions(_ args: [String: Any?]) -> FaceScannerOptions { 56 | let cameraOptions = args["cameraOptions"] as! [String: Any?] 57 | let captureDurationCountDown = cameraOptions["captureDurationCountDown"] as! Int 58 | let faceCropScale = cameraOptions["faceCropScale"] as! NSNumber 59 | let roiSize = cameraOptions["roiSize"] as! NSNumber 60 | let minFaceWidthPercentage = cameraOptions["minFaceWidthPercentage"] as! NSNumber 61 | let maxFaceWidthPercentage = cameraOptions["maxFaceWidthPercentage"] as! NSNumber 62 | return FaceScannerOptions( 63 | cameraOptions: FaceScannerCameraOptions( 64 | captureDurationCountDown: captureDurationCountDown, 65 | faceCropScale: faceCropScale.floatValue, 66 | roiSize: roiSize.floatValue, 67 | minFaceWidthPercentage: minFaceWidthPercentage.floatValue, 68 | maxFaceWidthPercentage: maxFaceWidthPercentage.floatValue 69 | ) 70 | ) 71 | } 72 | 73 | public func onInitialized() { 74 | self.eventStreamHandler?.sendOnInitializedEventToFlutter() 75 | } 76 | 77 | public func onFaceScanned(_ face: EkycID.LivenessFace) { 78 | self.eventStreamHandler?.sendOnFaceScannedEventToFlutter(face) 79 | } 80 | 81 | public func onFrameStatusChanged(_ frameStatus: EkycID.FrameStatus) { 82 | self.eventStreamHandler?.sendOnFrameStatusChangedEventToFlutter(frameStatus) 83 | } 84 | 85 | public func onCaptureCountDownChanged(current: Int, max: Int) { 86 | self.eventStreamHandler?.sendOnCaptureCountDownChangedEventToFlutter(current: current, max: max) 87 | } 88 | 89 | private func nextImage(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 90 | if self.scanner != nil { 91 | self.scanner.nextImage() 92 | } 93 | result(true) 94 | } 95 | 96 | private func dispose(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 97 | if (self.scanner != nil) { 98 | self.scanner!.stop() 99 | self.scanner = nil 100 | } 101 | result(true) 102 | } 103 | 104 | private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void { 105 | switch (call.method) { 106 | case "start": 107 | do { 108 | try self.start(call: call, result: result) 109 | } catch { 110 | result(FlutterError(code: "initialize Error", message: nil, details: nil)) 111 | } 112 | break 113 | case "dispose": 114 | do { 115 | try self.dispose(call: call, result: result) 116 | } catch { 117 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 118 | } 119 | break 120 | case "nextImage": 121 | do { 122 | try self.nextImage(call: call, result: result) 123 | } catch { 124 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 125 | } 126 | break 127 | default: 128 | result(FlutterMethodNotImplemented) 129 | break 130 | } 131 | } 132 | 133 | class FaceScannerEventStreamHandler: NSObject, FlutterStreamHandler { 134 | var events: FlutterEventSink? 135 | 136 | func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 137 | self.events = events 138 | return nil 139 | } 140 | 141 | func onCancel(withArguments arguments: Any?) -> FlutterError? { 142 | return nil 143 | } 144 | 145 | func sendOnInitializedEventToFlutter() { 146 | if (self.events != nil) { 147 | DispatchQueue.main.async { 148 | var event = [String: Any]() 149 | event["type"] = "onInitialized" 150 | self.events!(event) 151 | } 152 | } 153 | } 154 | 155 | func sendOnFrameStatusChangedEventToFlutter(_ frameStatus: FrameStatus) { 156 | if (self.events != nil) { 157 | DispatchQueue.main.async { 158 | var event = [String: Any]() 159 | event["type"] = "onFrameStatusChanged" 160 | event["values"] = "\(frameStatus)" 161 | self.events!(event) 162 | } 163 | } 164 | } 165 | 166 | func sendOnFaceScannedEventToFlutter(_ face: EkycID.LivenessFace) { 167 | if (self.events != nil) { 168 | DispatchQueue.main.async { 169 | var event = [String: Any]() 170 | event["type"] = "onFaceScanned" 171 | event["values"] = face.toFlutterMap() 172 | self.events!(event) 173 | } 174 | } 175 | } 176 | 177 | func sendOnCaptureCountDownChangedEventToFlutter(current: Int, max: Int) { 178 | if (self.events != nil) { 179 | DispatchQueue.main.async { 180 | var event = [String: Any]() 181 | event["type"] = "onCaptureCountDownChanged" 182 | var result = [String: Any?]() 183 | result["current"] = current 184 | result["max"] = max 185 | event["values"] = result 186 | self.events!(event) 187 | } 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/ekycsolutions/ekyc_id_flutter/FaceScanner/FlutterFaceScanner.kt: -------------------------------------------------------------------------------- 1 | package com.ekycsolutions.ekyc_id_flutter.FaceScanner 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.graphics.Bitmap 7 | import android.view.View 8 | import android.widget.LinearLayout 9 | import com.ekycsolutions.ekyc_id_flutter.R 10 | import com.ekycsolutions.ekycid.core.models.FrameStatus 11 | import com.ekycsolutions.ekycid.facescanner.FaceScannerEventListener 12 | import com.ekycsolutions.ekycid.facescanner.FaceScannerOptions 13 | import com.ekycsolutions.ekycid.facescanner.FaceScannerView 14 | import com.ekycsolutions.ekycid.facescanner.cameraview.FaceScannerCameraOptions 15 | import com.ekycsolutions.ekycid.livenessdetection.cameraview.LivenessFace 16 | import io.flutter.embedding.engine.plugins.FlutterPlugin 17 | import io.flutter.plugin.common.EventChannel 18 | import io.flutter.plugin.common.MethodCall 19 | import io.flutter.plugin.common.MethodChannel 20 | import io.flutter.plugin.platform.PlatformView 21 | import java.io.ByteArrayOutputStream 22 | 23 | 24 | class FlutterFaceScanner( 25 | private var binding: FlutterPlugin.FlutterPluginBinding, 26 | private var context: Context, 27 | private val viewId: Int 28 | ): PlatformView, MethodChannel.MethodCallHandler, FaceScannerEventListener { 29 | private var scanner: FaceScannerView? = null 30 | 31 | private var scannerView: LinearLayout = LinearLayout(context) 32 | private val methodChannel: MethodChannel = 33 | MethodChannel(binding.binaryMessenger, "FaceScanner_MethodChannel_$viewId") 34 | private val eventChannel: EventChannel = 35 | EventChannel(binding.binaryMessenger, "FaceScanner_EventChannel_$viewId") 36 | private val eventStreamHandler = FaceScannerEventStreamHandler(context) 37 | 38 | init { 39 | this.methodChannel.setMethodCallHandler(this) 40 | this.eventChannel.setStreamHandler(eventStreamHandler) 41 | } 42 | 43 | 44 | override fun getView(): View { 45 | return scannerView 46 | } 47 | 48 | override fun dispose() { 49 | 50 | } 51 | 52 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 53 | when (call.method) { 54 | "start" -> { 55 | start(call, result) 56 | } 57 | "nextImage" -> { 58 | nextImage(call, result) 59 | } 60 | "dispose" -> { 61 | disposeFlutter(call, result) 62 | } 63 | else -> { 64 | result.notImplemented() 65 | } 66 | } 67 | } 68 | 69 | private fun start(call: MethodCall, result: MethodChannel.Result) { 70 | try { 71 | val args = call.arguments as HashMap<*, *> 72 | val useFrontCamera = args["useFrontCamera"] as Boolean 73 | val cameraOptions = args["cameraOptions"] as HashMap 74 | this.scanner = FaceScannerView(context, useFrontCamera) 75 | this.scanner!!.addListener(this) 76 | this.scanner!!.start( 77 | FaceScannerOptions( 78 | FaceScannerCameraOptions( 79 | cameraOptions["captureDurationCountDown"] as Int, 80 | (cameraOptions["faceCropScale"] as Double).toFloat(), 81 | (cameraOptions["roiSize"] as Double).toFloat(), 82 | (cameraOptions["minFaceWidthPercentage"] as Double).toFloat(), 83 | (cameraOptions["maxFaceWidthPercentage"] as Double).toFloat(), 84 | ) 85 | ) 86 | ) 87 | 88 | this.scanner!!.layoutParams = LinearLayout.LayoutParams( 89 | LinearLayout.LayoutParams.MATCH_PARENT, 90 | LinearLayout.LayoutParams.MATCH_PARENT 91 | ) 92 | this.scannerView.addView(this.scanner) 93 | result.success(true) 94 | } catch (e: Exception) { 95 | result.error(e.toString(), e.message, "") 96 | } 97 | } 98 | 99 | private fun disposeFlutter(call: MethodCall, result: MethodChannel.Result) { 100 | try { 101 | if (this.scanner != null) { 102 | this.scanner!!.stop() 103 | this.scanner = null 104 | } 105 | result.success(true) 106 | } catch (e: Exception) { 107 | result.error(e.toString(), e.message, "") 108 | } 109 | } 110 | 111 | private fun nextImage(call: MethodCall, result: MethodChannel.Result) { 112 | try { 113 | if (this.scanner != null) { 114 | this.scanner!!.nextImage() 115 | } 116 | result.success(true) 117 | } catch (e: Exception) { 118 | result.error(e.toString(), e.message, "") 119 | } 120 | } 121 | 122 | override fun onInitialized() { 123 | this.eventStreamHandler?.sendOnInitializedEventToFlutter() 124 | } 125 | 126 | override fun onFaceScanned(face: LivenessFace) { 127 | this.eventStreamHandler?.sendOnFaceScannedEventToFlutter(face) 128 | } 129 | 130 | override fun onFrameStatusChanged(frameStatus: FrameStatus) { 131 | this.eventStreamHandler?.sendOnFrameStatusChangedEventToFlutter(frameStatus) 132 | } 133 | 134 | override fun onCaptureCountDownChanged(current: Int, max: Int) { 135 | this.eventStreamHandler?.sendOnCaptureCountDownChangedEventToFlutter(current, max) 136 | } 137 | 138 | class FaceScannerEventStreamHandler(private var context: Context) : EventChannel.StreamHandler { 139 | private var events: EventChannel.EventSink? = null 140 | 141 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 142 | this.events = events 143 | } 144 | 145 | override fun onCancel(arguments: Any?) { 146 | this.events = null 147 | } 148 | 149 | fun sendOnInitializedEventToFlutter() { 150 | if (events != null) { 151 | (context as Activity).runOnUiThread { 152 | val event = HashMap() 153 | event["type"] = "onInitialized" 154 | val values = HashMap() 155 | event["values"] = values 156 | events!!.success(event) 157 | } 158 | } 159 | } 160 | 161 | fun sendOnFrameStatusChangedEventToFlutter(frameStatus: FrameStatus) { 162 | if (events != null) { 163 | (context as Activity).runOnUiThread { 164 | val event = HashMap() 165 | event["type"] = "onFrameStatusChanged" 166 | event["values"] = frameStatus.name 167 | events!!.success(event) 168 | } 169 | } 170 | } 171 | 172 | fun sendOnFaceScannedEventToFlutter(face: LivenessFace) { 173 | if (events != null) { 174 | (context as Activity).runOnUiThread { 175 | val event = HashMap() 176 | event["type"] = "onFaceScanned" 177 | event["values"] = livenessFaceToFlutterMap(face) 178 | events!!.success(event) 179 | } 180 | } 181 | } 182 | 183 | fun sendOnCaptureCountDownChangedEventToFlutter(current: Int, max: Int) { 184 | if (events != null) { 185 | (context as Activity).runOnUiThread { 186 | val event = HashMap() 187 | event["type"] = "onCaptureCountDownChanged" 188 | val values = HashMap() 189 | values["current"] = current 190 | values["max"] = max 191 | event["values"] = values 192 | events!!.success(event) 193 | } 194 | } 195 | } 196 | 197 | private fun livenessFaceToFlutterMap(livenessFace: LivenessFace): HashMap { 198 | var values = HashMap() 199 | values["image"] = bitmapToFlutterByteArray(livenessFace.image!!) 200 | values["leftEyeOpenProbability"] = livenessFace.leftEyeOpenProbability 201 | values["rightEyeOpenProbability"] = livenessFace.rightEyeOpenProbability 202 | values["headEulerAngleX"] = livenessFace.headEulerAngleX 203 | values["headEulerAngleY"] = livenessFace.headEulerAngleY 204 | values["headEulerAngleZ"] = livenessFace.headEulerAngleZ 205 | 206 | if (livenessFace.headDirection != null) { 207 | values["headDirection"] = livenessFace.headDirection!!.name 208 | } else { 209 | values["headDirection"] = null 210 | } 211 | 212 | if (livenessFace.eyesStatus != null) { 213 | values["eyesStatus"] = livenessFace.eyesStatus!!.name 214 | } else { 215 | values["eyesStatus"] = null 216 | } 217 | 218 | return values 219 | } 220 | 221 | private fun bitmapToFlutterByteArray(image: Bitmap): ByteArray { 222 | val stream = ByteArrayOutputStream() 223 | image.compress(Bitmap.CompressFormat.JPEG, 90, stream) 224 | return stream.toByteArray() 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /ios/Classes/DocumentScanner/FlutterDocumentScanner.swift: -------------------------------------------------------------------------------- 1 | 2 | import AVFoundation 3 | import EkycID 4 | import Foundation 5 | 6 | public class FlutterDocumentScanner: NSObject, FlutterPlatformView, DocumentScannerEventListener { 7 | let viewId: Int64 8 | var flutterScannerView: UIView? 9 | var scanner: DocumentScannerView! 10 | var methodChannel: FlutterMethodChannel? 11 | var eventChannel: FlutterEventChannel? 12 | var eventStreamHandler: DocumentScannerEventStreamHandler? 13 | 14 | init(frame: CGRect, viewId: Int64, messenger: FlutterBinaryMessenger, args: Any?) { 15 | let screenSize = UIScreen.main.bounds 16 | let screenWidth = screenSize.width 17 | let screenHeight = screenSize.height 18 | 19 | self.viewId = viewId 20 | 21 | super.init() 22 | self.flutterScannerView = UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)) 23 | self.methodChannel = FlutterMethodChannel(name: "DocumentScanner_MethodChannel_" + String(viewId), binaryMessenger: messenger) 24 | self.eventChannel = FlutterEventChannel(name: "DocumentScanner_EventChannel_" + String(viewId), binaryMessenger: messenger) 25 | self.eventStreamHandler = DocumentScannerEventStreamHandler() 26 | self.methodChannel!.setMethodCallHandler(self.onMethodCall) 27 | self.eventChannel!.setStreamHandler(self.eventStreamHandler) 28 | } 29 | 30 | public func view() -> UIView { 31 | return self.flutterScannerView! 32 | } 33 | 34 | private func start(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 35 | let args = call.arguments as! [String: Any?] 36 | self.scanner = DocumentScannerView(frame: self.flutterScannerView!.frame) 37 | self.scanner.addListener(self) 38 | self.scanner.autoresizingMask = [.flexibleWidth, .flexibleHeight] 39 | self.flutterScannerView!.addSubview(self.scanner) 40 | self.scanner.start(options: self.argsToDocumentScannerOptions(args)) 41 | result(true) 42 | } 43 | 44 | private func argsToDocumentScannerOptions(_ args: [String: Any?]) -> DocumentScannerOptions { 45 | let cameraOptions = args["cameraOptions"] as! [String: Any?] 46 | let captureDurationCountDown = cameraOptions["captureDurationCountDown"] as! Int 47 | let faceCropScale = cameraOptions["faceCropScale"] as! NSNumber 48 | let roiSize = cameraOptions["roiSize"] as! NSNumber 49 | let minDocWidthPercentage = cameraOptions["minDocWidthPercentage"] as! NSNumber 50 | let maxDocWidthPercentage = cameraOptions["maxDocWidthPercentage"] as! NSNumber 51 | let scannableDocuments = args["scannableDocuments"] as! [[String: Any]] 52 | return DocumentScannerOptions( 53 | cameraOptions: DocumentScannerCameraOptions( 54 | captureDurationCountDown: captureDurationCountDown, 55 | faceCropScale: faceCropScale.floatValue, 56 | roiSize: roiSize.floatValue, 57 | minDocWidthPercentage: minDocWidthPercentage.floatValue, 58 | maxDocWidthPercentage: maxDocWidthPercentage.floatValue 59 | ), 60 | scannableDocuments: scannableDocuments.map { e in 61 | let doc = e as! [String: String?] 62 | let mainSide = doc["mainSide"]! 63 | let secondarySide = doc["secondarySide"]! 64 | 65 | return ScannableDocument( 66 | mainSide: StringToObjectDetectionObjectTypeMapping[mainSide!]!, 67 | secondarySide: secondarySide != nil ? StringToObjectDetectionObjectTypeMapping[secondarySide!]! : nil 68 | ) 69 | } 70 | ) 71 | } 72 | 73 | public func onInitialized() { 74 | self.eventStreamHandler?.sendOnInitializedEventToFlutter() 75 | } 76 | 77 | public func onDocumentScanned(mainSide: EkycID.DocumentScannerResult, secondarySide: EkycID.DocumentScannerResult?) { 78 | self.eventStreamHandler?.sendOnDocumentScannedEventToFlutter(mainSide: mainSide, secondarySide: secondarySide) 79 | } 80 | 81 | public func onFrameStatusChanged(_ frameStatus: EkycID.FrameStatus) { 82 | self.eventStreamHandler?.sendOnFrameStatusChangedEventToFlutter(frameStatus) 83 | } 84 | 85 | public func onCurrentSideChanged(_ currentSide: EkycID.DocumentSide) { 86 | self.eventStreamHandler?.sendOnCurrentSideChangedEventToFlutter(currentSide) 87 | } 88 | 89 | public func onCaptureCountDownChanged(current: Int, max: Int) { 90 | self.eventStreamHandler?.sendOnCaptureCountDownChangedEventToFlutter(current: current, max: max) 91 | } 92 | 93 | private func nextImage(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 94 | if self.scanner != nil { 95 | self.scanner.nextImage() 96 | } 97 | result(true) 98 | } 99 | 100 | private func dispose(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 101 | if (self.scanner != nil) { 102 | self.scanner!.stop() 103 | self.scanner = nil 104 | } 105 | result(true) 106 | } 107 | 108 | private func reset(call: FlutterMethodCall, result: @escaping FlutterResult) throws { 109 | if (self.scanner != nil) { 110 | self.scanner!.reset() 111 | } 112 | result(true) 113 | } 114 | 115 | private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { 116 | switch call.method { 117 | case "start": 118 | do { 119 | try self.start(call: call, result: result) 120 | } catch { 121 | result(FlutterError(code: "initialize Error", message: nil, details: nil)) 122 | } 123 | case "dispose": 124 | do { 125 | try self.dispose(call: call, result: result) 126 | } catch { 127 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 128 | } 129 | break 130 | case "nextImage": 131 | do { 132 | try self.nextImage(call: call, result: result) 133 | } catch { 134 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 135 | } 136 | break 137 | case "reset": 138 | do { 139 | try self.reset(call: call, result: result) 140 | } catch { 141 | result(FlutterError(code: "dispose Error", message: nil, details: nil)) 142 | } 143 | break 144 | default: 145 | result(FlutterMethodNotImplemented) 146 | } 147 | } 148 | 149 | class DocumentScannerEventStreamHandler: NSObject, FlutterStreamHandler { 150 | var events: FlutterEventSink? 151 | 152 | func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 153 | print("onListen") 154 | self.events = events 155 | return nil 156 | } 157 | 158 | func onCancel(withArguments arguments: Any?) -> FlutterError? { 159 | print("onCancel") 160 | return nil 161 | } 162 | 163 | func sendOnInitializedEventToFlutter() { 164 | if self.events != nil { 165 | DispatchQueue.main.async { 166 | var event = [String: Any]() 167 | event["type"] = "onInitialized" 168 | self.events!(event) 169 | } 170 | } 171 | } 172 | 173 | func sendOnFrameStatusChangedEventToFlutter(_ frameStatus: FrameStatus) { 174 | if (self.events != nil) { 175 | DispatchQueue.main.async { 176 | var event = [String: Any]() 177 | event["type"] = "onFrameStatusChanged" 178 | event["values"] = "\(frameStatus)" 179 | self.events!(event) 180 | } 181 | } 182 | } 183 | 184 | func sendOnCurrentSideChangedEventToFlutter(_ currentSide: EkycID.DocumentSide) { 185 | if (self.events != nil) { 186 | DispatchQueue.main.async { 187 | var event = [String: Any]() 188 | event["type"] = "onCurrentSideChanged" 189 | event["values"] = "\(currentSide)" 190 | self.events!(event) 191 | } 192 | } 193 | } 194 | 195 | func sendOnDocumentScannedEventToFlutter(mainSide: EkycID.DocumentScannerResult, secondarySide: EkycID.DocumentScannerResult?) { 196 | if (self.events != nil) { 197 | DispatchQueue.main.async { 198 | var event = [String: Any]() 199 | event["type"] = "onDocumentScanned" 200 | 201 | var result = [String: Any?]() 202 | result["mainSide"] = mainSide.toFlutterMap() 203 | if secondarySide != nil { 204 | result["secondarySide"] = secondarySide!.toFlutterMap() 205 | } 206 | 207 | event["values"] = result 208 | self.events!(event) 209 | } 210 | } 211 | } 212 | 213 | func sendOnCaptureCountDownChangedEventToFlutter(current: Int, max: Int) { 214 | if (self.events != nil) { 215 | DispatchQueue.main.async { 216 | var event = [String: Any]() 217 | event["type"] = "onCaptureCountDownChanged" 218 | var result = [String: Any?]() 219 | result["current"] = current 220 | result["max"] = max 221 | event["values"] = result 222 | self.events!(event) 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | extension DocumentScannerResult { 230 | func toFlutterMap() -> [String: Any?] { 231 | var values = [String: Any?]() 232 | values["documentType"] = "\(self.documentType)" 233 | values["documentGroup"] = "\(self.documentGroup)" 234 | values["fullImage"] = FlutterStandardTypedData(bytes: self.fullImage.jpegData(compressionQuality: 0.8)!) 235 | values["documentImage"] = FlutterStandardTypedData(bytes: self.documentImage.jpegData(compressionQuality: 0.8)!) 236 | if self.faceImage != nil { 237 | values["faceImage"] = FlutterStandardTypedData(bytes: self.faceImage!.jpegData(compressionQuality: 0.8)!) 238 | } else { 239 | values["faceImage"] = nil 240 | } 241 | 242 | return values 243 | } 244 | } 245 | --------------------------------------------------------------------------------