@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.
- package/README.md +128 -4
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +57 -2
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +17 -8
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- package/src/components/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/Icon.tsx +8 -7
- package/src/components/KycFlow.tsx +9 -2
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/stepHeaderMeta.tsx +6 -1
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/workflowMerge.ts +2 -0
- package/src/index.ts +5 -10
- package/src/lib/documentCaptureCheck.ts +136 -0
- package/src/lib/model-ready.ts +82 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- package/src/lib/selfie-sharpness.ts +211 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -0
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +18 -13
- package/src/screens/LivenessStep.tsx +37 -0
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -1
- package/src/screens/liveness/SelfiePreview.tsx +19 -4
- package/src/services/api-types.ts +28 -2
- package/src/services/api.ts +19 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/mediaCompress.ts +23 -5
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/kycStore.ts +8 -2
- package/src/types/config.ts +15 -0
- package/src/MyazaBiometricAuth.tsx +0 -119
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
- package/src/lib/biometric-auth.ts +0 -52
- package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
- package/src/services/api-biometric.ts +0 -37
- 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
|
-
}
|