EkycLivenessWidget
A drop-in widget that drives the full liveness flow: requests camera
permission, shows the live camera preview with a face-alignment oval, asks
the platform for a challenge, guides the user through it with on-device ML
Kit face detection, captures frames from the camera stream while they
perform it, and POSTs the sequence to /v1/liveness/check with
mode: challenge. The verdict is always the server's.
import 'package:drukverify_flutter/drukverify_flutter.dart';
EkycLivenessWidget(
client: client,
onResult: (LivenessResult r) {
if (r.error != null) {
// widget-level failure (camera permission denied, user cancelled, etc.)
return;
}
// r.isLive == true on success; r.serverResponse carries the full
// platform envelope (lc_id, score, request_id, …).
Navigator.of(context).pop(r);
},
);
EkycLiveness.start(context, client: client) pushes the same widget as a
full-screen route and resolves with the LivenessResult.
Props
| Prop | Type | Required | Notes |
|---|---|---|---|
client | EkycClient | yes | Your shared SDK client. |
onResult | ValueChanged<LivenessResult> | yes | Fires once per run, on success or failure. Branch on result.error, then on result.isLive. |
onCancel | VoidCallback? | no | Fires when the user taps close. |
config | LivenessConfig | no | Timeouts and theme tint; see below. |
The flow
- Align. The user centres their face in the oval; the widget waits for one second of steady alignment.
- Challenge. The widget calls
POST /v1/liveness/challenges. The server picks one action, blink, turn left or turn right, and issues a one-shot token that expires after about two minutes. - Perform. The prompt shows the action. The widget captures a neutral baseline frame, a frame the moment the on-device detector sees the action, periodic fallback frames, and one frame after the action completes, at most ten. A frame identical to the previous one is dropped on the phone.
- Upload. The frames go to
/v1/liveness/checkwith the token and anIdempotency-Key. A timed-out upload is retried once with the same key. - Verdict.
is_live,score,thresholdandreasoncome back from the server. A not-live verdict with reasonchallenge_action_not_detected,frame_discontinuity,identity_changedorduplicate_framesis retried once with a fresh challenge beforeonResultfires, withinmaxAttemptsPerChallenge.
If the user does not complete the action within challengeTimeout, the
widget discards the frames and restarts the same challenge, up to
maxAttemptsPerChallenge attempts, then fails with
LivenessChallengeFailed.
LivenessConfig
EkycLivenessWidget(
client: client,
config: LivenessConfig(
challengeTimeout: const Duration(seconds: 8),
primaryColor: Theme.of(context).colorScheme.primary, // brand the overlay
),
onResult: _onResult,
);
| Field | Default | Notes |
|---|---|---|
challengeTimeout | 8s | How long the user has to perform the action. Minimum 2 s. |
maxAttemptsPerChallenge | 2 | Attempts before the widget gives up, covering both on-device timeouts and server not-live verdicts. |
primaryColor | null (white) | Tint for the active-state oval and progress dots. |
allowedChallenges, challengeCount and randomize are still accepted
for source compatibility but ignored since 2.4.0: the server issues exactly
one challenge per run and chooses it. smile is never issued.
Camera preview
The widget renders the live camera preview behind the oval overlay. The
front camera is mirrored by Flutter's CameraPreview as users expect of a
selfie view; the frames uploaded to the server are the sensor's unmirrored
view, which is what its turn check assumes.
Result shape
class LivenessResult {
final bool? isLive;
final double? confidence;
final List<LivenessChallenge> completedChallenges;
final String? heroFramePath;
final List<String> framePaths;
final Map<String, dynamic>? serverResponse;
final LivenessError? error;
}
isLive/confidence— the server's verdict and score.completedChallenges— the action the user passed on-device.framePaths/heroFramePath— local paths of the uploaded frames and the last of them. The widget does not delete these; callresult.cleanup()when done.serverResponse— the raw/v1/liveness/checkenvelope (lc_id,score,reason,request_id, andcomponentswhen the platform returns them).error— non-null on failure: permission denied, camera unavailable, no action within the attempts, user cancelled, or a server rejection. ALivenessServerErrorcarries the platform errortype(no_face,low_image_quality,multiple_faces,frame_replayed,challenge_token_invalid), and the built-in failure screen titles it accordingly.
The lc_id is a stable identifier (lc_<32 hex>). Persist it server-side
so audit or risk review can re-fetch the check via
GET /v1/liveness/checks/:id.
Attempts and retries (2.5.0)
- The widget asks for the camera permission only. Nothing records
audio; drop
NSMicrophoneUsageDescriptionandRECORD_AUDIOif an earlier release made you add them. - Frames are resized to a 640 px long edge, a frame that repeats the previous camera buffer is dropped on the phone, and head turns complete at 15 degrees on-device, which is what the server's turn check needs.
- Every upload carries an
Idempotency-Key, a timed-out upload is retried once with the same key, and a retryable not-live verdict re-runs the challenge once with a fresh token.
Customizing the surrounding UI
The widget owns the capture area only. Wrap it in your own scaffold, app
bar and instructions; use EkycLiveness.start when a full-screen route is
enough.