ekyc_vn_flutter_sdk 1.0.0
ekyc_vn_flutter_sdk: ^1.0.0 copied to clipboard
On-device eKYC capture for Flutter: guided Vietnamese ID card and passport capture with card liveness, and a face liveness flow (look straight, left, right). Ready-made screens for Android and iOS.
ekyc_vn_flutter_sdk #
Ready-made eKYC capture screens for Flutter apps, running entirely on the device:
CardValidationView: guides the user to capture the front and back of a Vietnamese ID card (CCCD/CMND) or a passport. A card liveness model checks every frame and rejects screen photos, fingers over the card and glare.FaceValidationView: a face liveness flow. The user looks straight, then left, then right. Face detection and a liveness model run on every frame, and the SDK returns a straight-on face photo.
Both widgets are full screens: drop one into a route and handle the callback. The SDK sends no images or user data anywhere; you upload the returned images to your own eKYC backend.
The only network request comes from google_fonts, which downloads the Roboto font from fonts.gstatic.com the first time a screen opens (without network, text falls back to the system font). If your app must not reach Google servers, bundle the Roboto .ttf files in your app's assets and set GoogleFonts.config.allowRuntimeFetching = false in main().
Requirements #
| Minimum | |
|---|---|
| Flutter | 3.38.1 (Dart 3.10). Tested with 3.47.7 |
| Android | minSdk as set by your Flutter version (flutter.minSdkVersion, 24 on Flutter 3.47; the SDK itself runs on API 21+), compileSdk 36, Java 17 |
| Android build tools | Whatever your Flutter version requires. Flutter 3.47 needs Gradle 8.14+, Android Gradle Plugin 8.11.1+, Kotlin 2.2.20+ |
| iOS | 12.0, a real device (the simulator has no camera) |
1. Install #
Add the SDK and the camera override to your app's pubspec.yaml:
dependencies:
ekyc_vn_flutter_sdk: ^1.0.0
# Required. The SDK is tuned for Techainer's build of the camera plugin,
# which picks the capture resolution differently on Android.
dependency_overrides:
camera:
git:
url: https://github.com/Techainer/flutter-camera-package.git
ref: lucas_handle_resolution_prest
path: packages/camera/camera
Then run flutter pub get.
Without the override the app still builds against the official camera 0.10.x, but card images on some Android phones come out at a lower resolution than the SDK was tested with.
2. Platform setup #
Android #
In android/app/build.gradle (or build.gradle.kts):
android {
compileSdk 36
defaultConfig {
minSdkVersion flutter.minSdkVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
Add these two lines to android/gradle.properties. Projects created with a recent Flutter use Android Gradle Plugin 9 and Kotlin 2, which turn two warnings from tflite_flutter into build errors; on older projects the lines are harmless:
# tflite_flutter declares Java 11 while Kotlin 2 defaults to 17.
kotlin.jvm.target.validation.mode=warning
# litert-support and litert-support-api (pulled in by tflite_flutter) share a namespace.
android.uniquePackageNames=false
The camera permission is declared by the camera plugin; you do not need to add it to AndroidManifest.xml.
iOS #
-
New Flutter projects have no
ios/Podfileuntil the first iOS build; Flutter creates it then. If yourPodfilesets a platform, make it at least 12.0:platform :ios, '12.0' -
Add the camera description to
ios/Runner/Info.plist. iOS terminates the app on the first camera access if this key is missing:<key>NSCameraUsageDescription</key> <string>The camera is used to capture your ID card and face for identity verification.</string> <key>NSMicrophoneUsageDescription</key> <string>The microphone is not used.</string>The SDK never records audio, but the camera plugin links the audio APIs, so App Store review expects the microphone key to be present.
-
Run
cd ios && pod install.
3. Capture an ID card #
import 'package:ekyc_vn_flutter_sdk/ekyc_flutter_sdk.dart';
import 'package:flutter/material.dart';
class CardFrontScreen extends StatefulWidget {
const CardFrontScreen({super.key});
@override
State<CardFrontScreen> createState() => _CardFrontScreenState();
}
class _CardFrontScreenState extends State<CardFrontScreen> {
bool _loading = false;
Future<void> _onCard(
bool isValid,
String rawImagePath,
String cardImagePath,
String message,
Future<void> Function(dynamic command) camera,
) async {
setState(() => _loading = false);
if (!mounted) return;
if (isValid) {
// cardImagePath: the card, cropped and straightened. Upload this one.
// rawImagePath: the full camera frame, useful for audits.
await camera(CameraStatus.DISPOSE);
if (mounted) Navigator.of(context).pop(cardImagePath);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
await camera(CameraStatus.RESUME_PREVIEW); // let the user try again
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
CardValidationView(
cardValidationType: CardValidationType.CCCD, // or PASSPORT
currentStep: CardValidationStep.CARDFRONT, // or CARDBACK
callback: _onCard,
loadingCallBack: (show) => setState(() => _loading = show),
),
if (_loading) const Center(child: CircularProgressIndicator()),
],
);
}
}
How the screen behaves:
- The camera opens with a card frame. The text under the frame tells the user what is wrong (no card, wrong side, glare) and changes as they move.
- When the card is valid, the capture button becomes active and the user taps it.
- The SDK pauses the camera, calls
loadingCallBack(true), checks the captured frame again, then calls your callback once. - The camera stays paused after the callback. Call
camera(CameraStatus.RESUME_PREVIEW)to capture again, orcamera(CameraStatus.DISPOSE)before leaving the screen. The SDK never callsloadingCallBack(false); hide your loader in the callback.
Card callback parameters:
| Parameter | Type | Meaning |
|---|---|---|
isValid |
bool |
true if the captured card passed the liveness check |
rawImagePath |
String |
JPEG of the full camera frame, in the app's temporary directory. Empty if capture failed |
cardImagePath |
String |
JPEG of the card only, cropped. Empty if capture failed |
message |
String |
Vietnamese text to show the user, for example "Mặt trước CCCD hợp lệ" or "Ảnh bị loá" |
camera |
Future<void> Function(dynamic) |
Controls the paused camera: CameraStatus.RESUME_PREVIEW, PAUSE_PREVIEW, DISPOSE, INIT |
4. Face liveness #
class FaceScreen extends StatelessWidget {
const FaceScreen({super.key});
@override
Widget build(BuildContext context) {
return FaceValidationView(
multiLangStep: const {
'CHECKING_STRAIGHT': 'Nhìn thẳng',
'CHECKING_LEFT': 'Quay sang trái',
'CHECKING_RIGHT': 'Quay sang phải',
'DONE': 'Hoàn thành',
},
faceValidationCallback: (isValid, faceStraight, videoPath, errorCode, message, camera) async {
if (errorCode == 'CAMERA_ERROR') {
// The camera could not start: permission denied or camera busy.
Navigator.of(context).pop();
return;
}
// The callback also fires on every analysed frame while the user is moving.
// The flow is finished only when a face photo comes back.
if (isValid && faceStraight.isNotEmpty) {
Navigator.of(context).pop(faceStraight);
}
},
);
}
}
How the screen behaves:
- The front camera opens with an oval. The user puts their face inside it and follows the text: straight, left, right. Each step has a time limit; if it runs out, the flow restarts by itself.
- While the user moves, the callback fires for each analysed frame, with
errorCodeset to the detector's status. You can ignore these calls or use them for your own hints. - When all three steps pass, the SDK stops the camera and calls the callback with
isValid = trueandfaceStraightset to the path of the straight-on face photo.
Face callback parameters:
| Parameter | Type | Meaning |
|---|---|---|
isValid |
bool |
Per frame: whether this frame passed. Final call: true |
faceStraight |
String |
Path of the straight-on face JPEG. Only set on the final call; empty otherwise |
videoPath |
String |
Always empty in this version; video recording is turned off |
errorCode |
String |
Detector status for the frame, or CAMERA_ERROR if the camera failed to start |
message |
String |
Text for the user (Vietnamese or English depending on the status) |
camera |
Future<void> Function(dynamic) |
Same camera control as the card screen |
multiLangStep maps step keys to the instruction shown on screen. Pass all four keys: CHECKING_STRAIGHT, CHECKING_LEFT, CHECKING_RIGHT, DONE. The SDK has no built-in text for this line; a missing key shows the CHECKING_STRAIGHT text instead, or nothing if that is missing too.
5. Images returned #
All images are JPEG files in the app's temporary directory (getTemporaryDirectory()). The OS may delete them at any time, so upload them or copy them somewhere permanent right away.
6. A typical eKYC flow #
CardValidationView(CARDFRONT) -> front image
CardValidationView(CARDBACK) -> back image
FaceValidationView -> face image
|
v
your backend: OCR, face match, final decision
Example #
The example/ folder has a working app with one screen per flow (card front, card back, passport, face). Run it on a real device:
cd example
flutter run
Support #
Techainer, https://techainer.com