@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
|
@@ -47,9 +47,32 @@ export interface KeyPersonInvite {
|
|
|
47
47
|
inviteUrl: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The one status vocabulary, shared by `GET /api/kyc/status/:id`, the
|
|
52
|
+
* secret-key result route and every verification webhook.
|
|
53
|
+
*
|
|
54
|
+
* `status` is what happened; `checkStatus` beside it is what the CHECKS found.
|
|
55
|
+
* They differ when a person overrode the automated result, which is the case
|
|
56
|
+
* worth being able to see: `approved` with `checkStatus: 'failed'` means
|
|
57
|
+
* somebody accepted the applicant despite a failed check, and the reason says
|
|
58
|
+
* what they accepted them despite.
|
|
59
|
+
*/
|
|
60
|
+
export type SessionStatus =
|
|
61
|
+
| 'not_started'
|
|
62
|
+
| 'in_progress'
|
|
63
|
+
| 'processing'
|
|
64
|
+
| 'in_review'
|
|
65
|
+
| 'awaiting_resubmission'
|
|
66
|
+
| 'approved'
|
|
67
|
+
| 'declined'
|
|
68
|
+
| 'abandoned'
|
|
69
|
+
| 'expired'
|
|
70
|
+
| 'error';
|
|
71
|
+
|
|
50
72
|
export interface VerifyResponse {
|
|
51
73
|
verificationId: string;
|
|
52
|
-
|
|
74
|
+
/** Always `processing`: accepted, checks running. */
|
|
75
|
+
status: 'processing';
|
|
53
76
|
/**
|
|
54
77
|
* KYB with applicant verification: the KeyPerson row the applicant's OWN
|
|
55
78
|
* identity check must link back to, via `metadata.userId` on a second,
|
|
@@ -66,7 +89,9 @@ export interface VerifyResponse {
|
|
|
66
89
|
*/
|
|
67
90
|
export interface VerificationStatusResponse {
|
|
68
91
|
verificationId: string;
|
|
69
|
-
status:
|
|
92
|
+
status: SessionStatus;
|
|
93
|
+
/** What the CHECKS found, unchanged by any later decision. */
|
|
94
|
+
checkStatus?: 'pending' | 'verified' | 'failed' | 'not_found' | 'error';
|
|
70
95
|
reason?: string | null;
|
|
71
96
|
reasonCode?: string | null;
|
|
72
97
|
createdAt: string;
|
|
@@ -109,6 +134,14 @@ export interface SdkConfigResponse {
|
|
|
109
134
|
environment: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
|
|
110
135
|
idTypes: SdkConfigIdType[];
|
|
111
136
|
branding?: SdkConfigBranding;
|
|
137
|
+
/**
|
|
138
|
+
* The visitor's country, resolved from their IP.
|
|
139
|
+
*
|
|
140
|
+
* A GUESS and only ever a DEFAULT — nothing branches on it and it never
|
|
141
|
+
* reaches a verification. Deliberately not evidence: device intelligence
|
|
142
|
+
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
143
|
+
*/
|
|
144
|
+
geoCountry?: string | null;
|
|
112
145
|
}
|
|
113
146
|
|
|
114
147
|
/**
|
|
@@ -141,6 +174,14 @@ export interface WorkflowResolutionResponse {
|
|
|
141
174
|
/** Org allowlist + per-ID feature flags (same shape as /config). */
|
|
142
175
|
idTypes: SdkConfigIdType[];
|
|
143
176
|
branding?: SdkConfigBranding;
|
|
177
|
+
/**
|
|
178
|
+
* The visitor's country, resolved from their IP.
|
|
179
|
+
*
|
|
180
|
+
* A GUESS and only ever a DEFAULT — nothing branches on it and it never
|
|
181
|
+
* reaches a verification. Deliberately not evidence: device intelligence
|
|
182
|
+
* carries the same lookup as a RISK signal, and the two must not be confused.
|
|
183
|
+
*/
|
|
184
|
+
geoCountry?: string | null;
|
|
144
185
|
/** KYB only: the mapped applicant workflow, when configured and resolvable. */
|
|
145
186
|
applicantWorkflow?: ApplicantWorkflowPayload | null;
|
|
146
187
|
}
|
|
@@ -157,8 +198,123 @@ export interface ContactCheckResponse {
|
|
|
157
198
|
token: string;
|
|
158
199
|
}
|
|
159
200
|
|
|
201
|
+
/**
|
|
202
|
+
* A server-issued Active-Authentication challenge. `challenge` is base64 of the
|
|
203
|
+
* 8 bytes handed to the chip; `challengeId` is what rides the submission so the
|
|
204
|
+
* server can spend it (single-use — a replayed one is refused).
|
|
205
|
+
*/
|
|
206
|
+
export interface NfcChallengeResponse {
|
|
207
|
+
challengeId: string;
|
|
208
|
+
challenge: string;
|
|
209
|
+
expiresAt: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
160
212
|
export interface HealthResponse {
|
|
161
213
|
status: string;
|
|
162
214
|
}
|
|
163
215
|
|
|
164
216
|
// The mime types the server accepts (must mirror the server's upload allowlist).
|
|
217
|
+
|
|
218
|
+
/** `POST /session/start` — mint (or resume) an attempt session. */
|
|
219
|
+
export interface SessionStartResponse {
|
|
220
|
+
sessionId: string;
|
|
221
|
+
expiresAt: string;
|
|
222
|
+
resumed: boolean;
|
|
223
|
+
/** The session's own hosted web page. After a KYB submission it is the
|
|
224
|
+
* rehydrated success screen with every key person's invite link — the
|
|
225
|
+
* applicant's way back to those links once the app closes. */
|
|
226
|
+
url?: string;
|
|
227
|
+
/** Where the user got to, when resuming. Media references are already
|
|
228
|
+
* pruned server-side of anything that has since expired. */
|
|
229
|
+
progress?: {
|
|
230
|
+
step?: string;
|
|
231
|
+
mediaIds?: Record<string, string>;
|
|
232
|
+
data?: Record<string, unknown>;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** One person a submitted KYB application is still waiting on (the server's
|
|
237
|
+
* reconciled view — registry discovery can add people the applicant never
|
|
238
|
+
* listed, so this list supersedes the submit-time invites). */
|
|
239
|
+
export interface AwaitingPersonPayload {
|
|
240
|
+
id: string;
|
|
241
|
+
name: string;
|
|
242
|
+
role: string;
|
|
243
|
+
ownershipPct: number | null;
|
|
244
|
+
/** ISO-2, or null when the register gave free text no flag matches. */
|
|
245
|
+
country: string | null;
|
|
246
|
+
status: 'verified' | 'failed' | 'submitted' | 'pending' | 'not_needed';
|
|
247
|
+
/** Null once their check is done, or when they never needed one. */
|
|
248
|
+
inviteUrl: string | null;
|
|
249
|
+
isApplicant: boolean;
|
|
250
|
+
/** A company completes a KYB application, not a KYC - the list labels it so. */
|
|
251
|
+
isCorporate?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `GET /session/:sessionId/summary` — a submitted session, rebuilt server-side. */
|
|
255
|
+
export interface SessionSummaryResponse {
|
|
256
|
+
status: 'completed';
|
|
257
|
+
/** False while registry discovery is still reconciling the people list. */
|
|
258
|
+
keyPeopleSettled?: boolean;
|
|
259
|
+
keyPeople: AwaitingPersonPayload[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** One officer as the register names them — the key-people prefill's input. */
|
|
263
|
+
export interface RegistryOfficer {
|
|
264
|
+
name: string | null;
|
|
265
|
+
designation: string | null;
|
|
266
|
+
/** Everything else the register said about them (older servers omit these). */
|
|
267
|
+
roles?: string[] | null;
|
|
268
|
+
ownershipPct?: number | null;
|
|
269
|
+
email?: string | null;
|
|
270
|
+
isCorporate?: boolean | null;
|
|
271
|
+
registrationNumber?: string | null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** What the register holds about a company, when it answered. Everything is
|
|
275
|
+
* nullable because no register answers all of it for every company. */
|
|
276
|
+
export interface BusinessCompanyRecord {
|
|
277
|
+
name: string | null;
|
|
278
|
+
registrationNumber: string;
|
|
279
|
+
registrationDate: string | null;
|
|
280
|
+
typeOfEntity: string | null;
|
|
281
|
+
companyStatus: string | null;
|
|
282
|
+
address: string | null;
|
|
283
|
+
email: string | null;
|
|
284
|
+
phone: string | null;
|
|
285
|
+
taxId: string | null;
|
|
286
|
+
vatNumber: string | null;
|
|
287
|
+
natureOfBusiness: string | null;
|
|
288
|
+
city: string | null;
|
|
289
|
+
state: string | null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** `POST /business/select` — the paid registry check at selection. */
|
|
293
|
+
export interface BusinessSelectResponse {
|
|
294
|
+
checked: boolean;
|
|
295
|
+
/** Why the pre-flight did not run (`checked: false`) — the submit-time lookup
|
|
296
|
+
* still happens, so this is informational, never an error. */
|
|
297
|
+
reason?: string;
|
|
298
|
+
found?: boolean;
|
|
299
|
+
charged?: boolean;
|
|
300
|
+
business?: (BusinessCompanyRecord & { keyPeople: RegistryOfficer[] }) | null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** One candidate from the FREE name search — picking one is what runs the paid check. */
|
|
304
|
+
export interface BusinessSearchHit {
|
|
305
|
+
name: string;
|
|
306
|
+
registrationNumber: string;
|
|
307
|
+
status?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** `GET /business/search` — find a business by name. */
|
|
311
|
+
export interface BusinessSearchResponse {
|
|
312
|
+
results: BusinessSearchHit[];
|
|
313
|
+
/** Which source answered; a degraded fallback names itself here. */
|
|
314
|
+
source: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `GET /business/regions` — the registry regions of a split register. */
|
|
318
|
+
export interface BusinessRegionsResponse {
|
|
319
|
+
regions: { code: string; name: string }[];
|
|
320
|
+
}
|
|
@@ -10,7 +10,26 @@
|
|
|
10
10
|
export interface VerifyRequest {
|
|
11
11
|
country: string;
|
|
12
12
|
idType: string;
|
|
13
|
+
/** The attempt session this run happened under — the verification adopts its
|
|
14
|
+
* id, and a registry check paid at selection is not paid again at submit. */
|
|
15
|
+
sessionId?: string;
|
|
13
16
|
idNumber?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Multi-ID: every check in the run, in pick order (2–3). ONE verification
|
|
19
|
+
* comes back, judged by the workflow's pass policy. The top-level
|
|
20
|
+
* idType/idNumber mirror the first entry.
|
|
21
|
+
*/
|
|
22
|
+
idChecks?: Array<{
|
|
23
|
+
idType: string;
|
|
24
|
+
idNumber?: string;
|
|
25
|
+
documentFront?: string;
|
|
26
|
+
documentBack?: string;
|
|
27
|
+
/** Each check's OWN document recording. */
|
|
28
|
+
documentFrontVideo?: string;
|
|
29
|
+
documentBackVideo?: string;
|
|
30
|
+
/** This check's own chip read — the chip belongs to a PARTICULAR document. */
|
|
31
|
+
nfc?: VerifyRequest['nfc'];
|
|
32
|
+
}>;
|
|
14
33
|
/** The org's user reference → Entity.externalUserId at the seam (not matched). */
|
|
15
34
|
userId?: string;
|
|
16
35
|
userData?: {
|
|
@@ -50,6 +69,13 @@ export interface VerifyRequest {
|
|
|
50
69
|
email?: string;
|
|
51
70
|
phone?: string;
|
|
52
71
|
website?: string;
|
|
72
|
+
/** Registry facts the applicant states (the extended collectCompanyInfo
|
|
73
|
+
* fields) — where they differ from the register, that is the finding. */
|
|
74
|
+
dateOfIncorporation?: string;
|
|
75
|
+
taxId?: string;
|
|
76
|
+
vatNumber?: string;
|
|
77
|
+
companyType?: string;
|
|
78
|
+
natureOfBusiness?: string;
|
|
53
79
|
/** Uploaded supporting documents (honoured only when the workflow's
|
|
54
80
|
* `business.documents` block configures them). */
|
|
55
81
|
documents?: Array<{ type: string; mediaId: string }>;
|
|
@@ -77,7 +103,33 @@ export interface VerifyRequest {
|
|
|
77
103
|
* signer — and only the server may conclude a chip is genuine. `chipAuth`
|
|
78
104
|
* reports how the chip was unlocked and is informational.
|
|
79
105
|
*/
|
|
80
|
-
nfc?: {
|
|
106
|
+
nfc?: {
|
|
107
|
+
dg1: string;
|
|
108
|
+
sod?: string;
|
|
109
|
+
dg2?: string;
|
|
110
|
+
dg7?: string;
|
|
111
|
+
dg11?: string;
|
|
112
|
+
dg12?: string;
|
|
113
|
+
/**
|
|
114
|
+
* DG15 (the chip's Active-Authentication public key) and its signature over
|
|
115
|
+
* the challenge the server issued — the ANTI-CLONE proof. Passive
|
|
116
|
+
* authentication proves the issuing state signed this data; only these
|
|
117
|
+
* prove it is the chip they signed it onto. Verified server-side against a
|
|
118
|
+
* SOD-bound DG15: a client that checked its own chip could be patched.
|
|
119
|
+
*/
|
|
120
|
+
dg15?: string;
|
|
121
|
+
aaChallengeId?: string;
|
|
122
|
+
aaSignature?: string;
|
|
123
|
+
chipAuth?: string;
|
|
124
|
+
/**
|
|
125
|
+
* WHY the session is on that protocol. A chip reading over BAC because it
|
|
126
|
+
* offers no PACE is nothing to act on; one reading over BAC because our
|
|
127
|
+
* PACE broke is a bug, and `chipAuth` alone cannot tell them apart.
|
|
128
|
+
* Diagnostic only — the server records it and never judges on it.
|
|
129
|
+
*/
|
|
130
|
+
paceOutcome?: string;
|
|
131
|
+
paceDetail?: string;
|
|
132
|
+
};
|
|
81
133
|
/** The workflow that drove this flow — attributes the submission to it. */
|
|
82
134
|
workflowId?: string;
|
|
83
135
|
/**
|
package/src/services/api.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { SDK_VERSION } from './deviceMetadata';
|
|
2
2
|
import type {
|
|
3
|
+
BusinessRegionsResponse,
|
|
4
|
+
BusinessSearchResponse,
|
|
5
|
+
BusinessSelectResponse,
|
|
3
6
|
ContactCheckResponse,
|
|
7
|
+
NfcChallengeResponse,
|
|
4
8
|
ContactSendResponse,
|
|
5
9
|
HealthResponse,
|
|
6
10
|
MediaUploadType,
|
|
7
11
|
SdkConfigResponse,
|
|
12
|
+
SessionStartResponse,
|
|
13
|
+
SessionSummaryResponse,
|
|
8
14
|
UploadFile,
|
|
9
15
|
UploadResponse,
|
|
10
16
|
VerificationStatusResponse,
|
|
@@ -234,6 +240,18 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
234
240
|
});
|
|
235
241
|
},
|
|
236
242
|
|
|
243
|
+
/**
|
|
244
|
+
* A fresh Active-Authentication challenge for a chip read — the nonce the
|
|
245
|
+
* chip signs to prove it is the original document rather than a copy of
|
|
246
|
+
* one. It has to come from the SERVER: a nonce the client chose would let
|
|
247
|
+
* a captured signature be replayed forever, which is the clone the check
|
|
248
|
+
* exists to catch. Best-effort by contract at every call site — a chip
|
|
249
|
+
* read without one is exactly the read we did before.
|
|
250
|
+
*/
|
|
251
|
+
async nfcChallenge(): Promise<NfcChallengeResponse> {
|
|
252
|
+
return request<NfcChallengeResponse>('/nfc/challenge', { method: 'POST' });
|
|
253
|
+
},
|
|
254
|
+
|
|
237
255
|
async config(signal?: AbortSignal): Promise<SdkConfigResponse> {
|
|
238
256
|
return request<SdkConfigResponse>('/config', signal ? { signal } : {});
|
|
239
257
|
},
|
|
@@ -250,6 +268,92 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
|
|
|
250
268
|
);
|
|
251
269
|
},
|
|
252
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Begin (or resume) a verification ATTEMPT session. Best-effort by
|
|
273
|
+
* contract: sessions power resumability, the dashboard's live attempt
|
|
274
|
+
* view, and the registry check at selection — verifying is never
|
|
275
|
+
* conditional on one existing.
|
|
276
|
+
*/
|
|
277
|
+
async startSession(input: {
|
|
278
|
+
externalUserId?: string;
|
|
279
|
+
workflowId?: string;
|
|
280
|
+
/** Persistent device id — the anonymous-mount resume fallback. */
|
|
281
|
+
deviceRef?: string;
|
|
282
|
+
}): Promise<SessionStartResponse> {
|
|
283
|
+
return request<SessionStartResponse>('/session/start', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
body: JSON.stringify(input),
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A submitted session, rebuilt server-side — the reconciled key-people
|
|
291
|
+
* list the success screen polls. Same body a hosted link reads by token,
|
|
292
|
+
* so mobile and hosted success screens cannot tell different stories
|
|
293
|
+
* about one application.
|
|
294
|
+
*/
|
|
295
|
+
async sessionSummary(sessionId: string): Promise<SessionSummaryResponse> {
|
|
296
|
+
return request<SessionSummaryResponse>(`/session/${sessionId}/summary`);
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Save where the user has got to. Losing a save costs some re-typing on
|
|
301
|
+
* resume and must never interrupt them now — callers swallow failures.
|
|
302
|
+
*/
|
|
303
|
+
async saveProgress(sessionId: string, progress: unknown): Promise<void> {
|
|
304
|
+
await request(`/session/${encodeURIComponent(sessionId)}/progress`, {
|
|
305
|
+
method: 'PUT',
|
|
306
|
+
body: JSON.stringify(progress),
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Find a business by name. FREE — no provider charge here or upstream, so
|
|
312
|
+
* the applicant may look as many times as they need. Throws on a provider
|
|
313
|
+
* failure so the caller can show "unavailable" rather than an empty list,
|
|
314
|
+
* which would read as "this business is not registered".
|
|
315
|
+
*/
|
|
316
|
+
async businessSearch(params: {
|
|
317
|
+
country: string;
|
|
318
|
+
subdivisionCode?: string;
|
|
319
|
+
query: string;
|
|
320
|
+
limit?: number;
|
|
321
|
+
}): Promise<BusinessSearchResponse> {
|
|
322
|
+
const qs = new URLSearchParams({ country: params.country, query: params.query });
|
|
323
|
+
if (params.subdivisionCode) qs.set('subdivisionCode', params.subdivisionCode);
|
|
324
|
+
if (params.limit) qs.set('limit', String(params.limit));
|
|
325
|
+
return request<BusinessSearchResponse>(`/business/search?${qs.toString()}`);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/** Registry regions for a country. Empty when it has a single register. */
|
|
329
|
+
async businessRegions(country: string): Promise<BusinessRegionsResponse> {
|
|
330
|
+
return request<BusinessRegionsResponse>(
|
|
331
|
+
`/business/regions?country=${encodeURIComponent(country)}`,
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The PAID registry check for the company the applicant identified, run at
|
|
337
|
+
* selection so the register's key people come back BEFORE the form asks
|
|
338
|
+
* for them. Never fails the flow: a short balance, unconfigured pricing or
|
|
339
|
+
* a spent lookup budget returns `checked: false` and the lookup happens at
|
|
340
|
+
* submit as before.
|
|
341
|
+
*/
|
|
342
|
+
async businessSelect(body: {
|
|
343
|
+
sessionId: string;
|
|
344
|
+
country: string;
|
|
345
|
+
subdivisionCode?: string;
|
|
346
|
+
product?: string;
|
|
347
|
+
registrationNumber: string;
|
|
348
|
+
registrationName?: string;
|
|
349
|
+
sandboxOutcome?: string;
|
|
350
|
+
}): Promise<BusinessSelectResponse> {
|
|
351
|
+
return request<BusinessSelectResponse>('/business/select', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: JSON.stringify(body),
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
|
|
253
357
|
async health(): Promise<HealthResponse> {
|
|
254
358
|
// Public endpoint — no auth needed, but the shared headers are harmless.
|
|
255
359
|
return request<HealthResponse>('/health');
|
|
@@ -15,7 +15,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
|
|
|
15
15
|
* Single source of truth for the SDK version — also used by `services/api.ts`
|
|
16
16
|
* for the `X-SDK-Version` header. Keep in sync with `package.json`.
|
|
17
17
|
*/
|
|
18
|
-
export const SDK_VERSION = '2.
|
|
18
|
+
export const SDK_VERSION = '2.5.0';
|
|
19
19
|
|
|
20
20
|
export interface ReactNativeDeviceMetadata {
|
|
21
21
|
sdkType: 'react-native';
|
package/src/services/errors.ts
CHANGED
|
@@ -74,8 +74,16 @@ export function mapToKycError(err: unknown, context: ErrorContext): KYCError {
|
|
|
74
74
|
: err.message || 'This verification feature is currently disabled for your organization.';
|
|
75
75
|
return new KYCError('feature_disabled', message);
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// Nothing came back at all — DNS, routing, a refused connection. Kept
|
|
78
|
+
// SEPARATE from a 5xx: telling somebody the server errored when their
|
|
79
|
+
// request never arrived sends them looking through server logs for a
|
|
80
|
+
// request that was never made.
|
|
81
|
+
if (err.statusCode === 0) {
|
|
82
|
+
const code: KYCErrorCode = context === 'upload' ? 'upload_failed' : 'network_error';
|
|
83
|
+
return new KYCError(code, "Couldn't reach the server. Check your connection and try again.");
|
|
84
|
+
}
|
|
85
|
+
if (err.statusCode >= 500) {
|
|
86
|
+
// Transient server error that survived retries — the request DID arrive.
|
|
79
87
|
const code: KYCErrorCode = context === 'upload' ? 'upload_failed' : 'network_error';
|
|
80
88
|
return new KYCError(code, 'A server error occurred. Please try again in a moment.');
|
|
81
89
|
}
|
|
@@ -51,4 +51,33 @@ export interface MyazaFaceDetector extends HybridObject<{ ios: 'swift'; android:
|
|
|
51
51
|
* `faceCount: 0` when no face is present.
|
|
52
52
|
*/
|
|
53
53
|
detectFace(frame: Frame): FaceResult;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the detector can run RIGHT NOW.
|
|
57
|
+
*
|
|
58
|
+
* Android fetches its model through Google Play Services rather than bundling
|
|
59
|
+
* it (see android/build.gradle), so there is a window — first launch, or a
|
|
60
|
+
* device with no GMS at all — where detection cannot work. This MUST be
|
|
61
|
+
* checked before the camera opens: `detectFace` can only answer in
|
|
62
|
+
* `FaceResult`, where "model missing" and "no face in frame" are the same
|
|
63
|
+
* `faceCount: 0`, so relying on it would leave the user staring at
|
|
64
|
+
* "position your face" forever with nothing to explain why.
|
|
65
|
+
*
|
|
66
|
+
* iOS is always true — Apple Vision is a system framework with nothing to
|
|
67
|
+
* fetch.
|
|
68
|
+
*/
|
|
69
|
+
isModelReady(): boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask Play Services to download the model, resolving `true` once it is usable
|
|
73
|
+
* and `false` if it cannot be obtained (no GMS, no network, user declined).
|
|
74
|
+
*
|
|
75
|
+
* Call this EARLY — the SDK primes it at flow start, so the download overlaps
|
|
76
|
+
* the consent and ID-type screens and the model is warm by the time liveness
|
|
77
|
+
* runs. This mirrors the web SDK, which calls `primeFaceMesh()` on mount for
|
|
78
|
+
* exactly the same reason.
|
|
79
|
+
*
|
|
80
|
+
* Safe to call repeatedly; resolves immediately when already ready.
|
|
81
|
+
*/
|
|
82
|
+
prepareModel(): Promise<boolean>;
|
|
54
83
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { BusinessCheckResult, KycState } from './state';
|
|
2
|
+
import { EMPTY_BUSINESS_CHECK } from './state';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The paid registry check at SELECTION, split from kycStore.ts (200-line rule).
|
|
6
|
+
//
|
|
7
|
+
// Run when the applicant confirms their company, so the register answers
|
|
8
|
+
// BEFORE the details screen asks them to confirm what it said — and its key
|
|
9
|
+
// people arrive before that step asks for them. Only a definitive "not on the
|
|
10
|
+
// register" stops the flow: everything else (a short balance, an outage, a
|
|
11
|
+
// spent lookup budget) continues and is checked at submission, exactly as it
|
|
12
|
+
// was before this existed. Mirrors the web SDK's useBusinessCheck — keep the
|
|
13
|
+
// two in lockstep.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type Set = (partial: Partial<KycState> | ((s: KycState) => Partial<KycState>)) => void;
|
|
17
|
+
|
|
18
|
+
export async function runBusinessCheck(
|
|
19
|
+
get: () => KycState,
|
|
20
|
+
set: Set,
|
|
21
|
+
): Promise<BusinessCheckResult> {
|
|
22
|
+
const s = get();
|
|
23
|
+
const registrationNumber = s.business.registrationNumber?.trim();
|
|
24
|
+
if (!registrationNumber) return { canContinue: true, company: null };
|
|
25
|
+
|
|
26
|
+
// Already checked this exact company — do not pay to be told again.
|
|
27
|
+
//
|
|
28
|
+
// Only a SETTLED answer is reused. 'unavailable' is deliberately not one: an
|
|
29
|
+
// outage said nothing about the company, so a repeat press retries the
|
|
30
|
+
// register rather than replaying the outage. And a remembered answer keeps
|
|
31
|
+
// its meaning — a stored not_found still blocks.
|
|
32
|
+
const normalized = registrationNumber.toUpperCase();
|
|
33
|
+
const settled =
|
|
34
|
+
s.businessCheck.status !== 'idle' &&
|
|
35
|
+
s.businessCheck.status !== 'checking' &&
|
|
36
|
+
s.businessCheck.status !== 'unavailable';
|
|
37
|
+
if (s.businessCheck.checkedNumber === normalized && settled) {
|
|
38
|
+
return {
|
|
39
|
+
canContinue: s.businessCheck.status !== 'not_found',
|
|
40
|
+
company: s.businessCheck.company,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// No session means no anchor for the charge, so there is nothing to run
|
|
45
|
+
// against. The check happens at submission, exactly as it did before.
|
|
46
|
+
if (!s.sessionId) return { canContinue: true, company: null };
|
|
47
|
+
|
|
48
|
+
set((cur) => ({
|
|
49
|
+
businessCheck: { ...cur.businessCheck, status: 'checking', checkedNumber: normalized },
|
|
50
|
+
}));
|
|
51
|
+
try {
|
|
52
|
+
const res = await s.api.businessSelect({
|
|
53
|
+
sessionId: s.sessionId,
|
|
54
|
+
country: s.business.country || s.config.business?.country || '',
|
|
55
|
+
...(s.business.subdivisionCode.trim()
|
|
56
|
+
? { subdivisionCode: s.business.subdivisionCode.trim() }
|
|
57
|
+
: {}),
|
|
58
|
+
...(s.business.registrationName.trim()
|
|
59
|
+
? { registrationName: s.business.registrationName.trim() }
|
|
60
|
+
: {}),
|
|
61
|
+
...(s.business.product ? { product: s.business.product } : {}),
|
|
62
|
+
registrationNumber,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!res.checked) {
|
|
66
|
+
// The organisation could not be charged. Not the applicant's problem and
|
|
67
|
+
// not something they can fix, so it is not shown as an error — the flow
|
|
68
|
+
// continues and the check runs at submission. `lookup_limit_reached` is
|
|
69
|
+
// the one they DID cause, by re-picking company after company, and the
|
|
70
|
+
// one they can act on: check the number rather than keep trying.
|
|
71
|
+
set((cur) => ({
|
|
72
|
+
businessCheck: {
|
|
73
|
+
...cur.businessCheck,
|
|
74
|
+
status: res.reason === 'lookup_limit_reached' ? 'limit_reached' : 'skipped',
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
return { canContinue: true, company: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!res.found) {
|
|
81
|
+
// A definitive "not on the register" is worth stopping for: continuing
|
|
82
|
+
// would spend the applicant's time on documents for a company that will
|
|
83
|
+
// fail anyway.
|
|
84
|
+
set((cur) => ({
|
|
85
|
+
businessCheck: { ...cur.businessCheck, status: 'not_found', company: null, officers: [] },
|
|
86
|
+
}));
|
|
87
|
+
return { canContinue: false, company: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { keyPeople, ...company } = res.business!;
|
|
91
|
+
set((cur) => ({
|
|
92
|
+
businessCheck: {
|
|
93
|
+
...cur.businessCheck,
|
|
94
|
+
status: 'found',
|
|
95
|
+
company,
|
|
96
|
+
officers: keyPeople ?? [],
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
return { canContinue: true, company };
|
|
100
|
+
} catch {
|
|
101
|
+
// A register outage is NOT "this company does not exist" — telling the
|
|
102
|
+
// applicant their business is unregistered on the strength of a 503 is the
|
|
103
|
+
// one wrong answer here. Retryable, and it never blocks: the check still
|
|
104
|
+
// happens at submission.
|
|
105
|
+
set((cur) => ({ businessCheck: { ...cur.businessCheck, status: 'unavailable' } }));
|
|
106
|
+
return { canContinue: true, company: null };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Back to square one — called when WHICH company this is about changes. */
|
|
111
|
+
export function resetBusinessCheck(set: Set): void {
|
|
112
|
+
set({ businessCheck: { ...EMPTY_BUSINESS_CHECK } });
|
|
113
|
+
}
|
package/src/store/derive.ts
CHANGED
|
@@ -76,6 +76,10 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
|
|
|
76
76
|
hasPhoneVerification: config.phoneVerification?.enabled === true,
|
|
77
77
|
hasPoa: hasProofOfAddressStep(config.proofOfAddress),
|
|
78
78
|
hasQuestionnaire: hasActiveQuestionnaire(config.questionnaire),
|
|
79
|
+
// Present only on a session a reviewer sent back. Narrows the order to the
|
|
80
|
+
// steps they ticked, so somebody fixing one blurry photo is not walked
|
|
81
|
+
// through the whole flow again.
|
|
82
|
+
resubmit: config.resubmit,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -148,6 +152,15 @@ export function businessSubmission(state: KycState): {
|
|
|
148
152
|
...(text(b.email) ? { email: text(b.email) } : {}),
|
|
149
153
|
...(text(b.phone) ? { phone: text(b.phone) } : {}),
|
|
150
154
|
...(text(b.website) ? { website: text(b.website) } : {}),
|
|
155
|
+
// The five registry facts, sent only when filled. The server drops any
|
|
156
|
+
// the workflow has switched off, so an over-send is validate-and-drop
|
|
157
|
+
// rather than an error, and an under-send is what the required-field
|
|
158
|
+
// 422 is for.
|
|
159
|
+
...(text(b.dateOfIncorporation) ? { dateOfIncorporation: text(b.dateOfIncorporation) } : {}),
|
|
160
|
+
...(text(b.taxId) ? { taxId: text(b.taxId) } : {}),
|
|
161
|
+
...(text(b.vatNumber) ? { vatNumber: text(b.vatNumber) } : {}),
|
|
162
|
+
...(text(b.companyType) ? { companyType: text(b.companyType) } : {}),
|
|
163
|
+
...(text(b.natureOfBusiness) ? { natureOfBusiness: text(b.natureOfBusiness) } : {}),
|
|
151
164
|
// Extras ride along ONLY when the workflow configures them — the server
|
|
152
165
|
// ignores an unconfigured block, but sending one is a claim we did not
|
|
153
166
|
// collect properly.
|
|
@@ -155,6 +168,8 @@ export function businessSubmission(state: KycState): {
|
|
|
155
168
|
? { documents: app.documents.map((d) => ({ type: d.type, mediaId: d.mediaId })) }
|
|
156
169
|
: {}),
|
|
157
170
|
...(hasKeyPeopleCollection(config) && people.length > 0 ? { keyPeople: people } : {}),
|
|
171
|
+
// The FATF fallback, attested: no natural person qualifies as a UBO.
|
|
172
|
+
...(app.uboUnidentifiable ? { uboUnidentifiable: true } : {}),
|
|
158
173
|
...(hasApplicantVerification(config) && app.applicantRole
|
|
159
174
|
? {
|
|
160
175
|
applicant: {
|