@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,30 @@
1
+ import { KYCApiError } from './api';
2
+ import { UPLOAD_HINT } from '../config/uploadLimits';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Why an upload failed, in words the person can act on.
6
+ //
7
+ // Every upload used to fail as "we could not upload that document", which tells
8
+ // the applicant nothing about whether to pick a smaller file, a different
9
+ // format, or simply try again on better signal. The server already says which
10
+ // it is; this reads its answer and hands back the sentence that matches.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** The refusal, as something the applicant can do something about. */
14
+ export function uploadFailureMessage(error: unknown): string {
15
+ const status = error instanceof KYCApiError ? error.statusCode : null;
16
+ const raw = error instanceof Error ? error.message.toLowerCase() : '';
17
+
18
+ if (status === 413 || raw.includes('too large')) {
19
+ return `That file is too large. ${UPLOAD_HINT}`;
20
+ }
21
+ if (raw.includes('mimetype') || raw.includes('mime type') || raw.includes('unsupported')) {
22
+ return `That file type is not supported. ${UPLOAD_HINT}`;
23
+ }
24
+ // A 4xx we have no better words for still says the file is the problem, so
25
+ // the person retries with a DIFFERENT one rather than the same one twice.
26
+ if (status !== null && status >= 400 && status < 500) {
27
+ return `We could not read that file. ${UPLOAD_HINT}`;
28
+ }
29
+ return 'We could not upload that document. Please check your connection and try again.';
30
+ }
@@ -102,6 +102,14 @@ export async function resolveWorkflow(
102
102
  status: 'ready',
103
103
  idTypes: res.idTypes,
104
104
  branding: res.branding,
105
+ geoCountry: res.geoCountry,
106
+ // The resolution route does not serve these yet, so a workflow mount
107
+ // reads undefined and the address flow offers no search screen. That
108
+ // degrades correctly, and the passthrough means it starts working the
109
+ // day the server carries them.
110
+ addressSearch: res.addressSearch,
111
+ addressSearchMode: res.addressSearchMode,
112
+ mapsFrameUrl: res.mapsFrameUrl ?? null,
105
113
  environment: res.environment,
106
114
  fatal: false,
107
115
  },
@@ -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,97 @@
1
+ import type { AddressState } from './state';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Restoring a saved address pin.
5
+ //
6
+ // The whole address object is written into session progress verbatim, so the
7
+ // snapshot was written by WHATEVER build saved it: coerce every field back to
8
+ // its declared type and degrade to restoring less, never to breaking the flow.
9
+ //
10
+ // Deliberately does NOT restore the device fix (deviceLat / deviceLng /
11
+ // deviceAccuracy / capturedAt). That is the attest-presence reading, and it is
12
+ // taken fresh at confirm — a stale one would claim the applicant stood at the
13
+ // address on a day they did not.
14
+ //
15
+ // Split from session.ts per the 200-line rule. A MIRROR of the web SDK's
16
+ // RESTORE_PROGRESS address branch and the Flutter port.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function str(v: unknown): string | null {
20
+ return typeof v === 'string' ? v : null;
21
+ }
22
+
23
+ /** The `parts` breakdown, when the snapshot carries an object for it. */
24
+ function restoreParts(v: unknown): Pick<AddressState, 'parts'> {
25
+ if (!v || typeof v !== 'object') return {};
26
+ const p = v as Record<string, unknown>;
27
+ return {
28
+ parts: {
29
+ street: str(p['street']),
30
+ area: str(p['area']),
31
+ city: str(p['city']),
32
+ state: str(p['state']),
33
+ postcode: str(p['postcode']),
34
+ country: str(p['country']),
35
+ },
36
+ };
37
+ }
38
+
39
+ /** The picked-label anchor, only when it is a real coordinate pair. */
40
+ function restorePickedAt(v: unknown): Pick<AddressState, 'pickedAt'> {
41
+ if (!v || typeof v !== 'object') return {};
42
+ const p = v as Record<string, unknown>;
43
+ return typeof p['lat'] === 'number' && typeof p['lng'] === 'number'
44
+ ? { pickedAt: { lat: p['lat'], lng: p['lng'] } }
45
+ : {};
46
+ }
47
+
48
+ /** The Street View frame, only when every field of it survived. A partial
49
+ * frame cannot be re-rendered, and half of one is not a capture. */
50
+ function restoreStreetView(v: unknown): Pick<AddressState, 'streetView'> {
51
+ if (!v || typeof v !== 'object') return {};
52
+ const sv = v as Record<string, unknown>;
53
+ return typeof sv['panoId'] === 'string' &&
54
+ typeof sv['heading'] === 'number' &&
55
+ typeof sv['pitch'] === 'number' &&
56
+ typeof sv['fov'] === 'number'
57
+ ? {
58
+ streetView: {
59
+ panoId: sv['panoId'],
60
+ heading: sv['heading'],
61
+ pitch: sv['pitch'],
62
+ fov: sv['fov'],
63
+ },
64
+ }
65
+ : {};
66
+ }
67
+
68
+ /**
69
+ * Rebuild the address from a progress snapshot. The caller has already checked
70
+ * that `lat` and `lng` are numbers — without those there is no pin, and there
71
+ * is nothing to restore.
72
+ */
73
+ export function restoreAddress(a: Record<string, unknown>): AddressState {
74
+ return {
75
+ lat: a['lat'] as number,
76
+ lng: a['lng'] as number,
77
+ accuracy: typeof a['accuracy'] === 'number' ? a['accuracy'] : null,
78
+ directions: typeof a['directions'] === 'string' ? a['directions'] : '',
79
+ propertyName: typeof a['propertyName'] === 'string' ? a['propertyName'] : '',
80
+ propertyNumber: typeof a['propertyNumber'] === 'string' ? a['propertyNumber'] : '',
81
+ // The resolved line and its breakdown survive a restart too. Dropping them
82
+ // is why a resumed session showed raw coordinates where the applicant had
83
+ // picked an address.
84
+ ...(typeof a['label'] === 'string' ? { label: a['label'] } : {}),
85
+ ...(typeof a['street'] === 'string' ? { street: a['street'] } : {}),
86
+ // The edit-details claims survive a restart like every other typed field.
87
+ ...(typeof a['unit'] === 'string' ? { unit: a['unit'] } : {}),
88
+ ...(typeof a['neighbourhood'] === 'string' ? { neighbourhood: a['neighbourhood'] } : {}),
89
+ ...(typeof a['city'] === 'string' ? { city: a['city'] } : {}),
90
+ ...(typeof a['state'] === 'string' ? { state: a['state'] } : {}),
91
+ ...(typeof a['postcode'] === 'string' ? { postcode: a['postcode'] } : {}),
92
+ ...(a['labelKept'] === true ? { labelKept: true } : {}),
93
+ ...restorePickedAt(a['pickedAt']),
94
+ ...restoreParts(a['parts']),
95
+ ...restoreStreetView(a['streetView']),
96
+ };
97
+ }
@@ -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
+ }
@@ -7,14 +7,19 @@
7
7
  // keeps a single answer to each of those questions.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
+ import { configScope } from '../lib/scope';
10
11
  import { requiresDocumentCapture, supportsNfcChip } from '../config/idTypes';
11
12
  import {
13
+ buildStepOrder,
12
14
  getStepProgress,
13
15
  nextStepInOrder,
14
16
  previousStepInOrder,
15
17
  type StepOrderOptions,
16
18
  } from '../config/stepOrder';
17
19
  import { hasProofOfAddressStep } from '../config/proofOfAddress';
20
+ import { addressFlowFor, hasAddressCollectionStep } from '../config/addressCollection';
21
+ import { webViewAvailable } from '../lib/webview-available';
22
+ import { addressVendorsStubbed } from '../lib/address-flow';
18
23
  import { businessProductsForCountry, isBusinessFlow } from '../config/business';
19
24
  import {
20
25
  hasApplicantVerification,
@@ -22,6 +27,7 @@ import {
22
27
  hasKeyPeopleCollection,
23
28
  } from '../config/businessSteps';
24
29
  import { hasActiveQuestionnaire } from '../config/questionnaire';
30
+ import { hasConsentStep, openingStepOf } from '../config/consentStep';
25
31
  import { applicantSelfCountry, keyPeoplePayload } from '../config/keyPeople';
26
32
  import { featuresFor } from './serverConfig';
27
33
  import type { VerifyRequest } from '../services/api';
@@ -58,6 +64,7 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
58
64
  const idType = state.selectedIdType;
59
65
  return {
60
66
  isBusiness: isBusinessFlow(config),
67
+ scope: configScope(config),
61
68
  business: config.business,
62
69
  hasDocCapture: idType ? requiresDocumentCapture(idType) : true,
63
70
  hasNfc: nfcEnabled(state),
@@ -72,10 +79,25 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
72
79
  hasCountrySelect:
73
80
  countrySelectOptions(state).length > 1 &&
74
81
  applicantSelfCountry(state.businessApplication) === null,
82
+ hasConsent: hasConsentStep(config),
75
83
  hasEmailVerification: config.emailVerification?.enabled === true,
76
84
  hasPhoneVerification: config.phoneVerification?.enabled === true,
77
85
  hasPoa: hasProofOfAddressStep(config.proofOfAddress),
86
+ hasAddressCollection: hasAddressCollectionStep(config.addressCollection),
87
+ // An absent search flag means no search SCREEN, never an error: the
88
+ // applicant places the pin by hand, the fallback every address failure
89
+ // degrades to.
90
+ addressFlow: addressFlowFor(
91
+ config,
92
+ state.serverConfig.addressSearch === true,
93
+ addressVendorsStubbed({ environment: state.serverConfig.environment }),
94
+ Boolean(state.serverConfig.mapsFrameUrl) && webViewAvailable(),
95
+ ),
78
96
  hasQuestionnaire: hasActiveQuestionnaire(config.questionnaire),
97
+ // Present only on a session a reviewer sent back. Narrows the order to the
98
+ // steps they ticked, so somebody fixing one blurry photo is not walked
99
+ // through the whole flow again.
100
+ resubmit: config.resubmit,
79
101
  };
80
102
  }
81
103
 
@@ -148,6 +170,15 @@ export function businessSubmission(state: KycState): {
148
170
  ...(text(b.email) ? { email: text(b.email) } : {}),
149
171
  ...(text(b.phone) ? { phone: text(b.phone) } : {}),
150
172
  ...(text(b.website) ? { website: text(b.website) } : {}),
173
+ // The five registry facts, sent only when filled. The server drops any
174
+ // the workflow has switched off, so an over-send is validate-and-drop
175
+ // rather than an error, and an under-send is what the required-field
176
+ // 422 is for.
177
+ ...(text(b.dateOfIncorporation) ? { dateOfIncorporation: text(b.dateOfIncorporation) } : {}),
178
+ ...(text(b.taxId) ? { taxId: text(b.taxId) } : {}),
179
+ ...(text(b.vatNumber) ? { vatNumber: text(b.vatNumber) } : {}),
180
+ ...(text(b.companyType) ? { companyType: text(b.companyType) } : {}),
181
+ ...(text(b.natureOfBusiness) ? { natureOfBusiness: text(b.natureOfBusiness) } : {}),
151
182
  // Extras ride along ONLY when the workflow configures them — the server
152
183
  // ignores an unconfigured block, but sending one is a claim we did not
153
184
  // collect properly.
@@ -155,6 +186,8 @@ export function businessSubmission(state: KycState): {
155
186
  ? { documents: app.documents.map((d) => ({ type: d.type, mediaId: d.mediaId })) }
156
187
  : {}),
157
188
  ...(hasKeyPeopleCollection(config) && people.length > 0 ? { keyPeople: people } : {}),
189
+ // The FATF fallback, attested: no natural person qualifies as a UBO.
190
+ ...(app.uboUnidentifiable ? { uboUnidentifiable: true } : {}),
158
191
  ...(hasApplicantVerification(config) && app.applicantRole
159
192
  ? {
160
193
  applicant: {
@@ -222,11 +255,18 @@ export function previousStepBefore(step: KYCStep, state: KycState): KYCStep {
222
255
  return previousStepInOrder(step, stepOrderOptions(state));
223
256
  }
224
257
 
258
+ /**
259
+ * The step this flow OPENS on: 'consent', or the first real step when the
260
+ * workflow switched the consent screen off (`consentStep: false`). The store
261
+ * starts and resets here, the progress watcher judges "untouched" against it,
262
+ * and the flow hides Back on it. Never depends on anything the applicant has
263
+ * yet to choose: only later steps do.
264
+ */
265
+ export function openingStep(state: KycState): KYCStep {
266
+ return openingStepOf(buildStepOrder(stepOrderOptions(state)));
267
+ }
268
+
225
269
  /** Percentage complete, for the sheet's progress indicator. */
226
270
  export function stepProgress(step: KYCStep, state: KycState): number {
227
271
  return getStepProgress(step, stepOrderOptions(state));
228
272
  }
229
-
230
- // ---------------------------------------------------------------------------
231
- // Store factory
232
- // ---------------------------------------------------------------------------