@myazahq/kyc-sdk-react-native 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +128 -4
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +57 -2
  9. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  10. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  12. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  13. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  18. package/package.json +17 -8
  19. package/src/MyazaKYC.tsx +4 -11
  20. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  21. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  22. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  23. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  24. package/src/assets/fonts/OFL-Karla.txt +93 -0
  25. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  26. package/src/assets/fonts/README.md +12 -0
  27. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  28. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  30. package/src/capture/useAutoCapture.ts +8 -1
  31. package/src/components/DocumentCropper.tsx +30 -24
  32. package/src/components/DocumentReview.tsx +11 -1
  33. package/src/components/DocumentReviewSide.tsx +6 -2
  34. package/src/components/DocumentReviewZoom.tsx +5 -1
  35. package/src/components/Icon.tsx +8 -7
  36. package/src/components/KycFlow.tsx +9 -2
  37. package/src/components/MyazaButton.tsx +9 -0
  38. package/src/components/RequiredDocumentPill.tsx +114 -0
  39. package/src/components/StepHeader.tsx +23 -3
  40. package/src/components/documentReviewCopy.ts +39 -0
  41. package/src/components/fonts.ts +14 -17
  42. package/src/components/stepHeaderMeta.tsx +6 -1
  43. package/src/config/documentCaptureMethods.ts +31 -0
  44. package/src/config/workflowMerge.ts +2 -0
  45. package/src/index.ts +5 -10
  46. package/src/lib/documentCaptureCheck.ts +136 -0
  47. package/src/lib/model-ready.ts +82 -0
  48. package/src/lib/prime-models.ts +50 -0
  49. package/src/lib/resubmit.ts +36 -7
  50. package/src/lib/selfie-sharpness.ts +211 -0
  51. package/src/liveness/avatarSource.ts +59 -0
  52. package/src/liveness/useModelReady.ts +9 -55
  53. package/src/mrz/extract.ts +71 -4
  54. package/src/mrz/textRecognizer.ts +36 -0
  55. package/src/mrz/useTextModelReady.ts +21 -0
  56. package/src/screens/DocumentCaptureStep.tsx +177 -93
  57. package/src/screens/LivenessAvatar.tsx +18 -13
  58. package/src/screens/LivenessStep.tsx +37 -0
  59. package/src/screens/MrzScanView.tsx +33 -0
  60. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  61. package/src/screens/document/UploadPhase.tsx +177 -0
  62. package/src/screens/document/useDocumentCamera.ts +109 -0
  63. package/src/screens/liveness/LivenessOutcome.tsx +4 -1
  64. package/src/screens/liveness/SelfiePreview.tsx +19 -4
  65. package/src/services/api-types.ts +28 -2
  66. package/src/services/api.ts +19 -3
  67. package/src/services/deviceMetadata.ts +1 -1
  68. package/src/services/mediaCompress.ts +23 -5
  69. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  70. package/src/store/kycStore.ts +8 -2
  71. package/src/types/config.ts +15 -0
  72. package/src/MyazaBiometricAuth.tsx +0 -119
  73. package/src/assets/liveness/Blink.gif +0 -0
  74. package/src/assets/liveness/Nod.gif +0 -0
  75. package/src/assets/liveness/Smile.gif +0 -0
  76. package/src/assets/liveness/Turn.gif +0 -0
  77. package/src/lib/biometric-auth.ts +0 -52
  78. package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
  79. package/src/services/api-biometric.ts +0 -37
  80. package/src/services/api-types-biometric.ts +0 -41
@@ -1,119 +0,0 @@
1
- import React, { useCallback, useState } from 'react';
2
- import { Modal, Platform } from 'react-native';
3
-
4
- import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
5
- import { MyazaButton } from './components/MyazaButton';
6
- import { BiometricAuthFlow } from './screens/biometric/BiometricAuthFlow';
7
- import { primeFaceModel } from './liveness/visionCameraFaceDetector';
8
- import { defaultReauthLabel } from './lib/biometric-auth';
9
- import type { KYCAppearance, ResolvedKYCConfig, SupportedCountry } from './types/config';
10
- import type { KYCError } from './types/verification';
11
- import type { BiometricAuthResponse } from './services/api-types-biometric';
12
-
13
- // ---------------------------------------------------------------------------
14
- // MyazaBiometricAuth — returning-user face re-authentication ("prove it's
15
- // still you"). Mirrors the web SDK's MyazaBiometricAuth: a verified user
16
- // re-authenticates with a live selfie matched 1:1 against their enrollment
17
- // reference (no gov-DB call, no re-KYC). Self-contained: its own trigger +
18
- // modal, hosting the SDK's real liveness step inside a scoped runtime.
19
- // ---------------------------------------------------------------------------
20
-
21
- export interface MyazaBiometricAuthProps {
22
- /** Publishable API key (`pk_…`). The environment is derived from its prefix. */
23
- apiKey: string;
24
- /** Dev-only base-URL override for `pk_dev_` keys. */
25
- devUrl?: string;
26
- /** The org's user reference (Entity.externalUserId) to re-authenticate. */
27
- externalUserId: string;
28
- /** Presence Intelligence method (default 'gestures'). */
29
- livenessMode?: 'gestures' | 'flash' | 'both';
30
- flashSequenceLength?: number;
31
- appearance?: KYCAppearance;
32
- disableClose?: boolean;
33
- /** Open the modal on mount (no trigger button is rendered). */
34
- defaultOpen?: boolean;
35
- /** Custom trigger label (default "Verify it's you"). */
36
- children?: string;
37
- disabled?: boolean;
38
- onOpen?: () => void;
39
- /** The user re-authenticated. `token` is a single-use proof — verify it from
40
- * your backend with a secret key at `/biometric/verify-proof`. */
41
- onAuthenticated?: (result: { attemptId: string; confidence: number | null; token?: string }) => void;
42
- /** The check ran but the user did not pass (no match / liveness failed). */
43
- onFailed?: (result: { status: BiometricAuthResponse['status']; attemptId: string; confidence: number | null }) => void;
44
- /** A technical error (network, not enrolled, insufficient credits, …). */
45
- onError?: (error: KYCError) => void;
46
- onClose?: () => void;
47
- }
48
-
49
- const MODAL_PRESENTATION = Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen';
50
-
51
- export function MyazaBiometricAuth(props: MyazaBiometricAuthProps): React.ReactElement {
52
- const { children, disabled, defaultOpen, onOpen, onAuthenticated, onFailed, onError, onClose } = props;
53
- const [open, setOpen] = useState(defaultOpen === true);
54
-
55
- // The liveness step reads exactly the keys a KYC flow's config carries; the
56
- // scope parks the step order on consent → liveness → submitted, and the
57
- // flow only ever mounts the middle one. `country` is required by the
58
- // resolved shape but nothing on this path reads it.
59
- const config: ResolvedKYCConfig = {
60
- apiKey: props.apiKey,
61
- devUrl: props.devUrl,
62
- country: 'NG' as SupportedCountry,
63
- scope: 'biometric-authentication',
64
- enableSelfie: true,
65
- livenessMode: props.livenessMode ?? 'gestures',
66
- flashSequenceLength: props.flashSequenceLength,
67
- appearance: props.appearance,
68
- disableClose: props.disableClose,
69
- userId: props.externalUserId,
70
- onError,
71
- };
72
-
73
- const openFlow = useCallback(() => {
74
- void primeFaceModel();
75
- setOpen(true);
76
- onOpen?.();
77
- }, [onOpen]);
78
- const close = useCallback(() => {
79
- setOpen(false);
80
- onClose?.();
81
- }, [onClose]);
82
- const blockDismiss = props.disableClose === true;
83
-
84
- return (
85
- <MyazaThemeProvider appearance={props.appearance}>
86
- {defaultOpen !== true && (
87
- <MyazaButton
88
- label={children ?? defaultReauthLabel(props.appearance?.companyName)}
89
- disabled={disabled}
90
- fullWidth={false}
91
- onPress={openFlow}
92
- />
93
- )}
94
- <Modal
95
- visible={open}
96
- animationType="slide"
97
- presentationStyle={blockDismiss ? 'fullScreen' : MODAL_PRESENTATION}
98
- statusBarTranslucent
99
- navigationBarTranslucent
100
- onRequestClose={blockDismiss ? () => undefined : close}
101
- onDismiss={blockDismiss ? undefined : close}
102
- >
103
- {open ? (
104
- // A fresh store per open: the runtime provider creates it once per
105
- // mount, and the modal's children mount once per open.
106
- <KycRuntimeProvider config={config}>
107
- <BiometricAuthFlow
108
- externalUserId={props.externalUserId}
109
- onAuthenticated={(r) => onAuthenticated?.({ attemptId: r.attemptId, confidence: r.confidence, token: r.token })}
110
- onFailed={(r) => onFailed?.({ status: r.status, attemptId: r.attemptId, confidence: r.confidence })}
111
- onError={onError}
112
- onClose={close}
113
- />
114
- </KycRuntimeProvider>
115
- ) : null}
116
- </Modal>
117
- </MyazaThemeProvider>
118
- );
119
- }
Binary file
Binary file
Binary file
Binary file
@@ -1,52 +0,0 @@
1
- import { KYCApiError } from '../services/api';
2
- import { KYCError } from '../types/verification';
3
- import type { BiometricAuthResponse } from '../services/api-types-biometric';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Biometric re-authentication — the pure half. Mirrors the web SDK's
7
- // MyazaBiometricAuth error mapping and outcome shapes (keep in lockstep).
8
- // ---------------------------------------------------------------------------
9
-
10
- export type BiometricAuthOutcome =
11
- | { kind: 'success'; result: BiometricAuthResponse }
12
- | { kind: 'failed'; result: BiometricAuthResponse }
13
- | { kind: 'error'; message: string };
14
-
15
- export interface BiometricAuthenticated {
16
- attemptId: string;
17
- confidence: number | null;
18
- /** Single-use proof; verify from your backend at `/biometric/verify-proof`. */
19
- token?: string;
20
- }
21
-
22
- export interface BiometricAuthFailed {
23
- status: BiometricAuthResponse['status'];
24
- attemptId: string;
25
- confidence: number | null;
26
- }
27
-
28
- /** A technical failure, in the SDK's typed vocabulary + a message a person
29
- * can read. Never the server's own wording — that is an internal contract. */
30
- export function mapAuthError(err: unknown): KYCError {
31
- if (err instanceof KYCApiError) {
32
- if (err.statusCode === 404 && err.code === 'not_enrolled') {
33
- return new KYCError('unknown', "You're not set up for face verification yet.");
34
- }
35
- if (err.statusCode === 402) {
36
- return new KYCError('insufficient_credits', 'Face verification is temporarily unavailable.');
37
- }
38
- if (err.statusCode === 401 || err.statusCode === 403) {
39
- return new KYCError('invalid_api_key', 'This app is not authorised for face verification.');
40
- }
41
- }
42
- return new KYCError('network_error', 'Something went wrong. Please try again.');
43
- }
44
-
45
- /** The verdict as the callbacks and the result screen read it. */
46
- export function outcomeOf(result: BiometricAuthResponse): BiometricAuthOutcome {
47
- return result.authenticated ? { kind: 'success', result } : { kind: 'failed', result };
48
- }
49
-
50
- export function defaultReauthLabel(companyName?: string): string {
51
- return companyName ? `Verify it's you with ${companyName}` : "Verify it's you";
52
- }
@@ -1,170 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { ActivityIndicator, Pressable, View } from 'react-native';
3
-
4
- import { spacing } from '../../config/theme';
5
- import { useKyc, useKycConfig, useKycStore, useTheme } from '../../components/runtime';
6
- import { KycSheet } from '../../components/KycSheet';
7
- import { ToastProvider } from '../../components/toast';
8
- import { MyazaText } from '../../components/Typography';
9
- import { MyazaButton } from '../../components/MyazaButton';
10
- import { Icon } from '../../components/Icon';
11
- import { LivenessStep } from '../LivenessStep';
12
- import { mapAuthError, outcomeOf, type BiometricAuthOutcome } from '../../lib/biometric-auth';
13
- import type { KYCError } from '../../types/verification';
14
- import type { BiometricAuthResponse } from '../../services/api-types-biometric';
15
-
16
- // ---------------------------------------------------------------------------
17
- // The re-auth flow inside its own runtime: intro → the REAL liveness step →
18
- // authenticating → result. The liveness step is the ordinary one — camera,
19
- // challenges, capture ring, selfie upload — mounted alone with the store
20
- // parked on 'liveness'. Its Continue advances the store to 'submitted' (the
21
- // scope's step order), which is the hand-over: the uploaded selfie's mediaId
22
- // is read off the store and sent to /biometric/authenticate. The submitted
23
- // screen is never rendered, so nothing ever calls /verify.
24
- // ---------------------------------------------------------------------------
25
-
26
- type View_ = 'intro' | 'capture' | 'authenticating' | 'result';
27
-
28
- export function BiometricAuthFlow({
29
- externalUserId,
30
- onAuthenticated,
31
- onFailed,
32
- onError,
33
- onClose,
34
- }: {
35
- externalUserId: string;
36
- onAuthenticated?: (result: BiometricAuthResponse) => void;
37
- onFailed?: (result: BiometricAuthResponse) => void;
38
- onError?: (error: KYCError) => void;
39
- onClose: () => void;
40
- }): React.ReactElement {
41
- const config = useKycConfig();
42
- const store = useKycStore();
43
- const currentStep = useKyc((s) => s.currentStep);
44
- const selfieId = useKyc((s) => s.mediaIds.selfie);
45
- const immersive = useKyc((s) => s.immersiveCapture);
46
- const [view, setView] = useState<View_>('intro');
47
- const [outcome, setOutcome] = useState<BiometricAuthOutcome | null>(null);
48
- const inFlight = useRef(false);
49
-
50
- const startCapture = () => {
51
- store.setState({ currentStep: 'liveness', mediaIds: {}, selfiePreviewUri: null });
52
- setOutcome(null);
53
- setView('capture');
54
- };
55
-
56
- // The hand-over: the step's Continue moved the store past 'liveness' with
57
- // the selfie uploaded.
58
- useEffect(() => {
59
- if (view !== 'capture' || currentStep === 'liveness' || !selfieId || inFlight.current) return;
60
- inFlight.current = true;
61
- setView('authenticating');
62
- const mode = config.livenessMode ?? 'gestures';
63
- store
64
- .getState()
65
- .api.authenticate({ externalUserId, selfie: selfieId, liveness: { mode, passed: true } })
66
- .then((result) => {
67
- const next = outcomeOf(result);
68
- setOutcome(next);
69
- if (next.kind === 'success') onAuthenticated?.(result);
70
- else onFailed?.(result);
71
- })
72
- .catch((err: unknown) => {
73
- const kyc = mapAuthError(err);
74
- setOutcome({ kind: 'error', message: kyc.message });
75
- onError?.(kyc);
76
- })
77
- .finally(() => {
78
- inFlight.current = false;
79
- setView('result');
80
- });
81
- }, [view, currentStep, selfieId, config.livenessMode, externalUserId, store, onAuthenticated, onFailed, onError]);
82
-
83
- const title =
84
- view === 'capture' ? 'Face check' : view === 'authenticating' ? "Verifying it's you" : "Verify it's you";
85
- const dismissBlocked = config.disableClose === true || view === 'authenticating';
86
-
87
- return (
88
- <ToastProvider>
89
- <KycSheet
90
- title={title}
91
- description={view === 'capture' ? 'Follow the prompts, then hold still for the photo.' : null}
92
- progress={null}
93
- stepCount={null}
94
- onBack={view === 'capture' ? () => setView('intro') : null}
95
- onClose={dismissBlocked ? () => undefined : onClose}
96
- immersive={immersive}
97
- >
98
- {view === 'intro' && <Intro onStart={startCapture} />}
99
- {view === 'capture' && <LivenessStep />}
100
- {view === 'authenticating' && <Authenticating />}
101
- {view === 'result' && outcome && <Result outcome={outcome} onRetry={startCapture} onClose={onClose} />}
102
- </KycSheet>
103
- </ToastProvider>
104
- );
105
- }
106
-
107
- function Intro({ onStart }: { onStart: () => void }): React.ReactElement {
108
- const { colors } = useTheme();
109
- return (
110
- <View style={{ alignItems: 'center', paddingVertical: spacing.lg }}>
111
- <View style={{ width: 80, height: 80, borderRadius: 40, backgroundColor: `${colors.primary}1A`, alignItems: 'center', justifyContent: 'center' }}>
112
- <Icon name="user" size={36} color={colors.primary} />
113
- </View>
114
- <View style={{ height: spacing.lg }} />
115
- <MyazaText variant="body" style={{ textAlign: 'center' }} color={colors.textSecondary}>
116
- We'll take a quick face check to confirm your identity. No documents needed.
117
- </MyazaText>
118
- <View style={{ height: spacing.xl }} />
119
- <MyazaButton label="Start" onPress={onStart} />
120
- </View>
121
- );
122
- }
123
-
124
- function Authenticating(): React.ReactElement {
125
- const { colors } = useTheme();
126
- return (
127
- <View style={{ alignItems: 'center', paddingVertical: spacing.xl * 2 }}>
128
- <ActivityIndicator size="large" color={colors.primary} />
129
- <View style={{ height: spacing.lg }} />
130
- <MyazaText variant="body" style={{ fontWeight: '600' }}>Verifying it's you…</MyazaText>
131
- <MyazaText variant="bodySmall" color={colors.textSecondary}>This only takes a moment.</MyazaText>
132
- </View>
133
- );
134
- }
135
-
136
- function Result({ outcome, onRetry, onClose }: { outcome: BiometricAuthOutcome; onRetry: () => void; onClose: () => void }): React.ReactElement {
137
- const { colors } = useTheme();
138
- const ok = outcome.kind === 'success';
139
- const tint = ok ? colors.success : colors.error;
140
- const heading = ok ? "You're verified" : outcome.kind === 'failed' ? "Couldn't verify you" : 'Something went wrong';
141
- const detail = ok
142
- ? "We confirmed it's really you."
143
- : outcome.kind === 'failed'
144
- ? "We couldn't confirm it's you. Make sure your face is clear and well lit, then try again."
145
- : outcome.message;
146
- return (
147
- <View style={{ alignItems: 'center', paddingVertical: spacing.lg }}>
148
- <View style={{ width: 80, height: 80, borderRadius: 40, backgroundColor: `${tint}1A`, alignItems: 'center', justifyContent: 'center' }}>
149
- {/* The web's result: a check, or the alert circle the flow's own
150
- verdict screen uses (never a bare X), under a heading-font title. */}
151
- <Icon name={ok ? 'check' : 'alert'} size={36} color={tint} />
152
- </View>
153
- <View style={{ height: spacing.lg }} />
154
- <MyazaText variant="heading2" style={{ textAlign: 'center' }}>{heading}</MyazaText>
155
- <View style={{ height: spacing.xs }} />
156
- <MyazaText variant="body" color={colors.textSecondary} style={{ textAlign: 'center' }}>{detail}</MyazaText>
157
- <View style={{ height: spacing.xl }} />
158
- {ok ? (
159
- <MyazaButton label="Done" onPress={onClose} />
160
- ) : (
161
- <>
162
- <MyazaButton label="Try again" onPress={onRetry} />
163
- <Pressable onPress={onClose} accessibilityRole="button" hitSlop={8} style={{ paddingVertical: spacing.md }}>
164
- <MyazaText variant="body" color={colors.textSecondary}>Close</MyazaText>
165
- </Pressable>
166
- </>
167
- )}
168
- </View>
169
- );
170
- }
@@ -1,37 +0,0 @@
1
- import type {
2
- BiometricAuthRequest,
3
- BiometricAuthResponse,
4
- BiometricStatusResponse,
5
- } from './api-types-biometric';
6
-
7
- // ---------------------------------------------------------------------------
8
- // The biometric re-authentication calls, split out of createKYCApi (200-line
9
- // rule). Given the client's own `request`, so they ride the same base URL,
10
- // bearer key, SDK-version header and error mapping as everything else.
11
- // ---------------------------------------------------------------------------
12
-
13
- export type JsonRequest = <T>(path: string, init?: RequestInit) => Promise<T>;
14
-
15
- export function biometricCalls(request: JsonRequest) {
16
- return {
17
- /**
18
- * Re-authenticate a verified user by matching a live selfie 1:1 against
19
- * their KYC enrollment reference. Publishable-safe. Uniform 404
20
- * `not_enrolled` — a missing entity, a business entity and no template
21
- * all answer the same, so nothing can be probed.
22
- */
23
- async authenticate(body: BiometricAuthRequest): Promise<BiometricAuthResponse> {
24
- return request<BiometricAuthResponse>('/biometric/authenticate', {
25
- method: 'POST',
26
- body: JSON.stringify(body),
27
- });
28
- },
29
-
30
- /** Whether a user is enrolled for face re-auth (whether to OFFER it). */
31
- async getBiometricStatus(externalUserId: string): Promise<BiometricStatusResponse> {
32
- return request<BiometricStatusResponse>(
33
- `/biometric/status/${encodeURIComponent(externalUserId)}`,
34
- );
35
- },
36
- };
37
- }
@@ -1,41 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Biometric re-authentication wire shapes — a mirror of the web SDK's
3
- // services/api.ts (keep the two in lockstep). Publishable-safe: the verdict,
4
- // a confidence, and a single-use proof token. No PII.
5
- // ---------------------------------------------------------------------------
6
-
7
- /** What the SDK asserts about the liveness run that produced the selfie. */
8
- export interface BiometricLivenessClaim {
9
- mode: 'gestures' | 'flash' | 'both';
10
- passed: boolean;
11
- }
12
-
13
- /** Request body for `POST /api/kyc/biometric/authenticate`. */
14
- export interface BiometricAuthRequest {
15
- /** The org's user reference (Entity.externalUserId) being re-authenticated. */
16
- externalUserId: string;
17
- /** A mediaId from `upload(file, 'selfie')` — the live selfie. */
18
- selfie: string;
19
- liveness?: BiometricLivenessClaim;
20
- }
21
-
22
- /**
23
- * Response from `POST /api/kyc/biometric/authenticate`. `token` is present
24
- * only on `authenticated` — redeem it from your backend with a secret key at
25
- * `/biometric/verify-proof`.
26
- */
27
- export interface BiometricAuthResponse {
28
- authenticated: boolean;
29
- status: 'authenticated' | 'no_match' | 'liveness_failed';
30
- confidence: number | null;
31
- live: boolean;
32
- attemptId: string;
33
- token?: string;
34
- }
35
-
36
- /** Response from `GET /api/kyc/biometric/status/:externalUserId`. */
37
- export interface BiometricStatusResponse {
38
- enrolled: boolean;
39
- enrolledAt?: string;
40
- lastAuthenticatedAt?: string | null;
41
- }