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
| Prop | Type | Required | Notes |
|---|---|---|---|
client | EkycClient | yes | Pass your shared SDK client. |
onResult | ValueChanged<LivenessResult> | yes | Fires once per run, on success or failure. Branch on result.error. |
onCancel | VoidCallback? | no | Fires when the user taps the close button. Defaults to a no-op. |
config | LivenessConfig | no | Controls 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 field | Default | Notes |
|---|---|---|
allowedChallenges | all four | The pool the widget draws from. Empty list is invalid. |
challengeCount | 3 | How many challenges per run. Must be <= allowedChallenges.length. |
randomize | true | Shuffle challenges per run. Set false for deterministic ordering during tests. |
maxAttemptsPerChallenge | 2 | Retries before giving up on a single challenge and emitting Failure. |
challengeTimeout | 8s | Per-challenge clock. Minimum 2s. |
primaryColor | null (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 fromserverResponse.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; callresult.cleanup()once you're done.serverResponse— the raw/v1/liveness/checkenvelope (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),
),
],
),
)