@myazahq/kyc-sdk-react-native 2.3.0 → 2.5.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/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
- package/ios/HybridMyazaFaceDetector.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +1 -2
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +25 -24
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +14 -0
- package/src/components/KycSheet.tsx +39 -140
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +23 -4
- package/src/components/MyazaSelect.tsx +17 -40
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +37 -4
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepView.tsx +26 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +242 -0
- package/src/components/glass/GlassSheet.tsx +50 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- package/src/components/stepHeaderMeta.tsx +3 -1
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
- package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
- package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
- package/src/config/business.ts +6 -6
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +14 -3
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +24 -4
- package/src/config/theme.ts +8 -2
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +5 -0
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +109 -0
- package/src/screens/BusinessDetailsStep.tsx +131 -145
- package/src/screens/BusinessDocumentSlot.tsx +2 -3
- package/src/screens/BusinessDocumentsStep.tsx +1 -1
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +86 -51
- package/src/screens/ConsentStep.tsx +31 -0
- package/src/screens/ContactDestinationField.tsx +1 -0
- package/src/screens/ContactVerificationStep.tsx +34 -3
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessStep.tsx +18 -0
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/ProofOfAddressStep.tsx +6 -3
- package/src/screens/SubmittedStep.tsx +66 -10
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/services/api-types.ts +158 -2
- package/src/services/api-verify-types.ts +53 -1
- package/src/services/api.ts +104 -0
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/workflowGate.ts +1 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +15 -0
- package/src/store/kycStore.ts +241 -14
- package/src/store/serverConfig.ts +2 -0
- package/src/store/session.ts +208 -0
- package/src/store/state.ts +155 -2
- package/src/store/submit.ts +63 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +29 -3
- package/src/types/verification.ts +1 -1
- package/src/types/workflow.ts +15 -0
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
package/src/store/state.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { FlashHole } from '../components/flashHoleGeometry';
|
|
|
9
9
|
|
|
10
10
|
import type { StoreApi } from 'zustand/vanilla';
|
|
11
11
|
|
|
12
|
-
import type { KYCApi, KeyPersonInvite } from '../services/api';
|
|
12
|
+
import type { BusinessCompanyRecord, KYCApi, KeyPersonInvite, RegistryOfficer } from '../services/api';
|
|
13
13
|
import type { IdType, KYCStep, ResolvedKYCConfig } from '../types/config';
|
|
14
14
|
import type { PoaDocumentType } from '../types/workflow';
|
|
15
15
|
import type { ApplicantRole } from '../types/business';
|
|
@@ -35,7 +35,7 @@ export interface KYCMediaIds {
|
|
|
35
35
|
|
|
36
36
|
export interface KYCSubmissionResult {
|
|
37
37
|
verificationId: string;
|
|
38
|
-
status: '
|
|
38
|
+
status: 'processing';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export type DocumentScanPhase = 'front' | 'back' | 'complete';
|
|
@@ -48,12 +48,69 @@ export interface BusinessState {
|
|
|
48
48
|
product: string | null;
|
|
49
49
|
registrationNumber: string;
|
|
50
50
|
registrationName: string;
|
|
51
|
+
/** Dev/sandbox only: pins the canned outcome served instead of calling the
|
|
52
|
+
* register. Sent as metadata.sandboxOutcome; production ignores it. */
|
|
53
|
+
sandboxOutcome: string;
|
|
54
|
+
/** ISO 3166-2 registry region, for the four countries whose register is
|
|
55
|
+
* split by state or emirate. Empty for everywhere else. Rides the search
|
|
56
|
+
* and the selection-time check; the submission relies on the prepaid
|
|
57
|
+
* record, mirroring the web SDK. */
|
|
58
|
+
subdivisionCode: string;
|
|
51
59
|
/** Where key-people invite links are emailed. */
|
|
52
60
|
contactEmail: string;
|
|
53
61
|
address: string;
|
|
54
62
|
email: string;
|
|
55
63
|
phone: string;
|
|
56
64
|
website: string;
|
|
65
|
+
/** Registry facts the applicant states; submitted as their own answer. */
|
|
66
|
+
dateOfIncorporation: string;
|
|
67
|
+
taxId: string;
|
|
68
|
+
vatNumber: string;
|
|
69
|
+
companyType: string;
|
|
70
|
+
natureOfBusiness: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The registry check run when the applicant confirms their company.
|
|
75
|
+
*
|
|
76
|
+
* `skipped` and `limit_reached` are normal outcomes, not failures: the
|
|
77
|
+
* organisation could not be charged (or this application has spent its lookup
|
|
78
|
+
* budget), so the flow carries on and the check happens at submission instead.
|
|
79
|
+
* Mirrors the web SDK's BusinessCheckState — keep the two in lockstep.
|
|
80
|
+
*/
|
|
81
|
+
export interface BusinessCheckState {
|
|
82
|
+
status: 'idle' | 'checking' | 'found' | 'not_found' | 'skipped' | 'unavailable' | 'limit_reached';
|
|
83
|
+
/** What the register holds, when it answered. */
|
|
84
|
+
company: BusinessCompanyRecord | null;
|
|
85
|
+
/** The officers on file — what makes the key-people question a confirmation. */
|
|
86
|
+
officers: RegistryOfficer[];
|
|
87
|
+
/** Which company was checked (normalised), so a changed number re-runs it. */
|
|
88
|
+
checkedNumber: string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Which form fields the REGISTER filled, as opposed to the applicant.
|
|
91
|
+
*
|
|
92
|
+
* Kept so that changing which company this is can clear exactly those and
|
|
93
|
+
* nothing else. Without it, switching company left the previous register's
|
|
94
|
+
* address and email sitting in the form under the new company's name — and
|
|
95
|
+
* because the prefill only writes into empty fields, those leftovers also
|
|
96
|
+
* blocked the new register's real values from ever landing.
|
|
97
|
+
*/
|
|
98
|
+
prefilled: (keyof BusinessState)[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const EMPTY_BUSINESS_CHECK: BusinessCheckState = {
|
|
102
|
+
status: 'idle',
|
|
103
|
+
company: null,
|
|
104
|
+
officers: [],
|
|
105
|
+
checkedNumber: null,
|
|
106
|
+
prefilled: [],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** What `checkBusiness` resolves with. Only a definitive "not on the register"
|
|
110
|
+
* stops the flow; the company record is handed back for the prefill. */
|
|
111
|
+
export interface BusinessCheckResult {
|
|
112
|
+
canContinue: boolean;
|
|
113
|
+
company: BusinessCompanyRecord | null;
|
|
57
114
|
}
|
|
58
115
|
|
|
59
116
|
/** One uploaded supporting document. */
|
|
@@ -83,6 +140,13 @@ export interface BusinessApplicationState {
|
|
|
83
140
|
* one screening, no duplicate invite.
|
|
84
141
|
*/
|
|
85
142
|
applicantKeyPersonIndex: number | null;
|
|
143
|
+
/**
|
|
144
|
+
* The applicant attests that no natural person qualifies as a UBO (public
|
|
145
|
+
* share structures, complex trusts, nominee arrangements) - the FATF
|
|
146
|
+
* fallback. An attestation the server records and the org can branch on,
|
|
147
|
+
* never a verdict; the registry lookup still says what it says.
|
|
148
|
+
*/
|
|
149
|
+
uboUnidentifiable: boolean;
|
|
86
150
|
}
|
|
87
151
|
|
|
88
152
|
export const EMPTY_BUSINESS_APPLICATION: BusinessApplicationState = {
|
|
@@ -91,6 +155,7 @@ export const EMPTY_BUSINESS_APPLICATION: BusinessApplicationState = {
|
|
|
91
155
|
applicantRole: null,
|
|
92
156
|
applicantName: '',
|
|
93
157
|
applicantKeyPersonIndex: null,
|
|
158
|
+
uboUnidentifiable: false,
|
|
94
159
|
};
|
|
95
160
|
|
|
96
161
|
export const EMPTY_BUSINESS: BusinessState = {
|
|
@@ -98,11 +163,18 @@ export const EMPTY_BUSINESS: BusinessState = {
|
|
|
98
163
|
product: null,
|
|
99
164
|
registrationNumber: '',
|
|
100
165
|
registrationName: '',
|
|
166
|
+
sandboxOutcome: '',
|
|
167
|
+
subdivisionCode: '',
|
|
101
168
|
contactEmail: '',
|
|
102
169
|
address: '',
|
|
103
170
|
email: '',
|
|
104
171
|
phone: '',
|
|
105
172
|
website: '',
|
|
173
|
+
dateOfIncorporation: '',
|
|
174
|
+
taxId: '',
|
|
175
|
+
vatNumber: '',
|
|
176
|
+
companyType: '',
|
|
177
|
+
natureOfBusiness: '',
|
|
106
178
|
};
|
|
107
179
|
|
|
108
180
|
export interface ContactState {
|
|
@@ -112,6 +184,16 @@ export interface ContactState {
|
|
|
112
184
|
emailToken?: string;
|
|
113
185
|
/** Single-use proof from a passed phone check. */
|
|
114
186
|
phoneToken?: string;
|
|
187
|
+
/**
|
|
188
|
+
* Channels whose proof the SERVER refused at submit (422
|
|
189
|
+
* contact_verification_required). Proofs are single-use and expire ~30
|
|
190
|
+
* minutes after the OTP check, but they ride session progress and are
|
|
191
|
+
* restored on resume — so a resumed attempt can carry a dead proof while
|
|
192
|
+
* the step still shows "verified". This routes the person back to
|
|
193
|
+
* re-verify instead of a retry that resubmits the same dead token forever;
|
|
194
|
+
* setContactVerified clears its channel.
|
|
195
|
+
*/
|
|
196
|
+
expired?: Array<'email' | 'phone'>;
|
|
115
197
|
}
|
|
116
198
|
|
|
117
199
|
/** Document-capture sub-phase — drives the sheet header title/description. */
|
|
@@ -120,11 +202,47 @@ export type DocumentCapturePhase = 'front' | 'front-preview' | 'back' | 'review'
|
|
|
120
202
|
/** The mediaIds keys settable via `setMediaId`. */
|
|
121
203
|
export type MediaIdKey = keyof KYCMediaIds;
|
|
122
204
|
|
|
205
|
+
/** One committed multi-ID check. `documentFront/Back` are mediaIds; the
|
|
206
|
+
* `*Image` fields are LOCAL previews and never reach the wire. */
|
|
207
|
+
export interface MultiIdSlot {
|
|
208
|
+
idType: IdType;
|
|
209
|
+
idNumber?: string;
|
|
210
|
+
documentFront?: string;
|
|
211
|
+
documentBack?: string;
|
|
212
|
+
/** Each check records its OWN document capture. The row's flat
|
|
213
|
+
* documentFrontVideo column holds one, so a multi-ID run keeps them per
|
|
214
|
+
* check or loses all but one. */
|
|
215
|
+
documentFrontVideo?: string;
|
|
216
|
+
documentBackVideo?: string;
|
|
217
|
+
/** This check's own chip read. The chip belongs to a PARTICULAR document, so
|
|
218
|
+
* sending it top-level attributed it to the primary check — which is how a
|
|
219
|
+
* passport's chip read was dropped for being submitted alongside a BVN. */
|
|
220
|
+
chipData?: EmrtdReadResult | null;
|
|
221
|
+
documentFrontImage?: string;
|
|
222
|
+
documentBackImage?: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
123
225
|
export interface KycState {
|
|
124
226
|
config: ResolvedKYCConfig;
|
|
125
227
|
api: KYCApi;
|
|
126
228
|
|
|
127
229
|
currentStep: KYCStep;
|
|
230
|
+
/**
|
|
231
|
+
* The attempt SESSION this run is recorded under (`/session/start`). Null
|
|
232
|
+
* when minting failed or preview — verifying is never conditional on it. It
|
|
233
|
+
* rides the /verify body so the verification adopts the session's id, and it
|
|
234
|
+
* is what the registry check at selection anchors its charge on.
|
|
235
|
+
*/
|
|
236
|
+
sessionId: string | null;
|
|
237
|
+
/** The session's own hosted web page (see SessionStartResponse.url). */
|
|
238
|
+
sessionUrl: string | null;
|
|
239
|
+
/**
|
|
240
|
+
* What the register said about the company the applicant identified — the
|
|
241
|
+
* paid check run at SELECTION (`/business/select`), so the officer list is
|
|
242
|
+
* already here by the time the key-people step asks for it. `officers` is
|
|
243
|
+
* the prefill's input; `checkedNumber` stops a re-check of the same company.
|
|
244
|
+
*/
|
|
245
|
+
businessCheck: BusinessCheckState;
|
|
128
246
|
/**
|
|
129
247
|
* The country picked on the multi-region country-select step. Null on a
|
|
130
248
|
* single-country flow, where `config.country` is the answer.
|
|
@@ -132,6 +250,22 @@ export interface KycState {
|
|
|
132
250
|
selectedCountry: string | null;
|
|
133
251
|
selectedIdType: IdType | null;
|
|
134
252
|
idNumber: string | null;
|
|
253
|
+
/**
|
|
254
|
+
* Multi-ID: which check the applicant is on (0-based), and the ones already
|
|
255
|
+
* committed. A committed slot keeps its LOCAL preview images as well as its
|
|
256
|
+
* mediaIds, so stepping back into it restores what was captured rather than
|
|
257
|
+
* asking for a document that is still perfectly good.
|
|
258
|
+
*/
|
|
259
|
+
multiIdSlotIndex: number;
|
|
260
|
+
multiIdSlots: MultiIdSlot[];
|
|
261
|
+
/**
|
|
262
|
+
* Capture previews handed back when stepping BACK into a committed slot.
|
|
263
|
+
*
|
|
264
|
+
* RN keeps document previews in the capture screen's own state, so a restored
|
|
265
|
+
* slot has to hand them somewhere the remounting screen can read them. Null
|
|
266
|
+
* on the forward journey.
|
|
267
|
+
*/
|
|
268
|
+
multiIdRestored: { front?: string; back?: string } | null;
|
|
135
269
|
mediaIds: KYCMediaIds;
|
|
136
270
|
submissionResult: KYCSubmissionResult | null;
|
|
137
271
|
serverConfig: ServerConfigState;
|
|
@@ -219,13 +353,32 @@ export interface KycState {
|
|
|
219
353
|
setCountry: (country: string) => void;
|
|
220
354
|
setIdType: (idType: IdType) => void;
|
|
221
355
|
setIdNumber: (idNumber: string) => void;
|
|
356
|
+
/** Commit the current slot's evidence and move to the next check. The
|
|
357
|
+
* previews come from the capture screen, which owns them. */
|
|
358
|
+
commitMultiIdSlot: (nextStep: KYCStep, previews?: { front?: string; back?: string }) => void;
|
|
359
|
+
/** Step BACK into the previous slot, restoring what it captured. */
|
|
360
|
+
uncommitMultiIdSlot: (step: KYCStep) => void;
|
|
222
361
|
setMediaId: (key: MediaIdKey, mediaId: string) => void;
|
|
223
362
|
setDocumentMediaId: (mediaId: string, side: 'front' | 'back') => void;
|
|
224
363
|
setQuestionnaireAnswer: (key: string, value: QuestionnaireAnswerValue | undefined) => void;
|
|
225
364
|
setContactVerified: (channel: 'email' | 'phone', destination: string, token: string) => void;
|
|
226
365
|
setContactDestination: (channel: 'email' | 'phone', destination: string) => void;
|
|
366
|
+
/** The server refused these channels' proofs at submit — drop the tokens and
|
|
367
|
+
* flag the channels so their steps re-verify then resubmit. */
|
|
368
|
+
clearContactProofs: (channels: Array<'email' | 'phone'>) => void;
|
|
227
369
|
setBusinessField: <K extends keyof BusinessState>(key: K, value: BusinessState[K]) => void;
|
|
370
|
+
/** Writes the register's answers into empty fields + records which ones it
|
|
371
|
+
* filled, in one set — so a company change can clear exactly those. */
|
|
372
|
+
applyBusinessPrefill: (
|
|
373
|
+
patch: Partial<BusinessState>,
|
|
374
|
+
prefilled: (keyof BusinessState)[],
|
|
375
|
+
) => void;
|
|
376
|
+
setSessionId: (sessionId: string, sessionUrl?: string | null) => void;
|
|
377
|
+
/** Run the paid registry check for the typed company. Never blocks the flow. */
|
|
378
|
+
checkBusiness: () => Promise<BusinessCheckResult>;
|
|
228
379
|
setKeyPeople: (rows: KeyPersonEntry[]) => void;
|
|
380
|
+
/** The UBO-exemption attestation (FATF fallback) — see BusinessApplicationState. */
|
|
381
|
+
setUboUnidentifiable: (checked: boolean) => void;
|
|
229
382
|
setBusinessDocument: (doc: BusinessDocumentUpload) => void;
|
|
230
383
|
removeBusinessDocument: (type: string) => void;
|
|
231
384
|
setApplicant: (role: ApplicantRole, name: string, keyPersonIndex?: number | null) => void;
|
package/src/store/submit.ts
CHANGED
|
@@ -15,12 +15,39 @@
|
|
|
15
15
|
import { collectDeviceMetadata } from '../services/deviceMetadata';
|
|
16
16
|
import { generateRequestId } from '../utils/uuid';
|
|
17
17
|
import { hasActiveQuestionnaire, questionnairePayload } from '../config/questionnaire';
|
|
18
|
+
import { multiIdWireSlots } from '../lib/multi-id';
|
|
18
19
|
import { isBusinessFlow } from '../config/business';
|
|
19
20
|
import { businessSubmission, effectiveCountry } from './derive';
|
|
20
21
|
import type { ClientFingerprint } from '../services/fingerprint';
|
|
21
22
|
import type { VerifyRequest } from '../services/api';
|
|
22
23
|
import type { KycState } from './state';
|
|
23
24
|
|
|
25
|
+
/** The chip payload as the wire wants it. One builder, so a slot's chip and a
|
|
26
|
+
* single-ID run's are byte-identical to the server. */
|
|
27
|
+
export function nfcPayload(
|
|
28
|
+
chip: NonNullable<KycState['chipData']>,
|
|
29
|
+
): NonNullable<VerifyRequest['nfc']> {
|
|
30
|
+
return {
|
|
31
|
+
dg1: chip.dg1,
|
|
32
|
+
...(chip.sod ? { sod: chip.sod } : {}),
|
|
33
|
+
...(chip.dg2 ? { dg2: chip.dg2 } : {}),
|
|
34
|
+
...(chip.dg7 ? { dg7: chip.dg7 } : {}),
|
|
35
|
+
...(chip.dg11 ? { dg11: chip.dg11 } : {}),
|
|
36
|
+
...(chip.dg12 ? { dg12: chip.dg12 } : {}),
|
|
37
|
+
...(chip.dg15 ? { dg15: chip.dg15 } : {}),
|
|
38
|
+
...(chip.aaSignature ? { aaSignature: chip.aaSignature } : {}),
|
|
39
|
+
...(chip.aaChallengeId ? { aaChallengeId: chip.aaChallengeId } : {}),
|
|
40
|
+
chipAuth: chip.chipAuth,
|
|
41
|
+
// The PACE diagnostic. Read on every session and, until now, dropped here:
|
|
42
|
+
// every RN chip read reached the server with paceOutcome null, so the one
|
|
43
|
+
// question the field exists to answer — did this chip decline PACE, or did
|
|
44
|
+
// ours fail? — was unanswerable for the whole install base. Flutter has
|
|
45
|
+
// always sent it.
|
|
46
|
+
...(chip.paceOutcome ? { paceOutcome: chip.paceOutcome } : {}),
|
|
47
|
+
...(chip.paceDetail ? { paceDetail: chip.paceDetail } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
export function buildVerifyRequest(
|
|
25
52
|
state: KycState,
|
|
26
53
|
fingerprint: ClientFingerprint | undefined,
|
|
@@ -30,15 +57,38 @@ export function buildVerifyRequest(
|
|
|
30
57
|
// capture media at all.
|
|
31
58
|
const business = isBusinessFlow(state.config) ? businessSubmission(state) : null;
|
|
32
59
|
|
|
60
|
+
// Multi-ID: every check was committed as a slot, and the whole run submits as
|
|
61
|
+
// ONE verification the server judges by the workflow's pass policy. The FIRST
|
|
62
|
+
// slot fills the single-ID fields, so anything reading a verification's own
|
|
63
|
+
// idType/idNumber keeps one meaning.
|
|
64
|
+
const multiSlots = !business && state.multiIdSlots.length >= 2 ? state.multiIdSlots : null;
|
|
65
|
+
const primary = multiSlots?.[0];
|
|
66
|
+
|
|
33
67
|
return {
|
|
34
68
|
country: business ? business.country : effectiveCountry(state),
|
|
35
|
-
idType: business ? business.product : (state.selectedIdType ?? ''),
|
|
36
|
-
idNumber: business ? undefined : (state.idNumber ?? undefined),
|
|
69
|
+
idType: business ? business.product : (primary?.idType ?? state.selectedIdType ?? ''),
|
|
70
|
+
idNumber: business ? undefined : (primary?.idNumber ?? state.idNumber ?? undefined),
|
|
71
|
+
...(multiSlots
|
|
72
|
+
? {
|
|
73
|
+
idChecks: multiIdWireSlots(multiSlots).map((wire, i) => {
|
|
74
|
+
const chip = multiSlots[i]?.chipData;
|
|
75
|
+
return chip ? { ...wire, nfc: nfcPayload(chip) } : wire;
|
|
76
|
+
}),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
37
79
|
...(business ? { business: business.payload } : {}),
|
|
38
80
|
// The org's user reference (becomes Entity.externalUserId at the KYC seam).
|
|
39
81
|
...(state.config.userId ? { userId: state.config.userId } : {}),
|
|
40
82
|
userData: business ? undefined : state.config.userData,
|
|
41
|
-
|
|
83
|
+
// Multi-ID: the slot documents ride idChecks; only the RUN-level media (the
|
|
84
|
+
// one selfie and its video) sit at the top level. Sending a slot's document
|
|
85
|
+
// here too would file the last ID's capture as though it were the
|
|
86
|
+
// verification's own.
|
|
87
|
+
mediaIds: business
|
|
88
|
+
? {}
|
|
89
|
+
: multiSlots
|
|
90
|
+
? { ...state.mediaIds, documentFront: undefined, documentBack: undefined }
|
|
91
|
+
: state.mediaIds,
|
|
42
92
|
...(state.config.workflowId ? { workflowId: state.config.workflowId } : {}),
|
|
43
93
|
...(state.poaDocumentType ? { proofOfAddressType: state.poaDocumentType } : {}),
|
|
44
94
|
...(state.contact.emailToken || state.contact.phoneToken
|
|
@@ -60,16 +110,9 @@ export function buildVerifyRequest(
|
|
|
60
110
|
// Omitted entirely when absent rather than sent empty: the server treats a
|
|
61
111
|
// present-but-hollow block as a failed read, which is not the same as a
|
|
62
112
|
// document whose chip was never scanned.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
dg1: state.chipData.dg1,
|
|
67
|
-
...(state.chipData.sod ? { sod: state.chipData.sod } : {}),
|
|
68
|
-
...(state.chipData.dg2 ? { dg2: state.chipData.dg2 } : {}),
|
|
69
|
-
chipAuth: state.chipData.chipAuth,
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
: {}),
|
|
113
|
+
// Multi-ID: the chip rides its OWN check (above) — a top-level payload could
|
|
114
|
+
// only ever be attributed to the primary one.
|
|
115
|
+
...(!multiSlots && state.chipData ? { nfc: nfcPayload(state.chipData) } : {}),
|
|
73
116
|
// Only sent when the flow actually asked — an inactive questionnaire
|
|
74
117
|
// must not put an empty object on the verification record.
|
|
75
118
|
...(hasActiveQuestionnaire(state.config.questionnaire)
|
|
@@ -80,12 +123,19 @@ export function buildVerifyRequest(
|
|
|
80
123
|
),
|
|
81
124
|
}
|
|
82
125
|
: {}),
|
|
126
|
+
// The attempt session this run happened under: the verification adopts its
|
|
127
|
+
// id, and any registry check paid at selection is not paid again at submit.
|
|
128
|
+
...(state.sessionId ? { sessionId: state.sessionId } : {}),
|
|
83
129
|
// `metadata` is free-form passthrough. The SDK-owned keys (`requestId` —
|
|
84
130
|
// the server's idempotency key — and `device`) are written AFTER the
|
|
85
131
|
// caller's metadata so consumer keys can never clobber them. The user
|
|
86
132
|
// reference is the top-level `userId` above. Identical on web + Flutter.
|
|
87
133
|
metadata: {
|
|
88
134
|
...(state.config.metadata ?? {}),
|
|
135
|
+
// Ignored by production, so it is safe to send whenever it is set.
|
|
136
|
+
...(business && state.business.sandboxOutcome
|
|
137
|
+
? { sandboxOutcome: state.business.sandboxOutcome }
|
|
138
|
+
: {}),
|
|
89
139
|
requestId: generateRequestId(),
|
|
90
140
|
device: {
|
|
91
141
|
...(collectDeviceMetadata() as unknown as Record<string, unknown>),
|
|
@@ -14,6 +14,7 @@ import { collectDeviceMetadata } from '../services/deviceMetadata';
|
|
|
14
14
|
import { generateRequestId } from '../utils/uuid';
|
|
15
15
|
import { splitFullName } from '../config/keyPeople';
|
|
16
16
|
import { effectiveCountry } from './derive';
|
|
17
|
+
import { nfcPayload } from './submit';
|
|
17
18
|
import type { ClientFingerprint } from '../services/fingerprint';
|
|
18
19
|
import type { VerifyRequest } from '../services/api';
|
|
19
20
|
import type { KycState } from './state';
|
|
@@ -53,17 +54,10 @@ export function buildApplicantVerifyRequest(
|
|
|
53
54
|
...(userData ? { userData } : {}),
|
|
54
55
|
mediaIds: state.mediaIds,
|
|
55
56
|
// The chip read, when the leg ran the NFC step (an overlaid applicant
|
|
56
|
-
// workflow can enable it).
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
dg1: state.chipData.dg1,
|
|
61
|
-
...(state.chipData.sod ? { sod: state.chipData.sod } : {}),
|
|
62
|
-
...(state.chipData.dg2 ? { dg2: state.chipData.dg2 } : {}),
|
|
63
|
-
chipAuth: state.chipData.chipAuth,
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
: {}),
|
|
57
|
+
// workflow can enable it). Built by the SHARED builder, not a second copy
|
|
58
|
+
// of the block: a hand-rolled twin drifts silently, and the applicant's
|
|
59
|
+
// chip must reach the server in the same shape as everyone else's.
|
|
60
|
+
...(state.chipData ? { nfc: nfcPayload(state.chipData) } : {}),
|
|
67
61
|
// NO top-level `userId` here on purpose: the server prefers it over
|
|
68
62
|
// metadata.userId, so sending the org's own user ref would sever the
|
|
69
63
|
// KeyPerson link this submission exists to make.
|
package/src/types/business.ts
CHANGED
|
@@ -17,8 +17,21 @@ export type KeyPersonRole = 'director' | 'beneficial_owner' | 'signatory' | 'sha
|
|
|
17
17
|
* plus 'authorized_representative' (someone filing on the company's behalf). */
|
|
18
18
|
export type ApplicantRole = KeyPersonRole | 'authorized_representative';
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/** Company-profile fields a KYB workflow can ask for. The last five are
|
|
21
|
+
* registry facts the applicant STATES (asked as their own answer rather than
|
|
22
|
+
* filled from the register, because where the two differ that is the
|
|
23
|
+
* finding). Mirrors the web SDK's CompanyInfoField exactly. */
|
|
24
|
+
export const COMPANY_INFO_FIELDS = [
|
|
25
|
+
'address',
|
|
26
|
+
'email',
|
|
27
|
+
'phone',
|
|
28
|
+
'website',
|
|
29
|
+
'dateOfIncorporation',
|
|
30
|
+
'taxId',
|
|
31
|
+
'vatNumber',
|
|
32
|
+
'companyType',
|
|
33
|
+
'natureOfBusiness',
|
|
34
|
+
] as const;
|
|
22
35
|
export type CompanyInfoField = (typeof COMPANY_INFO_FIELDS)[number];
|
|
23
36
|
export type CompanyInfoMode = 'off' | 'optional' | 'required';
|
|
24
37
|
|
|
@@ -55,12 +68,20 @@ export interface WorkflowKeyPeopleConfig {
|
|
|
55
68
|
requirement?: 'all_in_scope' | 'ubos_only' | 'advisory';
|
|
56
69
|
/** Per-role verification depth overrides (win over `level`). */
|
|
57
70
|
perRole?: Partial<Record<KeyPersonRole, KeyPeopleLevel>>;
|
|
71
|
+
/** Emails are mandatory for the roles that are sent a verification link. */
|
|
72
|
+
requireEmail?: boolean;
|
|
73
|
+
/** Explicit override of WHICH roles must carry an email. */
|
|
74
|
+
requireEmailRoles?: KeyPersonRole[];
|
|
58
75
|
/** Invite distribution for full-KYC people. */
|
|
59
76
|
invite?: {
|
|
60
77
|
channel?: string;
|
|
61
78
|
/** The KYC workflow the per-person invite links run through. */
|
|
62
79
|
workflowId?: string;
|
|
63
80
|
};
|
|
81
|
+
/** Nested KYB: a corporate shareholder is invited into its OWN business
|
|
82
|
+
* application (the server mints the link; the SDK only tells the applicant
|
|
83
|
+
* the truth about what happens to a company they list). */
|
|
84
|
+
corporateKyb?: { enabled?: boolean; workflowId?: string };
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
/**
|
package/src/types/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResubmitConfig } from '../lib/resubmit';
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// The SDK's public configuration.
|
|
3
4
|
//
|
|
@@ -23,6 +24,7 @@ import type {
|
|
|
23
24
|
ProofOfAddressConfig,
|
|
24
25
|
QuestionnaireConfig,
|
|
25
26
|
WorkflowCountry,
|
|
27
|
+
MultiIdConfig,
|
|
26
28
|
} from './workflow';
|
|
27
29
|
|
|
28
30
|
export type * from './workflow';
|
|
@@ -62,7 +64,7 @@ export type KYCStep =
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
|
|
64
66
|
/** How flow progress is drawn — see {@link MyazaKYCConfig.progressStyle}. */
|
|
65
|
-
export type ProgressStyle = 'steps' | 'bar';
|
|
67
|
+
export type ProgressStyle = 'steps' | 'bar' | 'none';
|
|
66
68
|
|
|
67
69
|
export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
68
70
|
/**
|
|
@@ -155,6 +157,12 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
155
157
|
* reflection flash, or both. Workflow-driven.
|
|
156
158
|
*/
|
|
157
159
|
livenessMode?: LivenessMode;
|
|
160
|
+
/**
|
|
161
|
+
* KYB success screen: after submitting with key people still to verify,
|
|
162
|
+
* tapping Done offers the session's own web page so the applicant can reach
|
|
163
|
+
* the invite links again after the app closes. Default true.
|
|
164
|
+
*/
|
|
165
|
+
keyPeopleLinkRecovery?: boolean;
|
|
158
166
|
|
|
159
167
|
/** Colours in the flash sequence (2–5, default 4). Flash modes only. */
|
|
160
168
|
flashSequenceLength?: number;
|
|
@@ -175,6 +183,10 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
175
183
|
*/
|
|
176
184
|
countries?: WorkflowCountry[];
|
|
177
185
|
|
|
186
|
+
/** Multi-ID policy: several ID checks in one run, judged by a pass policy.
|
|
187
|
+
* KYC only — publish rejects it on a KYB workflow. */
|
|
188
|
+
multiId?: MultiIdConfig;
|
|
189
|
+
|
|
178
190
|
/** Email OTP possession check, right after consent. */
|
|
179
191
|
emailVerification?: EmailVerificationConfig;
|
|
180
192
|
|
|
@@ -187,6 +199,15 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
187
199
|
/** Compliance declarations asked just before submission. */
|
|
188
200
|
questionnaire?: QuestionnaireConfig;
|
|
189
201
|
|
|
202
|
+
/**
|
|
203
|
+
* A reviewer sent this attempt back to redo specific steps.
|
|
204
|
+
*
|
|
205
|
+
* Never set by a consumer and never part of a published workflow — it is
|
|
206
|
+
* stamped onto ONE session's config snapshot when somebody clicks "Send back".
|
|
207
|
+
* Absent means the ordinary full flow.
|
|
208
|
+
*/
|
|
209
|
+
resubmit?: ResubmitConfig;
|
|
210
|
+
|
|
190
211
|
/** eMRTD chip read for chip-capable documents. */
|
|
191
212
|
nfc?: NfcConfig;
|
|
192
213
|
|
|
@@ -215,8 +236,13 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
215
236
|
* • `'bar'` — a single thin bar pinned to the bottom edge of the header.
|
|
216
237
|
* Quieter, and unaffected by step count, so it suits long flows and hosts
|
|
217
238
|
* who would rather the chrome said less.
|
|
239
|
+
* • `'none'` — no progress in the header at all. For hosts whose own
|
|
240
|
+
* surface already communicates progress, or short flows where a
|
|
241
|
+
* step count is more noise than reassurance. The header keeps its
|
|
242
|
+
* brand row and controls; only the progress element is dropped.
|
|
218
243
|
*
|
|
219
|
-
*
|
|
244
|
+
* `'steps'` and `'bar'` convey the same fraction; the choice is how much room
|
|
245
|
+
* it takes. `'none'` opts out of conveying it here.
|
|
220
246
|
*/
|
|
221
247
|
progressStyle?: ProgressStyle;
|
|
222
248
|
|
|
@@ -250,7 +276,7 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
|
|
|
250
276
|
onStepChange?: (step: KYCStep) => void;
|
|
251
277
|
/**
|
|
252
278
|
* Fires immediately after the user submits. The submission is always
|
|
253
|
-
* status: '
|
|
279
|
+
* status: 'processing' — results arrive async via webhook.
|
|
254
280
|
*/
|
|
255
281
|
onSubmit?: (submission: KYCSubmission) => void;
|
|
256
282
|
onClose?: () => void;
|
package/src/types/workflow.ts
CHANGED
|
@@ -141,11 +141,26 @@ export interface WorkflowCountry {
|
|
|
141
141
|
idTypes?: string[];
|
|
142
142
|
/** Per-ID validation toggles — restrict-only; they never widen a grant. */
|
|
143
143
|
idOptions?: Record<string, WorkflowIdOption>;
|
|
144
|
+
/** Multi-ID: which IDs THIS country offers for each verification in the run.
|
|
145
|
+
* A pinned slot keeps its list; an absent entry offers everything. */
|
|
146
|
+
multiIdSlots?: Array<{ idTypes?: string[] }>;
|
|
144
147
|
govDbCheck?: boolean;
|
|
145
148
|
documentIntelligence?: boolean;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// ── Liveness ────────────────────────────────────────────────────────────────
|
|
149
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Multi-ID: several ID checks in ONE run, one selfie, one verification. The
|
|
155
|
+
* POLICY is workflow-level; WHICH IDs each verification offers is per country
|
|
156
|
+
* (`WorkflowCountry.multiIdSlots`), so multi-region flows work.
|
|
157
|
+
*/
|
|
158
|
+
export interface MultiIdConfig {
|
|
159
|
+
/** How many IDs the applicant completes (2–3). */
|
|
160
|
+
count: number;
|
|
161
|
+
/** How many must pass for the verification to be VERIFIED. */
|
|
162
|
+
minPassed: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
/** Gestures (default), screen-reflection flash, or both. */
|
|
151
166
|
export type LivenessMode = 'gestures' | 'flash' | 'both';
|