Skip to main content

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​

PropTypeRequiredNotes
clientEkycClientyesYour shared SDK client.
onResultValueChanged<LivenessResult>yesFires once per run, on success or failure. Branch on result.error, then on result.isLive.
onCancelVoidCallback?noFires when the user taps close.
configLivenessConfignoTimeouts and theme tint; see below.

The flow​

  1. Align. The user centres their face in the oval; the widget waits for one second of steady alignment.
  2. 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.
  3. 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.
  4. Upload. The frames go to /v1/liveness/check with the token and an Idempotency-Key. A timed-out upload is retried once with the same key.
  5. Verdict. is_live, score, threshold and reason come back from the server. A not-live verdict with reason challenge_action_not_detected, frame_discontinuity, identity_changed or duplicate_frames is retried once with a fresh challenge before onResult fires, within maxAttemptsPerChallenge.

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,
);
FieldDefaultNotes
challengeTimeout8sHow long the user has to perform the action. Minimum 2 s.
maxAttemptsPerChallenge2Attempts before the widget gives up, covering both on-device timeouts and server not-live verdicts.
primaryColornull (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; call result.cleanup() when done.
  • serverResponse — the raw /v1/liveness/check envelope (lc_id, score, reason, request_id, and components when the platform returns them).
  • error — non-null on failure: permission denied, camera unavailable, no action within the attempts, user cancelled, or a server rejection. A LivenessServerError carries the platform error type (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 NSMicrophoneUsageDescription and RECORD_AUDIO if 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.