@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.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 (246) hide show
  1. package/README.md +151 -5
  2. package/android/build.gradle +35 -3
  3. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
  4. package/app.plugin.js +89 -8
  5. package/ios/HybridMyazaFaceDetector.swift +16 -3
  6. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
  7. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
  8. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
  9. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
  10. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
  11. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
  12. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  13. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
  14. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
  15. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
  16. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
  17. package/package.json +14 -2
  18. package/src/MyazaBiometricAuth.tsx +119 -0
  19. package/src/MyazaKYC.tsx +12 -0
  20. package/src/components/CountryField.tsx +8 -0
  21. package/src/components/CountryRegionPicker.tsx +184 -0
  22. package/src/components/DashedBorder.tsx +55 -0
  23. package/src/components/DialCodePicker.tsx +53 -67
  24. package/src/components/DialCodeRow.tsx +102 -0
  25. package/src/components/FramedMapPicker.tsx +122 -0
  26. package/src/components/GeoBadge.tsx +34 -0
  27. package/src/components/Icon.tsx +2 -176
  28. package/src/components/KycFlow.tsx +60 -32
  29. package/src/components/LineSkeleton.tsx +100 -0
  30. package/src/components/MapChrome.tsx +67 -0
  31. package/src/components/MapPinMarker.tsx +54 -0
  32. package/src/components/MapPinPicker.tsx +179 -0
  33. package/src/components/MilestoneTrack.tsx +155 -0
  34. package/src/components/MultiIdProgress.tsx +107 -0
  35. package/src/components/MyazaInput.tsx +39 -4
  36. package/src/components/MyazaSelect.tsx +2 -1
  37. package/src/components/OptionRow.tsx +22 -3
  38. package/src/components/OwnershipSlider.tsx +102 -0
  39. package/src/components/PhoneNumberInput.tsx +45 -4
  40. package/src/components/PoweredBy.tsx +5 -5
  41. package/src/components/PresenceBlocks.tsx +153 -0
  42. package/src/components/StaggerIn.tsx +63 -0
  43. package/src/components/StepView.tsx +43 -0
  44. package/src/components/StickyActions.tsx +56 -0
  45. package/src/components/dialCodeRows.ts +64 -0
  46. package/src/components/glass/FloatingSheet.tsx +73 -21
  47. package/src/components/glass/GlassSheet.tsx +14 -2
  48. package/src/components/icon-map.ts +176 -0
  49. package/src/components/stepHeaderMeta.tsx +45 -5
  50. package/src/config/addressCollection.ts +125 -0
  51. package/src/config/biometricOptions.ts +100 -0
  52. package/src/config/business.ts +26 -7
  53. package/src/config/businessDetailsValidity.ts +52 -0
  54. package/src/config/businessPrefill.ts +72 -0
  55. package/src/config/businessSteps.ts +20 -3
  56. package/src/config/consentStep.ts +19 -0
  57. package/src/config/keyPeople.ts +111 -7
  58. package/src/config/keyPeopleCorporate.ts +19 -0
  59. package/src/config/keyPeopleOwnerTypes.ts +12 -0
  60. package/src/config/keyPeopleOwners.ts +26 -0
  61. package/src/config/keyPeoplePrefill.ts +138 -0
  62. package/src/config/keyPeopleSectionDefs.ts +65 -0
  63. package/src/config/keyPeopleSections.ts +167 -0
  64. package/src/config/proofOfAddress.ts +54 -11
  65. package/src/config/regions.ts +24 -0
  66. package/src/config/registrationHint.ts +1 -1
  67. package/src/config/stepOrder.ts +100 -6
  68. package/src/config/theme.ts +8 -2
  69. package/src/config/uploadLimits.ts +35 -0
  70. package/src/config/website.ts +36 -0
  71. package/src/config/workflowMerge.ts +16 -5
  72. package/src/emrtd/activeAuth.ts +100 -0
  73. package/src/emrtd/extras.ts +76 -0
  74. package/src/emrtd/files.ts +5 -0
  75. package/src/emrtd/index.ts +1 -0
  76. package/src/emrtd/read.ts +8 -1
  77. package/src/emrtd/session.ts +41 -0
  78. package/src/emrtd/stages.ts +11 -0
  79. package/src/index.ts +54 -1
  80. package/src/lib/address-current-location.ts +171 -0
  81. package/src/lib/address-field-modes.ts +119 -0
  82. package/src/lib/address-flow.ts +172 -0
  83. package/src/lib/address-helpers.ts +48 -0
  84. package/src/lib/address-line.ts +76 -0
  85. package/src/lib/address-pin-move.ts +97 -0
  86. package/src/lib/address-step-recovery.ts +63 -0
  87. package/src/lib/authed-image.ts +78 -0
  88. package/src/lib/biometric-auth.ts +52 -0
  89. package/src/lib/biometric-copy.ts +51 -0
  90. package/src/lib/captureRing.ts +83 -0
  91. package/src/lib/contact-recovery.ts +44 -0
  92. package/src/lib/country-adoption.ts +89 -0
  93. package/src/lib/inferred-country.ts +65 -0
  94. package/src/lib/livenessLayout.ts +49 -0
  95. package/src/lib/map-frame.ts +159 -0
  96. package/src/lib/map-tiles.ts +143 -0
  97. package/src/lib/multi-id.ts +210 -0
  98. package/src/lib/poa-country-gate.ts +27 -0
  99. package/src/lib/resubmit.ts +130 -0
  100. package/src/lib/result-copy.ts +116 -0
  101. package/src/lib/result-wait.ts +53 -0
  102. package/src/lib/review-map-surface.ts +26 -0
  103. package/src/lib/scope.ts +31 -0
  104. package/src/lib/screen-corners.ts +50 -0
  105. package/src/lib/selfie-upload-wait.ts +79 -0
  106. package/src/lib/step-log.ts +24 -3
  107. package/src/lib/street-view-fov.ts +42 -0
  108. package/src/lib/use-multi-id-plan.ts +29 -0
  109. package/src/lib/webview-available.ts +47 -0
  110. package/src/liveness/useLiveness.ts +6 -1
  111. package/src/liveness/useModelReady.ts +65 -0
  112. package/src/liveness/visionCameraFaceDetector.ts +35 -0
  113. package/src/presence/background-math.ts +100 -0
  114. package/src/presence/background-store.ts +82 -0
  115. package/src/presence/background.ts +165 -0
  116. package/src/presence/foreground-service.ts +193 -0
  117. package/src/presence/fs.ts +51 -0
  118. package/src/presence/geofence.ts +41 -0
  119. package/src/presence/math.ts +44 -0
  120. package/src/presence/post.ts +40 -0
  121. package/src/presence/report.ts +87 -0
  122. package/src/presence/sampler.ts +110 -0
  123. package/src/presence/status.ts +92 -0
  124. package/src/presence/store.ts +94 -0
  125. package/src/presence/tier.ts +36 -0
  126. package/src/presence/watch-wait.ts +81 -0
  127. package/src/screens/AddressCountryControl.tsx +115 -0
  128. package/src/screens/ApplicantRoleStep.tsx +9 -5
  129. package/src/screens/BusinessCheckPanel.tsx +89 -0
  130. package/src/screens/BusinessDetailsFields.tsx +112 -0
  131. package/src/screens/BusinessDetailsStep.tsx +133 -145
  132. package/src/screens/BusinessDocumentSlot.tsx +5 -5
  133. package/src/screens/BusinessDocumentsStep.tsx +5 -56
  134. package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
  135. package/src/screens/BusinessPickedCard.tsx +86 -0
  136. package/src/screens/BusinessPickedSection.tsx +51 -0
  137. package/src/screens/BusinessRegistryPickers.tsx +67 -0
  138. package/src/screens/BusinessSandboxToggle.tsx +114 -0
  139. package/src/screens/BusinessSearch.tsx +199 -0
  140. package/src/screens/BusinessSearchResults.tsx +115 -0
  141. package/src/screens/CompanyInfoFields.tsx +91 -51
  142. package/src/screens/ConsentStep.tsx +37 -17
  143. package/src/screens/ContactActions.tsx +54 -0
  144. package/src/screens/ContactDestinationField.tsx +5 -0
  145. package/src/screens/ContactEntryPanel.tsx +75 -0
  146. package/src/screens/ContactVerificationStep.tsx +43 -42
  147. package/src/screens/CountrySelectStep.tsx +13 -147
  148. package/src/screens/IdTypeStep.tsx +12 -5
  149. package/src/screens/KeepLinksSheet.tsx +88 -0
  150. package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
  151. package/src/screens/KeyPeopleAwaitList.tsx +126 -0
  152. package/src/screens/KeyPeoplePending.tsx +154 -0
  153. package/src/screens/KeyPeopleSection.tsx +162 -0
  154. package/src/screens/KeyPeopleSectionsList.tsx +103 -0
  155. package/src/screens/KeyPeopleUboExemption.tsx +72 -0
  156. package/src/screens/KeyPersonCard.tsx +36 -4
  157. package/src/screens/KeyPersonForm.tsx +195 -36
  158. package/src/screens/KeyPersonKindToggle.tsx +68 -0
  159. package/src/screens/KeyPersonOwners.tsx +109 -0
  160. package/src/screens/KeyPersonRoleChips.tsx +68 -0
  161. package/src/screens/KeyPersonSheet.tsx +92 -108
  162. package/src/screens/LivenessAvatar.tsx +15 -3
  163. package/src/screens/LivenessStep.tsx +172 -52
  164. package/src/screens/NfcStep.tsx +14 -0
  165. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  166. package/src/screens/ProofOfAddressParts.tsx +151 -0
  167. package/src/screens/ProofOfAddressStep.tsx +58 -96
  168. package/src/screens/SubmittedBadge.tsx +25 -0
  169. package/src/screens/SubmittedError.tsx +64 -0
  170. package/src/screens/SubmittedResult.tsx +117 -0
  171. package/src/screens/SubmittedStep.tsx +82 -119
  172. package/src/screens/SubmittedSuccess.tsx +127 -0
  173. package/src/screens/SubmittedWaiting.tsx +45 -0
  174. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  175. package/src/screens/address/AddressIntroGate.tsx +148 -0
  176. package/src/screens/address/AddressMapStub.tsx +50 -0
  177. package/src/screens/address/AddressPinStep.tsx +178 -0
  178. package/src/screens/address/AddressReviewStep.tsx +162 -0
  179. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  180. package/src/screens/address/AddressSearchField.tsx +115 -0
  181. package/src/screens/address/AddressSearchStep.tsx +94 -0
  182. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  183. package/src/screens/address/DetailsSheet.tsx +175 -0
  184. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  185. package/src/screens/address/EntranceDropzone.tsx +178 -0
  186. package/src/screens/address/EntranceFraming.tsx +66 -0
  187. package/src/screens/address/EntrancePills.tsx +60 -0
  188. package/src/screens/address/FramedStreetView.tsx +155 -0
  189. package/src/screens/address/IntroDisclosures.tsx +210 -0
  190. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  191. package/src/screens/address/PinSummaryRow.tsx +105 -0
  192. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  193. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  194. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  195. package/src/screens/address/SearchResults.tsx +105 -0
  196. package/src/screens/address/SearchScreen.tsx +176 -0
  197. package/src/screens/address/SkipForNow.tsx +43 -0
  198. package/src/screens/address/StreetViewChrome.tsx +81 -0
  199. package/src/screens/address/detail-values.ts +22 -0
  200. package/src/screens/address/fix-source.ts +27 -0
  201. package/src/screens/address/index.ts +8 -0
  202. package/src/screens/address/meta.ts +44 -0
  203. package/src/screens/address/use-address-flow.ts +200 -0
  204. package/src/screens/address/use-label-pin.ts +80 -0
  205. package/src/screens/address/use-pin-actions.ts +192 -0
  206. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  207. package/src/screens/consent/model.ts +79 -10
  208. package/src/screens/liveness/CaptureRing.tsx +91 -0
  209. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  210. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  211. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  212. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  213. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  214. package/src/screens/liveness/index.ts +4 -0
  215. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  216. package/src/screens/nfc/NfcReadProgress.tsx +1 -0
  217. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  218. package/src/screens/useAddressPhotoAttach.ts +77 -0
  219. package/src/screens/useAwaitingPeople.ts +90 -0
  220. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  221. package/src/screens/usePoaAttach.ts +12 -15
  222. package/src/services/api-biometric.ts +37 -0
  223. package/src/services/api-types-biometric.ts +41 -0
  224. package/src/services/api-types.ts +251 -3
  225. package/src/services/api-verify-types.ts +90 -1
  226. package/src/services/api.ts +237 -3
  227. package/src/services/deviceMetadata.ts +1 -1
  228. package/src/services/errors.ts +10 -2
  229. package/src/services/location.ts +234 -0
  230. package/src/services/uploadErrors.ts +30 -0
  231. package/src/services/workflowGate.ts +8 -0
  232. package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
  233. package/src/store/address.ts +97 -0
  234. package/src/store/businessCheck.ts +113 -0
  235. package/src/store/derive.ts +44 -4
  236. package/src/store/kycStore.ts +352 -16
  237. package/src/store/serverConfig.ts +20 -0
  238. package/src/store/session.ts +227 -0
  239. package/src/store/state.ts +289 -2
  240. package/src/store/submit.ts +80 -13
  241. package/src/store/submitApplicant.ts +5 -11
  242. package/src/types/business.ts +23 -2
  243. package/src/types/config.ts +70 -2
  244. package/src/types/verification.ts +13 -1
  245. package/src/types/workflow.ts +112 -1
  246. package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
@@ -0,0 +1,210 @@
1
+ // Multi-ID flows on React Native. A PORT of the web SDK's lib/multi-id.ts,
2
+ // kept byte-for-byte in its logic: the server validates the pick sequence the
3
+ // client produced, so a client that computes options differently produces
4
+ // submissions the server rejects. If you change a rule here, change it in
5
+ // kyc-sdk-react/src/lib/multi-id.ts, kyc-core/src/lib/multi-id.ts and the
6
+ // dashboard's required-ids-model.ts in the same commit.
7
+ //
8
+ // Multi-ID flows: a workflow's `multiId` block asks for SEVERAL ID checks in
9
+ // one run — the applicant picks each slot's ID from what the admin allowed for
10
+ // THEIR country (a picked ID disappears from later slots), ONE selfie covers
11
+ // the whole run, and everything submits as ONE verification the server judges
12
+ // by the pass policy.
13
+ //
14
+ // Multi-region works: the country-select step comes first as usual, and the
15
+ // run then walks THAT country's slots (`countries[].multiIdSlots`). The pure
16
+ // helpers here MIRROR the server's lib/multi-id.ts exactly — the safe-options
17
+ // rule is what keeps an applicant from ever being offered a pick that would
18
+ // strand a later slot, and the server validates the same sequence.
19
+
20
+ export interface MultiIdConfig {
21
+ count: number;
22
+ minPassed: number;
23
+ }
24
+
25
+ export interface MultiIdCountry {
26
+ country: string;
27
+ idTypes?: string[];
28
+ /** This country's per-verification ID allowlists. */
29
+ multiIdSlots?: Array<{ idTypes?: string[] }>;
30
+ }
31
+
32
+ interface MultiIdSourceConfig {
33
+ multiId?: MultiIdConfig;
34
+ subjectType?: string;
35
+ /** The EFFECTIVE country (post country-select) — whose slots the run walks. */
36
+ country?: string;
37
+ countries?: MultiIdCountry[];
38
+ idTypes?: string[];
39
+ }
40
+
41
+ /** The workflow's multiId POLICY — null for ordinary flows and every KYB flow. */
42
+ export function multiIdConfigFrom(config: MultiIdSourceConfig): MultiIdConfig | null {
43
+ if (config.subjectType === 'business') return null;
44
+ const block = config.multiId;
45
+ if (!block || typeof block.count !== 'number') return null;
46
+ const count = Math.trunc(block.count);
47
+ // REJECT an out-of-range count rather than clamping it. The server returns
48
+ // null here, so clamping made the client walk 3 checks for a config the
49
+ // server does not consider multi-ID at all — and clamping `minPassed`
50
+ // against the RAW count could ask for 9 of 3 to pass, which nothing can
51
+ // satisfy. Same rule, both sides, or the client builds submissions the
52
+ // server rejects.
53
+ if (!Number.isInteger(count) || count < 2 || count > 3) return null;
54
+ return {
55
+ count,
56
+ minPassed:
57
+ typeof block.minPassed === 'number' && Number.isInteger(block.minPassed)
58
+ ? Math.min(Math.max(block.minPassed, 1), count)
59
+ : count,
60
+ };
61
+ }
62
+
63
+ /** The country entry the run walks: the EFFECTIVE country's, else the legacy
64
+ * single-country shape. */
65
+ export function multiIdCountryEntry(config: MultiIdSourceConfig): MultiIdCountry | null {
66
+ const country = config.country;
67
+ if (config.countries && config.countries.length > 0) {
68
+ return config.countries.find((c) => c.country === country) ?? null;
69
+ }
70
+ return country ? { country, idTypes: config.idTypes } : null;
71
+ }
72
+
73
+ /**
74
+ * The ID types the picked country offers: its pinned list, else everything the
75
+ * server granted there.
76
+ */
77
+ export function multiIdOfferedTypes(
78
+ config: MultiIdSourceConfig,
79
+ serverIdTypes: Array<{ country: string; idType: string }>,
80
+ ): string[] {
81
+ const entry = multiIdCountryEntry(config);
82
+ if (!entry) return [];
83
+ if (entry.idTypes && entry.idTypes.length > 0) return entry.idTypes;
84
+ return serverIdTypes.filter((row) => row.country === entry.country).map((row) => row.idType);
85
+ }
86
+
87
+ /** Per-slot option lists (a pinned slot keeps its list; others offer everything). */
88
+ export function multiIdSlotOptions(
89
+ count: number,
90
+ slots: Array<{ idTypes?: string[] }> | undefined,
91
+ offered: string[],
92
+ ): string[][] {
93
+ const offeredSet = new Set(offered);
94
+ return Array.from({ length: count }, (_, i) => {
95
+ const pinned = slots?.[i]?.idTypes;
96
+ const base = pinned && pinned.length > 0 ? pinned : offered;
97
+ return base.filter((t) => offeredSet.has(t));
98
+ });
99
+ }
100
+
101
+ /** The first reachable dead-end across pick orders, or null. */
102
+ export function multiIdFirstDeadEnd(
103
+ slotOptions: string[][],
104
+ ): { picks: string[]; slotIndex: number } | null {
105
+ const walk = (index: number, picked: string[]): { picks: string[]; slotIndex: number } | null => {
106
+ if (index >= slotOptions.length) return null;
107
+ const available = slotOptions[index]!.filter((t) => !picked.includes(t));
108
+ if (available.length === 0) return { picks: picked, slotIndex: index };
109
+ for (const pick of available) {
110
+ const deadEnd = walk(index + 1, [...picked, pick]);
111
+ if (deadEnd) return deadEnd;
112
+ }
113
+ return null;
114
+ };
115
+ return walk(0, []);
116
+ }
117
+
118
+ /** The picks a slot may SAFELY offer: unused AND non-stranding. */
119
+ export function multiIdSafeOptions(
120
+ slotOptions: string[][],
121
+ slotIndex: number,
122
+ picked: string[],
123
+ ): string[] {
124
+ const remaining = slotOptions.slice(slotIndex + 1);
125
+ return (slotOptions[slotIndex] ?? [])
126
+ .filter((t) => !picked.includes(t))
127
+ .filter(
128
+ (t) =>
129
+ multiIdFirstDeadEnd(
130
+ remaining.map((opts) => opts.filter((o) => o !== t && !picked.includes(o))),
131
+ ) === null,
132
+ );
133
+ }
134
+
135
+ export interface MultiIdPlan {
136
+ count: number;
137
+ minPassed: number;
138
+ /** Which slot is being walked (clamped; equals count once every slot committed). */
139
+ index: number;
140
+ /** The current slot is the final one. */
141
+ last: boolean;
142
+ /** ID types committed so far, in order. */
143
+ picked: string[];
144
+ /** What the CURRENT slot's picker may offer. */
145
+ safeOptions: string[];
146
+ }
147
+
148
+ /** The active plan, or null for ordinary flows. */
149
+ export function multiIdPlan(
150
+ config: MultiIdSourceConfig,
151
+ state: { multiIdSlotIndex: number; multiIdSlots: Array<{ idType: string }> },
152
+ serverIdTypes: Array<{ country: string; idType: string }>,
153
+ ): MultiIdPlan | null {
154
+ const cfg = multiIdConfigFrom(config);
155
+ if (!cfg) return null;
156
+ const offered = multiIdOfferedTypes(config, serverIdTypes);
157
+ const options = multiIdSlotOptions(cfg.count, multiIdCountryEntry(config)?.multiIdSlots, offered);
158
+ const picked = state.multiIdSlots.map((s) => s.idType);
159
+ const index = Math.min(Math.max(state.multiIdSlotIndex, 0), cfg.count);
160
+ return {
161
+ count: cfg.count,
162
+ minPassed: cfg.minPassed,
163
+ index,
164
+ last: index >= cfg.count - 1,
165
+ picked,
166
+ safeOptions: index < cfg.count ? multiIdSafeOptions(options, index, picked) : [],
167
+ };
168
+ }
169
+
170
+ /** Which evidence step one slot's ID opens on. */
171
+ export function multiIdEvidenceStep(
172
+ def: { requiresDocumentCapture: boolean } | null | undefined,
173
+ ): 'id-input' | 'document-capture' {
174
+ return def && def.requiresDocumentCapture === false ? 'id-input' : 'document-capture';
175
+ }
176
+
177
+ /**
178
+ * The committed slots as the WIRE sees them — an explicit whitelist, because
179
+ * the slots also carry local preview images for the back journey and those must
180
+ * never reach the submission or the session-progress blob. Whitelist, not
181
+ * spread: a field added to the slot later cannot leak by default.
182
+ */
183
+ export function multiIdWireSlots(
184
+ slots: Array<{
185
+ idType: string;
186
+ idNumber?: string;
187
+ documentFront?: string;
188
+ documentBack?: string;
189
+ documentFrontVideo?: string;
190
+ documentBackVideo?: string;
191
+ }>,
192
+ ): Array<{
193
+ idType: string;
194
+ idNumber?: string;
195
+ documentFront?: string;
196
+ documentBack?: string;
197
+ documentFrontVideo?: string;
198
+ documentBackVideo?: string;
199
+ }> {
200
+ // The chip is attached by the caller after this whitelist (it is a payload,
201
+ // not a mediaId), so it is absent here and not stripped.
202
+ return slots.map((s) => ({
203
+ idType: s.idType,
204
+ ...(s.idNumber ? { idNumber: s.idNumber } : {}),
205
+ ...(s.documentFront ? { documentFront: s.documentFront } : {}),
206
+ ...(s.documentBack ? { documentBack: s.documentBack } : {}),
207
+ ...(s.documentFrontVideo ? { documentFrontVideo: s.documentFrontVideo } : {}),
208
+ ...(s.documentBackVideo ? { documentBackVideo: s.documentBackVideo } : {}),
209
+ }));
210
+ }
@@ -0,0 +1,27 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The Proof of Address step's country gate.
3
+ //
4
+ // On the ADDRESS scope the declared country is the applicant's own claim about
5
+ // their market: it picks the document kinds on offer, the PoA vendor market and
6
+ // rides the submission as the verification's country, and the scope seeds none
7
+ // (see AddressCountryControl). A document uploaded with no country behind it is
8
+ // not yet a complete answer, so Continue holds until one is declared (user
9
+ // decision 2026-09-08). Elsewhere the flow's own country stands and the gate
10
+ // never bites; an org that accepts exactly ONE country has the control show it
11
+ // as a settled fact, which counts as declared.
12
+ //
13
+ // A THREE-WAY MIRROR of the web SDK's lib/poa-country-gate.ts and Flutter's
14
+ // config/poa_country_gate.dart; change the rule in one and change all three.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Whether the Proof of Address step may continue as far as the COUNTRY is
18
+ * concerned (the upload has its own gate). */
19
+ export function poaCountryDeclared(facts: {
20
+ scope: string | null;
21
+ selectedCountry: string | null | undefined;
22
+ offered: readonly string[];
23
+ }): boolean {
24
+ if (facts.scope !== 'address') return true;
25
+ if (facts.selectedCountry?.trim()) return true;
26
+ return facts.offered.length === 1;
27
+ }
@@ -0,0 +1,130 @@
1
+ // Narrowing a flow to the steps a reviewer asked the applicant to redo.
2
+ //
3
+ // A reviewer looked at a finished verification and sent it back — usually
4
+ // because one thing was unreadable, not because everything was wrong. Making
5
+ // somebody retake a passport photo is reasonable; making them redo consent, the
6
+ // ID picker, liveness and a questionnaire to fix that photo is how you lose them.
7
+ //
8
+ // The instruction rides the session's config snapshot (`config.resubmit`), which
9
+ // is how every other per-session flow instruction travels. An SDK that predates
10
+ // this simply does not read the key and runs the whole flow, which is the safe
11
+ // degradation: asking for too much is recoverable, silently skipping a step the
12
+ // reviewer wanted is not.
13
+ import type { KYCStep } from '../types/config';
14
+
15
+ export interface ResubmitConfig {
16
+ /** Steps to redo, in flow order. Never empty — the server omits the key instead. */
17
+ steps: string[];
18
+ /** Reviewer's note to the applicant. */
19
+ message?: string | null;
20
+ }
21
+
22
+ /**
23
+ * Steps that are always kept, whatever the reviewer ticked.
24
+ *
25
+ * `consent` because a flow with no first screen is disorienting, and it is where
26
+ * the redo is explained. `submitted` because a flow has to end somewhere.
27
+ * Neither is something the applicant is being asked to redo — they are the
28
+ * frame around what is.
29
+ */
30
+ const ALWAYS: KYCStep[] = ['consent', 'submitted'];
31
+
32
+ /**
33
+ * One ID's evidence — a FAMILY, not alternatives a reviewer picks between.
34
+ *
35
+ * Which member a flow contains depends on the ID type: a number-only ID has
36
+ * `id-input`, a document ID has `document-capture` (and maybe `nfc`). At the
37
+ * moment the order is built nobody has chosen one yet, so the flow is shaped by
38
+ * a default. A plan naming `id-input` against an order still shaped for
39
+ * `document-capture` therefore matched NOTHING, fell through to the safety net
40
+ * below, and silently ran the entire flow — which is exactly the bug a reviewer
41
+ * sees as "I asked for the ID and it made them do everything again".
42
+ *
43
+ * Asking for one member asks for whichever this flow turns out to have.
44
+ */
45
+ const EVIDENCE: KYCStep[] = ['id-input', 'document-capture', 'nfc'];
46
+
47
+ /**
48
+ * Steps a narrowed flow keeps regardless, because without them it cannot
49
+ * produce a submission at all.
50
+ *
51
+ * A resubmission is a NEW verification on a FRESH session: nothing is carried
52
+ * forward from the one being redone, so the applicant must still say which ID
53
+ * this is and supply it. `POST /verify` requires an `idType`, and a number-only
54
+ * ID requires the number with it.
55
+ *
56
+ * So narrowing removes the things arranged AROUND the identity — liveness,
57
+ * proof of address, the questionnaire, contact checks — and never the identity
58
+ * itself. The alternative is a two-screen flow that collects a photo and then
59
+ * fails to submit, which is worse for the applicant than being asked for one
60
+ * extra screen.
61
+ */
62
+ const INDIVIDUAL_REQUIRED: KYCStep[] = ['id-type', ...EVIDENCE];
63
+ const BUSINESS_REQUIRED: KYCStep[] = ['business-details'];
64
+
65
+ /**
66
+ * Narrow a full step order to the redo, preserving flow order.
67
+ *
68
+ * Order comes from `order`, never from the reviewer's list: they ticked
69
+ * checkboxes, and walking somebody through liveness before document capture
70
+ * because that is the order the boxes were ticked in would be nonsense.
71
+ *
72
+ * Returns the ORIGINAL order untouched when the instruction is absent, empty, or
73
+ * matches nothing we know. That last case matters: a server that learns a new
74
+ * step name before this SDK does must not produce a two-screen flow that
75
+ * collects nothing.
76
+ */
77
+ export function applyResubmitSteps(
78
+ order: KYCStep[],
79
+ resubmit: ResubmitConfig | undefined | null,
80
+ ): KYCStep[] {
81
+ const asked = resubmit?.steps;
82
+ if (!asked?.length) return order;
83
+
84
+ const wantsEvidence = asked.some((step) => EVIDENCE.includes(step as KYCStep));
85
+
86
+ // Does the instruction name anything this flow actually has? If not, it came
87
+ // from a server that knows a step name this SDK does not — and narrowing on it
88
+ // would quietly drop whatever was really asked for. Checked BEFORE the
89
+ // required steps are added, or those alone would make every unknown plan look
90
+ // recognised and turn the safety net below into dead code.
91
+ const recognised = wantsEvidence || asked.some((step) => order.includes(step as KYCStep));
92
+ if (!recognised) return order;
93
+
94
+ const wanted = new Set<string>(asked);
95
+ if (wantsEvidence) for (const step of EVIDENCE) wanted.add(step);
96
+ for (const step of order.includes('business-details') ? BUSINESS_REQUIRED : INDIVIDUAL_REQUIRED) {
97
+ wanted.add(step);
98
+ }
99
+
100
+ const narrowed = order.filter((step) => wanted.has(step) || ALWAYS.includes(step));
101
+
102
+ // Nothing but the frame survived, so the instruction named steps this flow
103
+ // does not contain. Run everything rather than nothing.
104
+ const collects = narrowed.some((step) => !ALWAYS.includes(step));
105
+ return collects ? narrowed : order;
106
+ }
107
+
108
+ /** Whether this mount is a targeted redo, for the wording on screen. */
109
+ export function isResubmission(resubmit: ResubmitConfig | undefined | null): boolean {
110
+ return Boolean(resubmit?.steps?.length);
111
+ }
112
+
113
+ /**
114
+ * The reviewer's note to the applicant, when this mount is a targeted redo.
115
+ *
116
+ * The dashboard's send-back dialog asks a reviewer to explain the problem — its
117
+ * placeholder is literally "Your document photo was too dark to read, please
118
+ * retake it in good light." The server stamps that note onto the session and
119
+ * every SDK parses it into config, and until now NONE of them displayed it. So
120
+ * a send-back reached the applicant as a flow that had silently lost most of
121
+ * its steps, with nothing saying why they were back or what to do differently.
122
+ *
123
+ * Returns null when there is nothing to show, so a caller can render this
124
+ * unconditionally.
125
+ */
126
+ export function resubmitNote(resubmit: ResubmitConfig | undefined | null): string | null {
127
+ if (!isResubmission(resubmit)) return null;
128
+ const note = resubmit?.message?.trim();
129
+ return note && note.length > 0 ? note : null;
130
+ }
@@ -0,0 +1,116 @@
1
+ import type { VerificationOutcome } from './result-wait';
2
+ import type { BiometricCopyText } from '../config/biometricOptions';
3
+
4
+ // ─── What the terminal screens say ──────────────────────────────────────────
5
+ //
6
+ // Pure so it is testable without React. The server's own reason wins on a
7
+ // decline or an error when it sent one: it is written for the applicant.
8
+ // UK English, no em dashes (user-facing copy rule).
9
+
10
+ export type ResultTone = 'success' | 'error' | 'info';
11
+
12
+ export interface ResultCopy {
13
+ tone: ResultTone;
14
+ title: string;
15
+ description: string;
16
+ }
17
+
18
+ export interface WaitingCopy {
19
+ title: string;
20
+ description: string;
21
+ }
22
+
23
+ /**
24
+ * The ONE loading screen after the capture. On a re-authentication that waits
25
+ * for its verdict it spans the selfie upload, the submission and the poll, so
26
+ * it names the check rather than any of the three steps behind it. A retry in
27
+ * flight replaces the description, never the title: the person is still
28
+ * waiting for the same thing. `override` is the org's own words for the
29
+ * screen (lib/biometric-copy.ts), field by field over the default.
30
+ */
31
+ export function describeWaiting(opts: {
32
+ scope: string | null;
33
+ waitsForResult: boolean;
34
+ retry?: { attempt: number; total: number } | null;
35
+ override?: BiometricCopyText | null;
36
+ }): WaitingCopy {
37
+ const base = withOverride(waitingCopyFor(opts.scope, opts.waitsForResult), opts.override);
38
+ if (opts.retry) {
39
+ return { title: base.title, description: `Connection issue, retrying (${opts.retry.attempt}/${opts.retry.total}).` };
40
+ }
41
+ return base;
42
+ }
43
+
44
+ function waitingCopyFor(scope: string | null, waitsForResult: boolean): WaitingCopy {
45
+ if (scope === 'biometric-authentication') {
46
+ return waitsForResult
47
+ ? { title: "Checking it's you", description: 'Matching your selfie against the photo on record. This usually takes a few seconds.' }
48
+ : { title: 'Sending your face check', description: 'This only takes a moment.' };
49
+ }
50
+ if (scope === 'biometric-enrollment') {
51
+ return { title: 'Saving your selfie', description: 'It becomes the reference for your future face checks.' };
52
+ }
53
+ return { title: 'Submitting your verification', description: 'Please wait a moment.' };
54
+ }
55
+
56
+ /** The org's own words for a screen, over the default, field by field. */
57
+ function withOverride<T extends { title: string; description: string }>(base: T, override?: BiometricCopyText | null): T {
58
+ if (!override) return base;
59
+ return {
60
+ ...base,
61
+ ...(override.title ? { title: override.title } : {}),
62
+ ...(override.description ? { description: override.description } : {}),
63
+ };
64
+ }
65
+
66
+ /** What the person is told, per outcome. The server's own reason wins on a
67
+ * decline or an error when it sent one; it is written for the applicant.
68
+ * `copy` is the org's own words for the two verdict screens: on a decline
69
+ * its description wins even over the server's reason, since the org chose
70
+ * to say that. */
71
+ export function describeOutcome(
72
+ outcome: VerificationOutcome,
73
+ copy?: { verified?: BiometricCopyText | null; declined?: BiometricCopyText | null },
74
+ ): ResultCopy {
75
+ if (outcome.kind === 'timeout') {
76
+ return {
77
+ tone: 'info',
78
+ title: 'Still checking',
79
+ description: "This is taking longer than usual. You'll be notified as soon as it's done.",
80
+ };
81
+ }
82
+ switch (outcome.status) {
83
+ case 'approved':
84
+ return withOverride(
85
+ { tone: 'success', title: "You're verified", description: 'Your face matched the photo on record.' },
86
+ copy?.verified,
87
+ );
88
+ case 'declined':
89
+ return withOverride(
90
+ {
91
+ tone: 'error',
92
+ title: "We couldn't confirm it's you",
93
+ description: outcome.reason ?? "Your face didn't match the photo on record.",
94
+ },
95
+ copy?.declined,
96
+ );
97
+ case 'in_review':
98
+ return {
99
+ tone: 'info',
100
+ title: 'Under review',
101
+ description: "A reviewer will take a look. You'll be notified of the outcome.",
102
+ };
103
+ case 'error':
104
+ return {
105
+ tone: 'error',
106
+ title: 'Something went wrong',
107
+ description: outcome.reason ?? "We couldn't complete your check. Please try again in a moment.",
108
+ };
109
+ default:
110
+ return {
111
+ tone: 'info',
112
+ title: 'Check submitted',
113
+ description: "You'll be notified of the result.",
114
+ };
115
+ }
116
+ }
@@ -0,0 +1,53 @@
1
+ import type { SessionStatus, VerificationStatusResponse } from '../services/api-types';
2
+
3
+ // ─── Waiting for a verdict in the flow ──────────────────────────────────────
4
+ //
5
+ // The platform is fire-and-forget: /verify answers in milliseconds and the
6
+ // worker settles the check afterwards. A re-authentication is the one flow
7
+ // whose verdict the person is waiting for RIGHT THERE, so the submitted step
8
+ // polls the publishable status endpoint (state + reason, never result data)
9
+ // until the check leaves its pending states. Pure and injectable, like the
10
+ // presence watch wait: the screen owns nothing but the rendering.
11
+
12
+ export const RESULT_WAIT_MS = 60 * 1000;
13
+ export const RESULT_POLL_MS = 1500;
14
+
15
+ /** The states a submitted check passes through before it settles. */
16
+ const PENDING: ReadonlySet<SessionStatus> = new Set(['not_started', 'in_progress', 'processing']);
17
+
18
+ export type VerificationOutcome =
19
+ | { kind: 'settled'; status: SessionStatus; reason: string | null; reasonCode: string | null }
20
+ | { kind: 'timeout' };
21
+
22
+ export interface AwaitOutcomeDeps {
23
+ /** One status read; null on any failure (the wait keeps going). */
24
+ fetchStatus: () => Promise<VerificationStatusResponse | null>;
25
+ sleep?: (ms: number) => Promise<void>;
26
+ now?: () => number;
27
+ waitMs?: number;
28
+ pollMs?: number;
29
+ }
30
+
31
+ export function isPendingStatus(status: SessionStatus): boolean {
32
+ return PENDING.has(status);
33
+ }
34
+
35
+ /**
36
+ * Poll until the check settles or the budget runs out. A failed read is not
37
+ * a verdict: it is skipped and the next poll tries again, so a network blip
38
+ * mid-wait never reads as an outcome.
39
+ */
40
+ export async function awaitVerificationOutcome(deps: AwaitOutcomeDeps): Promise<VerificationOutcome> {
41
+ const sleep = deps.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
42
+ const now = deps.now ?? Date.now;
43
+ const deadline = now() + (deps.waitMs ?? RESULT_WAIT_MS);
44
+ const pollMs = deps.pollMs ?? RESULT_POLL_MS;
45
+ for (;;) {
46
+ const read = await deps.fetchStatus();
47
+ if (read && !isPendingStatus(read.status)) {
48
+ return { kind: 'settled', status: read.status, reason: read.reason ?? null, reasonCode: read.reasonCode ?? null };
49
+ }
50
+ if (now() >= deadline) return { kind: 'timeout' };
51
+ await sleep(pollMs);
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ // The review card's map falls back in the SAME order on every SDK: the picture
2
+ // (the Maps Static API through the server), then the framed Google map the pin
3
+ // step drew, then the built-in tiles. Pure, so a test can pin the order — the
4
+ // Flutter card skipped the middle rung and confirmed the address on
5
+ // OpenStreetMap where RN confirmed it on Google (user report 2026-09-08).
6
+ // Mirrors kyc-sdk-flutter's reviewMapSurface; keep the two in lockstep.
7
+
8
+ export type ReviewMapSurface = 'stub' | 'picture' | 'pending' | 'framed' | 'builtIn';
9
+
10
+ export function reviewMapSurface(facts: {
11
+ vendorsStubbed: boolean;
12
+ /** The picture's bytes have landed. */
13
+ hasStaticMap: boolean;
14
+ /** The picture was refused (a project without the Static API 404s). */
15
+ staticMapFailed: boolean;
16
+ /** A server-minted maps frame URL this install can render. */
17
+ hasFrame: boolean;
18
+ }): ReviewMapSurface {
19
+ if (facts.vendorsStubbed) return 'stub';
20
+ if (facts.hasStaticMap) return 'picture';
21
+ // Nothing to draw yet and no verdict either: hold the picture's space rather
22
+ // than flashing the live map for the half second before the bytes land.
23
+ if (!facts.staticMapFailed) return 'pending';
24
+ if (facts.hasFrame) return 'framed';
25
+ return 'builtIn';
26
+ }
@@ -0,0 +1,31 @@
1
+ // Workflow SCOPE — what a flow verifies about the subject (mirror of the
2
+ // server's lib/workflows/scope.ts and the web SDK's lib/scope.ts; keep the
3
+ // three in lockstep). Absent = the full verification.
4
+
5
+ export type WorkflowScope =
6
+ | 'address'
7
+ | 'biometric-authentication'
8
+ | 'biometric-enrollment'
9
+ | 'questionnaire'
10
+ | 'contact';
11
+
12
+ /** The marker idType a scoped submission carries (the KYB product-in-idType
13
+ * convention — the server requires the matching published workflow). */
14
+ export const SCOPE_ID_TYPES: Record<WorkflowScope, string> = {
15
+ address: 'address',
16
+ 'biometric-authentication': 'biometric-auth',
17
+ 'biometric-enrollment': 'biometric-enroll',
18
+ questionnaire: 'questionnaire',
19
+ contact: 'contact',
20
+ };
21
+
22
+ export function configScope(config: { scope?: WorkflowScope | string }): WorkflowScope | null {
23
+ const value = config.scope;
24
+ return value && value in SCOPE_ID_TYPES ? (value as WorkflowScope) : null;
25
+ }
26
+
27
+ /** The biometric scopes run the liveness capture; every other scope has no
28
+ * camera step at all. */
29
+ export function isFaceScope(scope: WorkflowScope | null): boolean {
30
+ return scope === 'biometric-authentication' || scope === 'biometric-enrollment';
31
+ }
@@ -0,0 +1,50 @@
1
+ import { Dimensions, Platform } from 'react-native';
2
+ import { initialWindowMetrics } from 'react-native-safe-area-context';
3
+
4
+ // Apple's grammar for nested corners is CONCENTRIC: an inset card's radius is
5
+ // the DISPLAY's corner radius minus the gap between card and screen edge —
6
+ // that is why the system share sheet's curve runs parallel to the phone's.
7
+ // No public API exposes the display radius, so iOS resolves it from the
8
+ // model's logical screen size (the per-device values Apple ships, from
9
+ // UIScreen's private _displayCornerRadius, in points). Unknown hardware falls
10
+ // back on whether the display is rounded at all: home-indicator devices are,
11
+ // square-corner devices are not.
12
+
13
+ /** `short x long` logical points → display corner radius in points. */
14
+ const IOS_RADII: Record<string, number> = {
15
+ // X / XS / 11 Pro are 39 on the same canvas as the 12–13 mini's 44; the
16
+ // minis are the ones still in pockets, so the shared key takes their value.
17
+ '375x812': 44,
18
+ '414x896': 41.5, // XR / 11 / XS Max / 11 Pro Max
19
+ '390x844': 47.33, // 12 / 12 Pro / 13 / 13 Pro / 14
20
+ '428x926': 53.33, // 12–13 Pro Max / 14 Plus
21
+ '393x852': 55, // 14 Pro / 15 / 15 Pro / 16 / 16e
22
+ '430x932': 55, // 14 Pro Max / 15 Plus / 15 Pro Max / 16 Plus
23
+ '402x874': 62, // 16 Pro
24
+ '440x956': 62, // 16 Pro Max
25
+ };
26
+
27
+ /**
28
+ * The display's own corner radius, in points/dp. 0 means a square-cornered
29
+ * screen (or one we cannot judge), and the caller picks its flat-world look.
30
+ */
31
+ export function displayCornerRadius(): number {
32
+ const { width, height } = Dimensions.get('screen');
33
+ const key = `${Math.round(Math.min(width, height))}x${Math.round(Math.max(width, height))}`;
34
+ const bottomInset = initialWindowMetrics?.insets?.bottom ?? 0;
35
+
36
+ if (Platform.OS === 'ios') {
37
+ const known = IOS_RADII[key];
38
+ if (known != null) return known;
39
+ // Unlisted model: a home indicator means a rounded display; 47 is the
40
+ // middle of the modern range.
41
+ return bottomInset > 0 ? 47 : 0;
42
+ }
43
+
44
+ // Android has no cheap signal (the RoundedCorner API needs native code).
45
+ // A small bottom inset is gesture navigation — overwhelmingly the modern,
46
+ // rounded-display phones — while ~48dp is the opaque 3-button bar, which
47
+ // says nothing. Modest on purpose: an under-estimate still looks composed,
48
+ // an over-estimate looks like a mistake.
49
+ return bottomInset > 0 && bottomInset <= 32 ? 32 : 0;
50
+ }