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, runs ML Kit face detection on-device to gate challenges, captures the verification frame, and POSTs it to /v1/liveness/check.

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);
},
);

Props

PropTypeRequiredNotes
clientEkycClientyesPass your shared SDK client.
onResultValueChanged<LivenessResult>yesFires once per run, on success or failure. Branch on result.error.
onCancelVoidCallback?noFires when the user taps the close button. Defaults to a no-op.
configLivenessConfignoControls challenge mix, timeouts, and theme tint. Defaults to a sensible 3-challenge passive flow.

The old v1 sdk:, mode:, and onError: props are gone. There is no LivenessMode enum in v2 — the same widget covers both passive and challenge flows, controlled by LivenessConfig.allowedChallenges and challengeCount.

Configuring challenges with LivenessConfig

EkycLivenessWidget(
client: client,
config: LivenessConfig(
allowedChallenges: const [
LivenessChallenge.blink,
LivenessChallenge.turnLeft,
LivenessChallenge.turnRight,
LivenessChallenge.smile,
],
challengeCount: 2, // pick 2 at random per run
randomize: true,
maxAttemptsPerChallenge: 2,
challengeTimeout: const Duration(seconds: 8),
primaryColor: Theme.of(context).colorScheme.primary, // brand the overlay
),
onResult: _onResult,
);
LivenessConfig fieldDefaultNotes
allowedChallengesall fourThe pool the widget draws from. Empty list is invalid.
challengeCount3How many challenges per run. Must be <= allowedChallenges.length.
randomizetrueShuffle challenges per run. Set false for deterministic ordering during tests.
maxAttemptsPerChallenge2Retries before giving up on a single challenge and emitting Failure.
challengeTimeout8sPer-challenge clock. Minimum 2s.
primaryColornull (white)Tint for the active-state oval and progress dots.

Camera preview

As of v2.1.3 the widget renders a live camera preview behind the oval overlay so the user can see themselves while following challenge prompts. Front-camera mirroring is handled by Flutter's CameraPreview natively on both Android and iOS, so the preview matches the user's expectations of a selfie-style view without any manual flip.

Versions before v2.1.3 composited the overlay on a black background — users had to follow prompts blind, which materially hurt completion rates. All consumers should be on v2.1.3 or later.

Result shape

class LivenessResult {
final bool? isLive;
final double? confidence;
final List<LivenessChallenge> completedChallenges;
final String? videoPath;
final String? heroFramePath;
final Map<String, dynamic>? serverResponse;
final LivenessError? error;
}
  • isLive / confidence — convenience accessors hydrated from serverResponse.
  • completedChallenges — which prompts the user actually passed (useful for analytics).
  • videoPath / heroFramePath — local file paths to the captured artifacts. The widget does not delete these; call result.cleanup() once you're done.
  • serverResponse — the raw /v1/liveness/check envelope (lc_id, score, request_id, is_live, etc.). Read here for diagnostics.
  • error — non-null on failure (camera permission denied, no face within timeout, user cancelled, network failure, or a server-side rejection).

The lc_id is a stable identifier (lc_<32 hex>). Persist it server-side so audit / risk review can re-fetch the check via GET /v1/liveness/checks/:id.

Customizing the surrounding UI

The widget owns the camera preview, oval, prompts, and progress dots. Anything outside its bounds is yours — wrap it in your own scaffold, app bar, instructional text, etc.:

Scaffold(
appBar: AppBar(title: const Text('Verify you')),
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text("Hold your phone at eye level."),
),
Expanded(
child: EkycLivenessWidget(client: client, onResult: _onResult),
),
],
),
)