@myazahq/kyc-sdk-react-native 2.6.0 → 3.1.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 +24 -14
- 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/GlassIconButton.tsx +2 -5
- package/src/components/Icon.tsx +17 -13
- package/src/components/KycFlow.tsx +10 -2
- package/src/components/KycSheet.tsx +0 -1
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +3 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icons/glyphs.ts +35 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/icons/map.ts +165 -0
- package/src/components/icons/names.ts +86 -0
- package/src/components/stepHeaderMeta.tsx +21 -1
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/stepOrder.ts +20 -5
- package/src/config/supportingDocuments.ts +131 -0
- package/src/config/theme.ts +29 -3
- package/src/config/workflowMerge.ts +15 -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/lib/supportingDocumentsIntro.ts +35 -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/BusinessDocumentSlot.tsx +37 -8
- 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/SupportingDocumentCard.tsx +134 -0
- package/src/screens/SupportingDocumentsStep.tsx +203 -0
- package/src/screens/consent/model.ts +22 -5
- 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/CaptureRing.tsx +31 -17
- package/src/screens/liveness/LivenessOutcome.tsx +4 -1
- package/src/screens/liveness/SelfiePreview.tsx +19 -4
- package/src/screens/supportingDocumentParts.tsx +126 -0
- package/src/screens/useBusinessDocumentAttach.ts +20 -9
- package/src/services/api-types.ts +31 -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/derive.ts +11 -0
- package/src/store/kycStore.ts +23 -2
- package/src/store/session.ts +18 -0
- package/src/store/state.ts +26 -0
- package/src/store/submit.ts +11 -0
- package/src/types/config.ts +25 -0
- package/src/types/workflow.ts +39 -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/components/icon-map.ts +0 -176
- 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
package/src/store/kycStore.ts
CHANGED
|
@@ -62,6 +62,7 @@ import { IDLE_SELFIE_UPLOAD } from '../lib/selfie-upload-wait';
|
|
|
62
62
|
import { resetBusinessCheck, runBusinessCheck } from './businessCheck';
|
|
63
63
|
import { startAttemptSession, watchSessionProgress } from './session';
|
|
64
64
|
import { applicantMediaCaptured, buildApplicantVerifyRequest } from './submitApplicant';
|
|
65
|
+
import { keptIdType } from '../lib/resubmit';
|
|
65
66
|
|
|
66
67
|
export * from './state';
|
|
67
68
|
export {
|
|
@@ -102,7 +103,10 @@ export function createKycStore(
|
|
|
102
103
|
businessCheck: { ...EMPTY_BUSINESS_CHECK },
|
|
103
104
|
selectedCountry: null,
|
|
104
105
|
countryAutoPicked: false,
|
|
105
|
-
|
|
106
|
+
// A send-back that did not ask for the ID keeps the original one: the
|
|
107
|
+
// ID steps are narrowed out, so the kept ID is selected here instead.
|
|
108
|
+
// A restored session only overwrites it with a saved pick.
|
|
109
|
+
selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
|
|
106
110
|
idNumber: null,
|
|
107
111
|
multiIdSlotIndex: 0,
|
|
108
112
|
multiIdSlots: [],
|
|
@@ -122,6 +126,7 @@ export function createKycStore(
|
|
|
122
126
|
contactChallenge: null,
|
|
123
127
|
business: EMPTY_BUSINESS,
|
|
124
128
|
businessApplication: EMPTY_BUSINESS_APPLICATION,
|
|
129
|
+
supportingDocuments: [],
|
|
125
130
|
applicantKeyPersonId: null,
|
|
126
131
|
keyPeopleInvites: [],
|
|
127
132
|
captureIntegrity: null,
|
|
@@ -419,6 +424,20 @@ export function createKycStore(
|
|
|
419
424
|
}));
|
|
420
425
|
},
|
|
421
426
|
|
|
427
|
+
setSupportingDocument(doc) {
|
|
428
|
+
set((s) => ({
|
|
429
|
+
// One upload per document, like the business slots: re-uploading
|
|
430
|
+
// replaces rather than appends.
|
|
431
|
+
supportingDocuments: [...s.supportingDocuments.filter((d) => d.type !== doc.type), doc],
|
|
432
|
+
}));
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
removeSupportingDocument(type) {
|
|
436
|
+
set((s) => ({
|
|
437
|
+
supportingDocuments: s.supportingDocuments.filter((d) => d.type !== type),
|
|
438
|
+
}));
|
|
439
|
+
},
|
|
440
|
+
|
|
422
441
|
removeBusinessDocument(type) {
|
|
423
442
|
set((s) => ({
|
|
424
443
|
businessApplication: {
|
|
@@ -661,7 +680,9 @@ export function createKycStore(
|
|
|
661
680
|
currentStep: opening,
|
|
662
681
|
selectedCountry: null,
|
|
663
682
|
countryAutoPicked: false,
|
|
664
|
-
|
|
683
|
+
// Same kept ID as at creation: the narrowed flow has no picker to
|
|
684
|
+
// choose it again.
|
|
685
|
+
selectedIdType: keptIdType(config.resubmit) as KycState['selectedIdType'],
|
|
665
686
|
idNumber: null,
|
|
666
687
|
multiIdSlotIndex: 0,
|
|
667
688
|
multiIdSlots: [],
|
package/src/store/session.ts
CHANGED
|
@@ -90,6 +90,17 @@ export function restoreAttemptProgress(
|
|
|
90
90
|
const s = store.getState();
|
|
91
91
|
const d = (progress.data ?? {}) as Record<string, unknown>;
|
|
92
92
|
|
|
93
|
+
// Validate-and-drop: a snapshot written by an older build must degrade to
|
|
94
|
+
// restoring less, never to breaking the flow.
|
|
95
|
+
const restoredDocs = Array.isArray(d['supportingDocuments'])
|
|
96
|
+
? (d['supportingDocuments'] as unknown[]).filter(
|
|
97
|
+
(doc): doc is { type: string; mediaId: string } =>
|
|
98
|
+
!!doc &&
|
|
99
|
+
typeof doc === 'object' &&
|
|
100
|
+
typeof (doc as { type?: unknown }).type === 'string' &&
|
|
101
|
+
typeof (doc as { mediaId?: unknown }).mediaId === 'string',
|
|
102
|
+
)
|
|
103
|
+
: null;
|
|
93
104
|
const app = d['businessApplication'] as
|
|
94
105
|
| (Partial<KycState['businessApplication']> & { keyPeople?: Array<Record<string, unknown>> })
|
|
95
106
|
| undefined;
|
|
@@ -116,6 +127,7 @@ export function restoreAttemptProgress(
|
|
|
116
127
|
...(d['business'] && typeof d['business'] === 'object'
|
|
117
128
|
? { business: { ...s.business, ...(d['business'] as object) } }
|
|
118
129
|
: {}),
|
|
130
|
+
...(restoredDocs ? { supportingDocuments: restoredDocs } : {}),
|
|
119
131
|
...(app
|
|
120
132
|
? {
|
|
121
133
|
businessApplication: {
|
|
@@ -180,6 +192,12 @@ export function progressFromState(s: ReturnType<KycStore['getState']>): Record<s
|
|
|
180
192
|
idNumber: s.idNumber || undefined,
|
|
181
193
|
business: s.business,
|
|
182
194
|
businessApplication: s.businessApplication,
|
|
195
|
+
// The uploads without their preview names: a restored attempt shows the
|
|
196
|
+
// slot as uploaded, which is what the mediaId is for.
|
|
197
|
+
supportingDocuments:
|
|
198
|
+
s.supportingDocuments.length > 0
|
|
199
|
+
? s.supportingDocuments.map((d) => ({ type: d.type, mediaId: d.mediaId }))
|
|
200
|
+
: undefined,
|
|
183
201
|
contact: s.contact,
|
|
184
202
|
questionnaireAnswers: s.questionnaireAnswers,
|
|
185
203
|
address: s.address ?? undefined,
|
package/src/store/state.ts
CHANGED
|
@@ -402,6 +402,24 @@ export interface KycState {
|
|
|
402
402
|
business: BusinessState;
|
|
403
403
|
/** The rest of the KYB application: people, documents, applicant role. */
|
|
404
404
|
businessApplication: BusinessApplicationState;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Supporting documents the person uploaded — artefacts the org holds on
|
|
408
|
+
* file, not the identity evidence this verification is decided on.
|
|
409
|
+
* `fileName` is display only and never rides the wire or session progress.
|
|
410
|
+
*/
|
|
411
|
+
supportingDocuments: Array<{
|
|
412
|
+
type: string;
|
|
413
|
+
mediaId: string;
|
|
414
|
+
fileName?: string;
|
|
415
|
+
/**
|
|
416
|
+
* Local URI of the picked image, for the card's thumbnail. Display only,
|
|
417
|
+
* like `fileName`: it never rides the wire or session progress, so a
|
|
418
|
+
* restored attempt shows the slot uploaded without a preview.
|
|
419
|
+
*/
|
|
420
|
+
previewUri?: string;
|
|
421
|
+
isPdf?: boolean;
|
|
422
|
+
}>;
|
|
405
423
|
/**
|
|
406
424
|
* Set by a KYB submission that requires applicant verification — the
|
|
407
425
|
* KeyPerson id the applicant's own individual check links back to.
|
|
@@ -500,6 +518,14 @@ export interface KycState {
|
|
|
500
518
|
setUboUnidentifiable: (checked: boolean) => void;
|
|
501
519
|
setBusinessDocument: (doc: BusinessDocumentUpload) => void;
|
|
502
520
|
removeBusinessDocument: (type: string) => void;
|
|
521
|
+
setSupportingDocument: (doc: {
|
|
522
|
+
type: string;
|
|
523
|
+
mediaId: string;
|
|
524
|
+
fileName?: string;
|
|
525
|
+
previewUri?: string;
|
|
526
|
+
isPdf?: boolean;
|
|
527
|
+
}) => void;
|
|
528
|
+
removeSupportingDocument: (type: string) => void;
|
|
503
529
|
setApplicant: (role: ApplicantRole, name: string, keyPersonIndex?: number | null) => void;
|
|
504
530
|
setCaptureIntegrity: (integrity: CaptureIntegrity) => void;
|
|
505
531
|
setMrzScan: (scan: MrzScan) => void;
|
package/src/store/submit.ts
CHANGED
|
@@ -99,6 +99,17 @@ export function buildVerifyRequest(
|
|
|
99
99
|
: state.mediaIds,
|
|
100
100
|
...(state.config.workflowId ? { workflowId: state.config.workflowId } : {}),
|
|
101
101
|
...(state.poaDocumentType ? { proofOfAddressType: state.poaDocumentType } : {}),
|
|
102
|
+
// Supporting documents — artefacts held on file. The server validates them
|
|
103
|
+
// against the resolved workflow's request list and drops anything it did
|
|
104
|
+
// not ask for. File names are display only and never ride the wire.
|
|
105
|
+
...(state.supportingDocuments.length > 0
|
|
106
|
+
? {
|
|
107
|
+
supportingDocuments: state.supportingDocuments.map((d) => ({
|
|
108
|
+
type: d.type,
|
|
109
|
+
mediaId: d.mediaId,
|
|
110
|
+
})),
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
102
113
|
// The smart address, when the step gathered one. Sent on individual AND
|
|
103
114
|
// business submissions (a KYB flow's pin is the business premises) — the
|
|
104
115
|
// server validates it against the workflow either way.
|
package/src/types/config.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
NfcConfig,
|
|
23
23
|
PhoneVerificationConfig,
|
|
24
24
|
ProofOfAddressConfig,
|
|
25
|
+
SupportingDocumentsConfig,
|
|
25
26
|
QuestionnaireConfig,
|
|
26
27
|
WorkflowCountry,
|
|
27
28
|
MultiIdConfig,
|
|
@@ -57,6 +58,7 @@ export type KYCStep =
|
|
|
57
58
|
| 'applicant-role'
|
|
58
59
|
| 'liveness'
|
|
59
60
|
| 'proof-of-address'
|
|
61
|
+
| 'supporting-documents'
|
|
60
62
|
// The address flow, in order: find it (search) → confirm it (the PIN step,
|
|
61
63
|
// which keeps the original 'address-collection' wire name so older session
|
|
62
64
|
// progress restores cleanly) → show it (entrance photo) → commit it.
|
|
@@ -142,6 +144,12 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
142
144
|
* mirroring the web SDK.
|
|
143
145
|
*/
|
|
144
146
|
businessName?: string;
|
|
147
|
+
/**
|
|
148
|
+
* The applicant's email, when you already hold it. Not asked for in the flow:
|
|
149
|
+
* it is submitted so your organisation can have the applicant emailed about a
|
|
150
|
+
* decision, for example once their verification is approved.
|
|
151
|
+
*/
|
|
152
|
+
email?: string;
|
|
145
153
|
};
|
|
146
154
|
|
|
147
155
|
/** Enable the live-selfie capture step. */
|
|
@@ -157,6 +165,15 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
157
165
|
*/
|
|
158
166
|
allowDocumentUpload?: boolean;
|
|
159
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Allow photographing the document with the live camera. Default `true`.
|
|
170
|
+
* When `false`, the document step never asks for camera access: the
|
|
171
|
+
* applicant chooses a photo of each side from the device instead. At least
|
|
172
|
+
* one of this and `allowDocumentUpload` stays on, so a config that switches
|
|
173
|
+
* both off keeps the camera.
|
|
174
|
+
*/
|
|
175
|
+
allowDocumentScan?: boolean;
|
|
176
|
+
|
|
160
177
|
/** Enable liveness detection during selfie capture. Default `true`. */
|
|
161
178
|
enableLiveness?: boolean;
|
|
162
179
|
|
|
@@ -231,6 +248,14 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
231
248
|
/** Proof-of-address document check, after capture. */
|
|
232
249
|
proofOfAddress?: ProofOfAddressConfig;
|
|
233
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Supporting documents: artefacts the org holds ON FILE, which are NOT the
|
|
253
|
+
* identity evidence the verification is decided on. The org names each one
|
|
254
|
+
* and writes the guidance under it; the result of any check never changes
|
|
255
|
+
* the verification's own status.
|
|
256
|
+
*/
|
|
257
|
+
supportingDocuments?: SupportingDocumentsConfig;
|
|
258
|
+
|
|
234
259
|
/** Address Intelligence: a map-pin smart address (+ optional door photo and
|
|
235
260
|
* directions), corroborated server-side. KYC AND KYB (premises pin). */
|
|
236
261
|
addressCollection?: AddressCollectionConfig;
|
package/src/types/workflow.ts
CHANGED
|
@@ -24,6 +24,45 @@ export type PoaDocumentType =
|
|
|
24
24
|
* server; read here only to word the step. */
|
|
25
25
|
export type PoaNameRule = 'required' | 'optional' | 'off';
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* One requested supporting document. There is no catalogue: the organisation
|
|
29
|
+
* names its own, so the SDK renders the title and guidance the workflow sent.
|
|
30
|
+
*/
|
|
31
|
+
export interface SupportingDocumentRequest {
|
|
32
|
+
/** The organisation's own slug — the wire `type` this upload submits as. */
|
|
33
|
+
key: string;
|
|
34
|
+
/** The title the applicant reads. A document without one is not asked for. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Guidance under the slot: which document, and what it has to show. */
|
|
37
|
+
description?: string;
|
|
38
|
+
/** Server-side only: what the document is corroborated against. */
|
|
39
|
+
checks?: Array<'id_number' | 'name'>;
|
|
40
|
+
/**
|
|
41
|
+
* The named values the server will read off it. The SDK reads the NAMES
|
|
42
|
+
* alone, to tell the applicant what the document is being taken for; the
|
|
43
|
+
* reading itself is entirely server-side.
|
|
44
|
+
*/
|
|
45
|
+
fields?: Array<{ key: string; label?: string }>;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
/** Which verified IDs it is asked for, as "CC/idType". Absent = every ID. */
|
|
48
|
+
idTypes?: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Offer it on every flow, whatever ID was verified, so `idTypes` decides only
|
|
51
|
+
* who MUST provide it rather than who sees the slot. Absent = the scope hides
|
|
52
|
+
* it from everybody else, which is the default.
|
|
53
|
+
*/
|
|
54
|
+
alwaysAsk?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SupportingDocumentsConfig {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
types?: SupportingDocumentRequest[];
|
|
60
|
+
/** Server-side only. */
|
|
61
|
+
retentionDays?: number;
|
|
62
|
+
/** Server-side only. */
|
|
63
|
+
returnedData?: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
export interface ProofOfAddressConfig {
|
|
28
67
|
/** Adds the Proof of Address step (after capture, before the questionnaire). */
|
|
29
68
|
enabled?: boolean;
|
|
@@ -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,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Info, BadgeCheck, BookUser, Building2, Stamp, Calendar, Camera, CameraOff, ChevronDown,
|
|
3
|
-
ChevronLeft, ChevronRight, Car, Check, CircleAlert, Contact, CreditCard, Fingerprint,
|
|
4
|
-
FileText, FlaskConical, IdCard, Image as ImageIcon, Landmark, Lightbulb, LocateFixed, MapPin,
|
|
5
|
-
MapPinHouse, MapPinCheck, MapPinned, Radar, BellRing, SlidersHorizontal, Minus, Globe, House,
|
|
6
|
-
Mail, Maximize2, Lock, MessageCircle, MessageSquare, Moon, MoveLeft, Nfc, Filter, Pencil,
|
|
7
|
-
PencilLine, Smartphone, RefreshCw, RefreshCcw, ScanFace, ScanLine, Search, Zap, ZapOff,
|
|
8
|
-
CircleDashed, CircleCheck, CircleHelp, CircleX, ShieldCheck, Sun, Timer, Upload, Plus,
|
|
9
|
-
UserRound, UserRoundPlus, Link2, Copy, Share2, UsersRound, VideoOff, Vote, X,
|
|
10
|
-
type LucideIcon
|
|
11
|
-
} from 'lucide-react-native';
|
|
12
|
-
|
|
13
|
-
// Centralised icon set — the SAME Lucide icons the Flutter SDK uses
|
|
14
|
-
// (`lucide_icons_flutter`) and the web SDK uses (`lucide-react`), so the
|
|
15
|
-
// glyphs are pixel-identical across all three platforms. The component that
|
|
16
|
-
// draws them is Icon.tsx (200-line split).
|
|
17
|
-
|
|
18
|
-
export type IconName =
|
|
19
|
-
// chrome
|
|
20
|
-
| 'close'
|
|
21
|
-
| 'back'
|
|
22
|
-
| 'moon'
|
|
23
|
-
| 'sun'
|
|
24
|
-
| 'check'
|
|
25
|
-
| 'lock'
|
|
26
|
-
| 'alert'
|
|
27
|
-
| 'flask'
|
|
28
|
-
| 'refresh'
|
|
29
|
-
| 'refresh-ccw'
|
|
30
|
-
| 'maximize'
|
|
31
|
-
| 'calendar'
|
|
32
|
-
| 'image'
|
|
33
|
-
| 'chevron-left'
|
|
34
|
-
| 'chevron-right'
|
|
35
|
-
| 'chevron-down'
|
|
36
|
-
// consent hero + process steps
|
|
37
|
-
| 'shield'
|
|
38
|
-
| 'badge-check'
|
|
39
|
-
| 'circle-dashed'
|
|
40
|
-
| 'circle-check'
|
|
41
|
-
| 'circle-help'
|
|
42
|
-
| 'circle-x'
|
|
43
|
-
| 'building-2'
|
|
44
|
-
| 'stamp'
|
|
45
|
-
| 'user'
|
|
46
|
-
| 'user-plus'
|
|
47
|
-
| 'plus'
|
|
48
|
-
| 'info'
|
|
49
|
-
| 'users'
|
|
50
|
-
| 'scan-line'
|
|
51
|
-
| 'scan-face'
|
|
52
|
-
| 'nfc'
|
|
53
|
-
| 'filter'
|
|
54
|
-
| 'pencil'
|
|
55
|
-
| 'pencil-line'
|
|
56
|
-
| 'search'
|
|
57
|
-
| 'zap'
|
|
58
|
-
| 'link'
|
|
59
|
-
| 'copy'
|
|
60
|
-
| 'share'
|
|
61
|
-
| 'zap-off'
|
|
62
|
-
| 'timer'
|
|
63
|
-
// id-type glyphs
|
|
64
|
-
| 'fingerprint'
|
|
65
|
-
| 'credit-card'
|
|
66
|
-
| 'globe'
|
|
67
|
-
| 'car'
|
|
68
|
-
| 'vote'
|
|
69
|
-
| 'file-text'
|
|
70
|
-
| 'house'
|
|
71
|
-
| 'landmark'
|
|
72
|
-
| 'id-card'
|
|
73
|
-
| 'contact'
|
|
74
|
-
| 'passport'
|
|
75
|
-
| 'camera'
|
|
76
|
-
| 'camera-off'
|
|
77
|
-
| 'video-off'
|
|
78
|
-
| 'upload'
|
|
79
|
-
| 'x'
|
|
80
|
-
// address collection (map picker + the presence milestone track)
|
|
81
|
-
| 'map-pin'
|
|
82
|
-
| 'map-pin-house'
|
|
83
|
-
| 'map-pin-check'
|
|
84
|
-
| 'map-pinned'
|
|
85
|
-
| 'radar'
|
|
86
|
-
| 'bell-ring'
|
|
87
|
-
| 'sliders'
|
|
88
|
-
| 'locate'
|
|
89
|
-
| 'minus'
|
|
90
|
-
| 'lightbulb'
|
|
91
|
-
// contact verification (OTP channels + step footer)
|
|
92
|
-
| 'mail'
|
|
93
|
-
| 'smartphone'
|
|
94
|
-
| 'message-square'
|
|
95
|
-
| 'message-circle';
|
|
96
|
-
|
|
97
|
-
// Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
|
|
98
|
-
// shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
|
|
99
|
-
// creditCard · globe · car · vote · fileText (+ chrome: X / ArrowLeft /
|
|
100
|
-
// Moon / Sun / Check / Lock / CircleAlert / RefreshCw / Camera).
|
|
101
|
-
export const ICONS: Record<IconName, LucideIcon> = {
|
|
102
|
-
close: X,
|
|
103
|
-
back: MoveLeft, // longer-shaft left arrow, matching Flutter's keyboard_backspace
|
|
104
|
-
moon: Moon,
|
|
105
|
-
sun: Sun,
|
|
106
|
-
check: Check,
|
|
107
|
-
lock: Lock,
|
|
108
|
-
alert: CircleAlert,
|
|
109
|
-
// The environment banner's mark, shared with the web SDK (same lucide glyph)
|
|
110
|
-
// and mirrored by Flutter's Icons.science_outlined.
|
|
111
|
-
flask: FlaskConical,
|
|
112
|
-
refresh: RefreshCw,
|
|
113
|
-
'refresh-ccw': RefreshCcw,
|
|
114
|
-
maximize: Maximize2,
|
|
115
|
-
calendar: Calendar,
|
|
116
|
-
image: ImageIcon,
|
|
117
|
-
'chevron-left': ChevronLeft,
|
|
118
|
-
'chevron-right': ChevronRight,
|
|
119
|
-
'chevron-down': ChevronDown,
|
|
120
|
-
shield: ShieldCheck,
|
|
121
|
-
'badge-check': BadgeCheck,
|
|
122
|
-
'circle-dashed': CircleDashed,
|
|
123
|
-
'circle-check': CircleCheck,
|
|
124
|
-
'circle-help': CircleHelp,
|
|
125
|
-
'circle-x': CircleX,
|
|
126
|
-
'building-2': Building2,
|
|
127
|
-
stamp: Stamp,
|
|
128
|
-
user: UserRound,
|
|
129
|
-
info: Info,
|
|
130
|
-
'user-plus': UserRoundPlus,
|
|
131
|
-
plus: Plus,
|
|
132
|
-
link: Link2,
|
|
133
|
-
copy: Copy,
|
|
134
|
-
share: Share2,
|
|
135
|
-
users: UsersRound,
|
|
136
|
-
'scan-line': ScanLine,
|
|
137
|
-
'scan-face': ScanFace,
|
|
138
|
-
nfc: Nfc,
|
|
139
|
-
filter: Filter,
|
|
140
|
-
pencil: Pencil,
|
|
141
|
-
'pencil-line': PencilLine,
|
|
142
|
-
search: Search,
|
|
143
|
-
zap: Zap,
|
|
144
|
-
'zap-off': ZapOff,
|
|
145
|
-
timer: Timer,
|
|
146
|
-
fingerprint: Fingerprint,
|
|
147
|
-
'credit-card': CreditCard,
|
|
148
|
-
globe: Globe,
|
|
149
|
-
car: Car,
|
|
150
|
-
vote: Vote,
|
|
151
|
-
'file-text': FileText,
|
|
152
|
-
house: House,
|
|
153
|
-
landmark: Landmark,
|
|
154
|
-
'map-pin': MapPin,
|
|
155
|
-
'map-pin-house': MapPinHouse,
|
|
156
|
-
'map-pin-check': MapPinCheck,
|
|
157
|
-
'map-pinned': MapPinned,
|
|
158
|
-
radar: Radar,
|
|
159
|
-
'bell-ring': BellRing,
|
|
160
|
-
sliders: SlidersHorizontal,
|
|
161
|
-
locate: LocateFixed,
|
|
162
|
-
minus: Minus,
|
|
163
|
-
'id-card': IdCard,
|
|
164
|
-
contact: Contact,
|
|
165
|
-
passport: BookUser,
|
|
166
|
-
camera: Camera,
|
|
167
|
-
'camera-off': CameraOff,
|
|
168
|
-
'video-off': VideoOff,
|
|
169
|
-
upload: Upload,
|
|
170
|
-
x: X,
|
|
171
|
-
lightbulb: Lightbulb,
|
|
172
|
-
mail: Mail,
|
|
173
|
-
smartphone: Smartphone,
|
|
174
|
-
'message-square': MessageSquare,
|
|
175
|
-
'message-circle': MessageCircle,
|
|
176
|
-
};
|
|
@@ -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
|
-
}
|