@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.
Files changed (129) hide show
  1. package/android/build.gradle +35 -3
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +75 -0
  3. package/ios/HybridMyazaFaceDetector.swift +8 -0
  4. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  5. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  6. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  7. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  8. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  9. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  10. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  11. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  12. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  13. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  14. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  15. package/package.json +1 -2
  16. package/src/MyazaKYC.tsx +12 -0
  17. package/src/components/BrandBar.tsx +137 -0
  18. package/src/components/DashedBorder.tsx +55 -0
  19. package/src/components/DialCodePicker.tsx +25 -24
  20. package/src/components/DocumentReviewSide.tsx +34 -14
  21. package/src/components/Icon.tsx +14 -0
  22. package/src/components/KycSheet.tsx +39 -140
  23. package/src/components/MediaSourceSheet.tsx +7 -37
  24. package/src/components/MultiIdProgress.tsx +107 -0
  25. package/src/components/MyazaDateField.tsx +6 -20
  26. package/src/components/MyazaInput.tsx +23 -4
  27. package/src/components/MyazaSelect.tsx +17 -40
  28. package/src/components/OwnershipSlider.tsx +102 -0
  29. package/src/components/PhoneNumberInput.tsx +37 -4
  30. package/src/components/SandboxBanner.tsx +92 -0
  31. package/src/components/StaggerIn.tsx +63 -0
  32. package/src/components/StepHeader.tsx +15 -2
  33. package/src/components/StepView.tsx +26 -0
  34. package/src/components/glass/ChromeGlass.tsx +65 -0
  35. package/src/components/glass/FloatingSheet.tsx +242 -0
  36. package/src/components/glass/GlassSheet.tsx +50 -0
  37. package/src/components/glass/GlassSurface.tsx +31 -3
  38. package/src/components/stepHeaderMeta.tsx +3 -1
  39. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  40. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  41. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  42. package/src/config/business.ts +6 -6
  43. package/src/config/businessDetailsValidity.ts +52 -0
  44. package/src/config/businessPrefill.ts +72 -0
  45. package/src/config/businessSteps.ts +14 -3
  46. package/src/config/keyPeople.ts +111 -7
  47. package/src/config/keyPeopleCorporate.ts +19 -0
  48. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  49. package/src/config/keyPeopleOwners.ts +26 -0
  50. package/src/config/keyPeoplePrefill.ts +138 -0
  51. package/src/config/keyPeopleSectionDefs.ts +65 -0
  52. package/src/config/keyPeopleSections.ts +167 -0
  53. package/src/config/registrationHint.ts +1 -1
  54. package/src/config/stepOrder.ts +24 -4
  55. package/src/config/theme.ts +8 -2
  56. package/src/config/website.ts +36 -0
  57. package/src/config/workflowMerge.ts +5 -0
  58. package/src/emrtd/activeAuth.ts +100 -0
  59. package/src/emrtd/extras.ts +76 -0
  60. package/src/emrtd/files.ts +5 -0
  61. package/src/emrtd/index.ts +1 -0
  62. package/src/emrtd/read.ts +8 -1
  63. package/src/emrtd/session.ts +41 -0
  64. package/src/emrtd/stages.ts +11 -0
  65. package/src/lib/contact-recovery.ts +44 -0
  66. package/src/lib/multi-id.ts +210 -0
  67. package/src/lib/resubmit.ts +130 -0
  68. package/src/lib/screen-corners.ts +50 -0
  69. package/src/lib/step-log.ts +24 -3
  70. package/src/lib/use-multi-id-plan.ts +29 -0
  71. package/src/liveness/useModelReady.ts +65 -0
  72. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  73. package/src/screens/ApplicantRoleStep.tsx +9 -5
  74. package/src/screens/BusinessCheckPanel.tsx +89 -0
  75. package/src/screens/BusinessDetailsFields.tsx +109 -0
  76. package/src/screens/BusinessDetailsStep.tsx +131 -145
  77. package/src/screens/BusinessDocumentSlot.tsx +2 -3
  78. package/src/screens/BusinessDocumentsStep.tsx +1 -1
  79. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  80. package/src/screens/BusinessPickedCard.tsx +86 -0
  81. package/src/screens/BusinessPickedSection.tsx +51 -0
  82. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  83. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  84. package/src/screens/BusinessSearch.tsx +199 -0
  85. package/src/screens/BusinessSearchResults.tsx +115 -0
  86. package/src/screens/CompanyInfoFields.tsx +86 -51
  87. package/src/screens/ConsentStep.tsx +31 -0
  88. package/src/screens/ContactDestinationField.tsx +1 -0
  89. package/src/screens/ContactVerificationStep.tsx +34 -3
  90. package/src/screens/IdTypeStep.tsx +12 -5
  91. package/src/screens/KeepLinksSheet.tsx +88 -0
  92. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  93. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  94. package/src/screens/KeyPeoplePending.tsx +154 -0
  95. package/src/screens/KeyPeopleSection.tsx +162 -0
  96. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  97. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  98. package/src/screens/KeyPersonCard.tsx +36 -4
  99. package/src/screens/KeyPersonForm.tsx +195 -36
  100. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  101. package/src/screens/KeyPersonOwners.tsx +109 -0
  102. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  103. package/src/screens/KeyPersonSheet.tsx +92 -108
  104. package/src/screens/LivenessStep.tsx +18 -0
  105. package/src/screens/NfcStep.tsx +14 -0
  106. package/src/screens/ProofOfAddressStep.tsx +6 -3
  107. package/src/screens/SubmittedStep.tsx +66 -10
  108. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  109. package/src/screens/useAwaitingPeople.ts +90 -0
  110. package/src/services/api-types.ts +158 -2
  111. package/src/services/api-verify-types.ts +53 -1
  112. package/src/services/api.ts +104 -0
  113. package/src/services/deviceMetadata.ts +1 -1
  114. package/src/services/errors.ts +10 -2
  115. package/src/services/workflowGate.ts +1 -0
  116. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  117. package/src/store/businessCheck.ts +113 -0
  118. package/src/store/derive.ts +15 -0
  119. package/src/store/kycStore.ts +241 -14
  120. package/src/store/serverConfig.ts +2 -0
  121. package/src/store/session.ts +208 -0
  122. package/src/store/state.ts +155 -2
  123. package/src/store/submit.ts +63 -13
  124. package/src/store/submitApplicant.ts +5 -11
  125. package/src/types/business.ts +23 -2
  126. package/src/types/config.ts +29 -3
  127. package/src/types/verification.ts +1 -1
  128. package/src/types/workflow.ts +15 -0
  129. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -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: 'pending';
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;
@@ -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
- mediaIds: business ? {} : state.mediaIds,
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
- ...(state.chipData
64
- ? {
65
- nfc: {
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). Same block as the individual flow's builder.
57
- ...(state.chipData
58
- ? {
59
- nfc: {
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.
@@ -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
- /** Supporting-document kinds a KYB workflow can request from the applicant. */
21
- export const COMPANY_INFO_FIELDS = ['address', 'email', 'phone', 'website'] as const;
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
  /**
@@ -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
- * Both convey the same fraction; the choice is how much room it takes.
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: 'pending' — results arrive async via webhook.
279
+ * status: 'processing' — results arrive async via webhook.
254
280
  */
255
281
  onSubmit?: (submission: KYCSubmission) => void;
256
282
  onClose?: () => void;
@@ -4,7 +4,7 @@
4
4
 
5
5
  export interface KYCSubmission {
6
6
  verificationId: string;
7
- status: 'pending';
7
+ status: 'processing';
8
8
  metadata: Record<string, string>;
9
9
  submittedAt: string;
10
10
  }
@@ -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';