@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,90 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { AppState } from 'react-native';
3
+ import type { KYCApi } from '../services/api';
4
+ import type { AwaitingPersonPayload } from '../services/api-types';
5
+
6
+ // The SERVER's list of who a submitted KYB application is waiting on — mirrors
7
+ // the web SDK's use-awaiting-people 1:1.
8
+ //
9
+ // MEMBERSHIP settles once: registry discovery runs after submission and can
10
+ // add people the applicant never listed, so nothing renders until the server
11
+ // says it has finished (`keyPeopleSettled`). A list shown earlier is one
12
+ // director short and would be contradicted moments later.
13
+ //
14
+ // STATUS stays live: the people named go and verify after this screen is first
15
+ // shown, so once settled the list re-reads slowly while anybody still owes a
16
+ // check — and re-reads on foreground, which is exactly when a stale badge
17
+ // would be noticed.
18
+
19
+ /** How often to re-ask while the server says it is still reconciling. */
20
+ const RETRY_MS = 1500;
21
+ /** How long to wait before showing whatever there is (discovery's final write
22
+ * is best-effort; a spinner forever is worse than a list that might be short). */
23
+ const GIVE_UP_MS = 15_000;
24
+ /** Status-refresh cadence once settled, while anybody owes a check. */
25
+ const POLL_MS = 20_000;
26
+
27
+ function anyoneStillOwes(people: AwaitingPersonPayload[]): boolean {
28
+ return people.some(
29
+ (p) => p.status === 'pending' || p.status === 'submitted' || p.status === 'failed',
30
+ );
31
+ }
32
+
33
+ export function useAwaitingPeople(
34
+ api: KYCApi,
35
+ sessionId: string | null,
36
+ enabled: boolean,
37
+ ): AwaitingPersonPayload[] | null {
38
+ const [people, setPeople] = useState<AwaitingPersonPayload[] | null>(null);
39
+
40
+ useEffect(() => {
41
+ if (!sessionId || !enabled) return undefined;
42
+ let cancelled = false;
43
+ let timer: ReturnType<typeof setTimeout> | null = null;
44
+ let committed = false;
45
+ const startedAt = Date.now();
46
+
47
+ const read = async (): Promise<void> => {
48
+ if (cancelled) return;
49
+ const summary = await api.sessionSummary(sessionId).catch(() => null);
50
+ if (cancelled) return;
51
+ const expired = Date.now() - startedAt > GIVE_UP_MS;
52
+
53
+ if (summary && (summary.keyPeopleSettled !== false || expired)) {
54
+ committed = true;
55
+ setPeople(summary.keyPeople);
56
+ if (anyoneStillOwes(summary.keyPeople)) {
57
+ timer = setTimeout(() => void read(), POLL_MS);
58
+ }
59
+ return;
60
+ }
61
+ if (committed) {
62
+ // One failed refresh must not end the updates: the list on screen is
63
+ // still true, only a little older.
64
+ timer = setTimeout(() => void read(), POLL_MS);
65
+ return;
66
+ }
67
+ if (expired) {
68
+ // Nothing readable at all — leave the list absent rather than empty,
69
+ // which would claim there is nobody to verify.
70
+ return;
71
+ }
72
+ timer = setTimeout(() => void read(), RETRY_MS);
73
+ };
74
+
75
+ const sub = AppState.addEventListener('change', (state) => {
76
+ if (state !== 'active') return;
77
+ if (timer) clearTimeout(timer);
78
+ void read();
79
+ });
80
+
81
+ void read();
82
+ return () => {
83
+ cancelled = true;
84
+ if (timer) clearTimeout(timer);
85
+ sub.remove();
86
+ };
87
+ }, [api, sessionId, enabled]);
88
+
89
+ return people;
90
+ }
@@ -0,0 +1,93 @@
1
+ import { useCallback } from 'react';
2
+ import * as ImagePicker from 'expo-image-picker';
3
+
4
+ import type { ResolvedBusinessDocumentType } from '../config/businessSteps';
5
+ import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
6
+ import { uploadSizeError } from '../config/uploadLimits';
7
+ import { loadDocumentPicker } from '../services/documentPicker';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Getting a company document off the device — the business-documents step's
11
+ // twin of usePoaAttach (extracted from BusinessDocumentsStep, 200-line rule).
12
+ // Three sources, one shape handed to the step's uploader. Every pick is judged
13
+ // against the shared upload caps BEFORE anything is compressed or sent, so
14
+ // the refusal names the file that was chosen (images 5 MB, PDFs 15 MB).
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type AttachBusinessDocument = (
18
+ slot: ResolvedBusinessDocumentType,
19
+ uri: string,
20
+ mimeType: string | undefined,
21
+ name: string,
22
+ ) => Promise<void>;
23
+
24
+ type Pick = (slot: ResolvedBusinessDocumentType) => Promise<void>;
25
+
26
+ export function useBusinessDocumentAttach(
27
+ attach: AttachBusinessDocument,
28
+ setError: (message: string) => void,
29
+ ): { takePhoto: Pick; choosePhoto: Pick; chooseFile: Pick } {
30
+ const tooLarge = useCallback(
31
+ (mime: string | undefined, size: number | undefined): boolean => {
32
+ const message = uploadSizeError(mime, size);
33
+ if (message) setError(message);
34
+ return message !== null;
35
+ },
36
+ [setError],
37
+ );
38
+
39
+ const takePhoto = useCallback<Pick>(
40
+ async (slot) => {
41
+ const permission = await ImagePicker.requestCameraPermissionsAsync();
42
+ if (!permission.granted) {
43
+ setError('Camera access is needed to photograph the document.');
44
+ return;
45
+ }
46
+ const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
47
+ const asset = result.canceled ? undefined : result.assets[0];
48
+ if (!asset || tooLarge(asset.mimeType, asset.fileSize)) return;
49
+ // A FRIENDLY name, not the picker's temp junk — camera/library assets
50
+ // carry generated names; the slot key says what the file IS.
51
+ await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
52
+ },
53
+ [attach, setError, tooLarge],
54
+ );
55
+
56
+ const choosePhoto = useCallback<Pick>(
57
+ async (slot) => {
58
+ const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
59
+ const asset = result.canceled ? undefined : result.assets[0];
60
+ if (!asset || tooLarge(asset.mimeType, asset.fileSize)) return;
61
+ await attach(slot, asset.uri, asset.mimeType ?? 'image/jpeg', `${slot.key}.jpg`);
62
+ },
63
+ [attach, tooLarge],
64
+ );
65
+
66
+ const chooseFile = useCallback<Pick>(
67
+ async (slot) => {
68
+ const picker = loadDocumentPicker();
69
+ if (!picker) {
70
+ setError('Choosing a file is not available in this app. Please photograph the document.');
71
+ return;
72
+ }
73
+ const result = await picker.getDocumentAsync({
74
+ type: [...POA_ACCEPTED_MIME_TYPES],
75
+ copyToCacheDirectory: true,
76
+ multiple: false,
77
+ });
78
+ const asset = result.canceled ? undefined : result.assets?.[0];
79
+ if (!asset) return;
80
+ // The picker's `type` filter is advisory on some platforms, so what came
81
+ // back is re-checked rather than trusted.
82
+ if (!isAcceptedPoaMimeType(asset.mimeType)) {
83
+ setError('Please choose a PDF, JPG or PNG file.');
84
+ return;
85
+ }
86
+ if (tooLarge(asset.mimeType, asset.size)) return;
87
+ await attach(slot, asset.uri, asset.mimeType, asset.name ?? slot.key);
88
+ },
89
+ [attach, setError, tooLarge],
90
+ );
91
+
92
+ return { takePhoto, choosePhoto, chooseFile };
93
+ }
@@ -1,11 +1,8 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import * as ImagePicker from 'expo-image-picker';
3
3
 
4
- import {
5
- isAcceptedPoaMimeType,
6
- POA_ACCEPTED_MIME_TYPES,
7
- POA_MAX_BYTES,
8
- } from '../config/proofOfAddress';
4
+ import { isAcceptedPoaMimeType, POA_ACCEPTED_MIME_TYPES } from '../config/proofOfAddress';
5
+ import { uploadSizeError } from '../config/uploadLimits';
9
6
  import { loadDocumentPicker } from '../services/documentPicker';
10
7
 
11
8
  // ---------------------------------------------------------------------------
@@ -38,13 +35,13 @@ export function usePoaAttach(
38
35
  takePhoto: () => Promise<void>;
39
36
  chooseFile: () => Promise<void>;
40
37
  } {
38
+ // Judged against the shared caps BEFORE anything is compressed or sent, so
39
+ // the refusal names the file that was chosen (images 5 MB, PDFs 15 MB).
41
40
  const tooLarge = useCallback(
42
- (size: number | undefined): boolean => {
43
- if (typeof size === 'number' && size > POA_MAX_BYTES) {
44
- setError('File is too large (max 20MB).');
45
- return true;
46
- }
47
- return false;
41
+ (mime: string | undefined, size: number | undefined): boolean => {
42
+ const message = uploadSizeError(mime, size);
43
+ if (message) setError(message);
44
+ return message !== null;
48
45
  },
49
46
  [setError],
50
47
  );
@@ -58,7 +55,7 @@ export function usePoaAttach(
58
55
  const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
59
56
  const asset = result.canceled ? undefined : result.assets[0];
60
57
  if (!asset) return;
61
- if (tooLarge(asset.fileSize)) return;
58
+ if (tooLarge(asset.mimeType, asset.fileSize)) return;
62
59
  await upload({
63
60
  uri: asset.uri,
64
61
  mimeType: asset.mimeType ?? 'image/jpeg',
@@ -73,7 +70,7 @@ export function usePoaAttach(
73
70
  });
74
71
  const asset = result.canceled ? undefined : result.assets[0];
75
72
  if (!asset) return;
76
- if (tooLarge(asset.fileSize)) return;
73
+ if (tooLarge(asset.mimeType, asset.fileSize)) return;
77
74
  await upload({
78
75
  uri: asset.uri,
79
76
  mimeType: asset.mimeType ?? 'image/jpeg',
@@ -97,10 +94,10 @@ export function usePoaAttach(
97
94
  // The picker's `type` filter is advisory on some platforms, so what came
98
95
  // back is re-checked rather than trusted.
99
96
  if (!isAcceptedPoaMimeType(asset.mimeType)) {
100
- setError('Please choose a photo (JPEG, PNG or WebP) or a PDF.');
97
+ setError('Please choose a PDF, JPG or PNG file.');
101
98
  return;
102
99
  }
103
- if (tooLarge(asset.size)) return;
100
+ if (tooLarge(asset.mimeType, asset.size)) return;
104
101
  await upload({
105
102
  uri: asset.uri,
106
103
  mimeType: asset.mimeType,
@@ -0,0 +1,37 @@
1
+ import type {
2
+ BiometricAuthRequest,
3
+ BiometricAuthResponse,
4
+ BiometricStatusResponse,
5
+ } from './api-types-biometric';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // The biometric re-authentication calls, split out of createKYCApi (200-line
9
+ // rule). Given the client's own `request`, so they ride the same base URL,
10
+ // bearer key, SDK-version header and error mapping as everything else.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type JsonRequest = <T>(path: string, init?: RequestInit) => Promise<T>;
14
+
15
+ export function biometricCalls(request: JsonRequest) {
16
+ return {
17
+ /**
18
+ * Re-authenticate a verified user by matching a live selfie 1:1 against
19
+ * their KYC enrollment reference. Publishable-safe. Uniform 404
20
+ * `not_enrolled` — a missing entity, a business entity and no template
21
+ * all answer the same, so nothing can be probed.
22
+ */
23
+ async authenticate(body: BiometricAuthRequest): Promise<BiometricAuthResponse> {
24
+ return request<BiometricAuthResponse>('/biometric/authenticate', {
25
+ method: 'POST',
26
+ body: JSON.stringify(body),
27
+ });
28
+ },
29
+
30
+ /** Whether a user is enrolled for face re-auth (whether to OFFER it). */
31
+ async getBiometricStatus(externalUserId: string): Promise<BiometricStatusResponse> {
32
+ return request<BiometricStatusResponse>(
33
+ `/biometric/status/${encodeURIComponent(externalUserId)}`,
34
+ );
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,41 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Biometric re-authentication wire shapes — a mirror of the web SDK's
3
+ // services/api.ts (keep the two in lockstep). Publishable-safe: the verdict,
4
+ // a confidence, and a single-use proof token. No PII.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /** What the SDK asserts about the liveness run that produced the selfie. */
8
+ export interface BiometricLivenessClaim {
9
+ mode: 'gestures' | 'flash' | 'both';
10
+ passed: boolean;
11
+ }
12
+
13
+ /** Request body for `POST /api/kyc/biometric/authenticate`. */
14
+ export interface BiometricAuthRequest {
15
+ /** The org's user reference (Entity.externalUserId) being re-authenticated. */
16
+ externalUserId: string;
17
+ /** A mediaId from `upload(file, 'selfie')` — the live selfie. */
18
+ selfie: string;
19
+ liveness?: BiometricLivenessClaim;
20
+ }
21
+
22
+ /**
23
+ * Response from `POST /api/kyc/biometric/authenticate`. `token` is present
24
+ * only on `authenticated` — redeem it from your backend with a secret key at
25
+ * `/biometric/verify-proof`.
26
+ */
27
+ export interface BiometricAuthResponse {
28
+ authenticated: boolean;
29
+ status: 'authenticated' | 'no_match' | 'liveness_failed';
30
+ confidence: number | null;
31
+ live: boolean;
32
+ attemptId: string;
33
+ token?: string;
34
+ }
35
+
36
+ /** Response from `GET /api/kyc/biometric/status/:externalUserId`. */
37
+ export interface BiometricStatusResponse {
38
+ enrolled: boolean;
39
+ enrolledAt?: string;
40
+ lastAuthenticatedAt?: string | null;
41
+ }
@@ -23,7 +23,9 @@ export type MediaUploadType =
23
23
  // The two kinds that accept a PDF — a proof of address is usually a
24
24
  // downloaded statement, and company paperwork is almost always a scan.
25
25
  | 'proof_of_address'
26
- | 'business_document';
26
+ | 'business_document'
27
+ // Address Intelligence door / premises photo (image only).
28
+ | 'address_photo';
27
29
 
28
30
  /**
29
31
  * A file to upload, in React Native's multipart shape. `uri` points at a
@@ -47,9 +49,32 @@ export interface KeyPersonInvite {
47
49
  inviteUrl: string;
48
50
  }
49
51
 
52
+ /**
53
+ * The one status vocabulary, shared by `GET /api/kyc/status/:id`, the
54
+ * secret-key result route and every verification webhook.
55
+ *
56
+ * `status` is what happened; `checkStatus` beside it is what the CHECKS found.
57
+ * They differ when a person overrode the automated result, which is the case
58
+ * worth being able to see: `approved` with `checkStatus: 'failed'` means
59
+ * somebody accepted the applicant despite a failed check, and the reason says
60
+ * what they accepted them despite.
61
+ */
62
+ export type SessionStatus =
63
+ | 'not_started'
64
+ | 'in_progress'
65
+ | 'processing'
66
+ | 'in_review'
67
+ | 'awaiting_resubmission'
68
+ | 'approved'
69
+ | 'declined'
70
+ | 'abandoned'
71
+ | 'expired'
72
+ | 'error';
73
+
50
74
  export interface VerifyResponse {
51
75
  verificationId: string;
52
- status: 'pending';
76
+ /** Always `processing`: accepted, checks running. */
77
+ status: 'processing';
53
78
  /**
54
79
  * KYB with applicant verification: the KeyPerson row the applicant's OWN
55
80
  * identity check must link back to, via `metadata.userId` on a second,
@@ -66,7 +91,9 @@ export interface VerifyResponse {
66
91
  */
67
92
  export interface VerificationStatusResponse {
68
93
  verificationId: string;
69
- status: 'pending' | 'verified' | 'failed' | 'not_found' | 'error';
94
+ status: SessionStatus;
95
+ /** What the CHECKS found, unchanged by any later decision. */
96
+ checkStatus?: 'pending' | 'verified' | 'failed' | 'not_found' | 'error';
70
97
  reason?: string | null;
71
98
  reasonCode?: string | null;
72
99
  createdAt: string;
@@ -109,6 +136,88 @@ export interface SdkConfigResponse {
109
136
  environment: 'DEVELOPMENT' | 'SANDBOX' | 'PRODUCTION';
110
137
  idTypes: SdkConfigIdType[];
111
138
  branding?: SdkConfigBranding;
139
+ /**
140
+ * The visitor's country, resolved from their IP.
141
+ *
142
+ * A GUESS and only ever a DEFAULT — nothing branches on it and it never
143
+ * reaches a verification. Deliberately not evidence: device intelligence
144
+ * carries the same lookup as a RISK signal, and the two must not be confused.
145
+ */
146
+ geoCountry?: string | null;
147
+ /** Whether the platform has a forward-search backend. Gates the whole
148
+ * address-search step; absent means no search screen, never an error. */
149
+ addressSearch?: boolean;
150
+ /** Which backend: 'autocomplete' (Places, as-you-type) or 'basic'
151
+ * (explicit submit). Absent when addressSearch is false. */
152
+ addressSearchMode?: 'autocomplete' | 'basic';
153
+ /**
154
+ * The framed Google-map picker page (OUR hosted /embed/map + a signed APP
155
+ * grant), for a WebView. Absent when the platform holds no Maps key — the
156
+ * built-in OSM picker is the fallback every map failure degrades to.
157
+ */
158
+ mapsFrameUrl?: string | null;
159
+ }
160
+
161
+ // ── Address search / reverse geocoding ──────────────────────────────────────
162
+ //
163
+ // Every one of these may fail, and every failure degrades to "place the pin by
164
+ // hand" — never to a blocked flow. Shapes mirror the web SDK's services/api.ts.
165
+
166
+ /** The pin's address broken down — what the details sheet displays as rows. */
167
+ export interface AddressParts {
168
+ street?: string | null;
169
+ area?: string | null;
170
+ city?: string | null;
171
+ state?: string | null;
172
+ postcode?: string | null;
173
+ /**
174
+ * ISO-2 of the pin's OWN country, from the geocoder. On the address scope
175
+ * the declared country follows it (lib/country-adoption.ts); it is never a
176
+ * row in the sheet, which shows the country the flow declared.
177
+ */
178
+ country?: string | null;
179
+ }
180
+
181
+ export interface AddressReverseResult {
182
+ line: string | null;
183
+ road: string | null;
184
+ parts?: AddressParts | null;
185
+ }
186
+
187
+ /** One candidate from the basic (explicit-submit) forward search. */
188
+ export interface AddressSearchHit {
189
+ label: string;
190
+ lat: number;
191
+ lng: number;
192
+ houseNumber: string | null;
193
+ road: string | null;
194
+ /** ISO-2 of the hit's own country (the declaration derives from it). */
195
+ country?: string | null;
196
+ }
197
+
198
+ /** One Places autocomplete suggestion. */
199
+ export interface PlaceSuggestion {
200
+ placeId: string;
201
+ mainText: string;
202
+ secondaryText: string;
203
+ }
204
+
205
+ /** A picked suggestion, resolved to coordinates + structured pieces. */
206
+ export interface ResolvedPlace {
207
+ lat: number;
208
+ lng: number;
209
+ houseNumber: string | null;
210
+ road: string | null;
211
+ formatted: string | null;
212
+ area?: string | null;
213
+ city?: string | null;
214
+ state?: string | null;
215
+ postcode?: string | null;
216
+ /**
217
+ * ISO-2 of the picked address's own country — the declaration derives from
218
+ * it on the address scope (a pick is the applicant's own statement).
219
+ */
220
+ country?: string | null;
112
221
  }
113
222
 
114
223
  /**
@@ -141,6 +250,28 @@ export interface WorkflowResolutionResponse {
141
250
  /** Org allowlist + per-ID feature flags (same shape as /config). */
142
251
  idTypes: SdkConfigIdType[];
143
252
  branding?: SdkConfigBranding;
253
+ /**
254
+ * The visitor's country, resolved from their IP.
255
+ *
256
+ * A GUESS and only ever a DEFAULT — nothing branches on it and it never
257
+ * reaches a verification. Deliberately not evidence: device intelligence
258
+ * carries the same lookup as a RISK signal, and the two must not be confused.
259
+ */
260
+ geoCountry?: string | null;
261
+ /**
262
+ * The address-search availability flags, when the resolution route carries
263
+ * them.
264
+ *
265
+ * A `workflowId` mount skips `/config` entirely, so these are the only route
266
+ * by which such a mount could learn a search backend exists. The server does
267
+ * not serve them here yet (only `/config` and the hosted bootstrap do), which
268
+ * is why they degrade to "no search screen" rather than an error.
269
+ */
270
+ addressSearch?: boolean;
271
+ addressSearchMode?: 'autocomplete' | 'basic';
272
+ /** The framed Google-map picker page, when the route carries one (see
273
+ * SdkConfigResponse.mapsFrameUrl). */
274
+ mapsFrameUrl?: string | null;
144
275
  /** KYB only: the mapped applicant workflow, when configured and resolvable. */
145
276
  applicantWorkflow?: ApplicantWorkflowPayload | null;
146
277
  }
@@ -157,8 +288,125 @@ export interface ContactCheckResponse {
157
288
  token: string;
158
289
  }
159
290
 
291
+ /**
292
+ * A server-issued Active-Authentication challenge. `challenge` is base64 of the
293
+ * 8 bytes handed to the chip; `challengeId` is what rides the submission so the
294
+ * server can spend it (single-use — a replayed one is refused).
295
+ */
296
+ export interface NfcChallengeResponse {
297
+ challengeId: string;
298
+ challenge: string;
299
+ expiresAt: string;
300
+ }
301
+
160
302
  export interface HealthResponse {
161
303
  status: string;
162
304
  }
163
305
 
164
306
  // The mime types the server accepts (must mirror the server's upload allowlist).
307
+
308
+ /** `POST /session/start` — mint (or resume) an attempt session. */
309
+ export interface SessionStartResponse {
310
+ sessionId: string;
311
+ expiresAt: string;
312
+ resumed: boolean;
313
+ /** The session's own hosted web page. After a KYB submission it is the
314
+ * rehydrated success screen with every key person's invite link — the
315
+ * applicant's way back to those links once the app closes. */
316
+ url?: string;
317
+ /** Where the user got to, when resuming. Media references are already
318
+ * pruned server-side of anything that has since expired. */
319
+ progress?: {
320
+ step?: string;
321
+ mediaIds?: Record<string, string>;
322
+ data?: Record<string, unknown>;
323
+ };
324
+ }
325
+
326
+ /** One person a submitted KYB application is still waiting on (the server's
327
+ * reconciled view — registry discovery can add people the applicant never
328
+ * listed, so this list supersedes the submit-time invites). */
329
+ export interface AwaitingPersonPayload {
330
+ id: string;
331
+ name: string;
332
+ role: string;
333
+ ownershipPct: number | null;
334
+ /** ISO-2, or null when the register gave free text no flag matches. */
335
+ country: string | null;
336
+ status: 'verified' | 'failed' | 'submitted' | 'pending' | 'not_needed';
337
+ /** Null once their check is done, or when they never needed one. */
338
+ inviteUrl: string | null;
339
+ isApplicant: boolean;
340
+ /** A company completes a KYB application, not a KYC - the list labels it so. */
341
+ isCorporate?: boolean;
342
+ }
343
+
344
+ /** `GET /session/:sessionId/summary` — a submitted session, rebuilt server-side. */
345
+ export interface SessionSummaryResponse {
346
+ status: 'completed';
347
+ /** False while registry discovery is still reconciling the people list. */
348
+ keyPeopleSettled?: boolean;
349
+ keyPeople: AwaitingPersonPayload[];
350
+ }
351
+
352
+ /** One officer as the register names them — the key-people prefill's input. */
353
+ export interface RegistryOfficer {
354
+ name: string | null;
355
+ designation: string | null;
356
+ /** Everything else the register said about them (older servers omit these). */
357
+ roles?: string[] | null;
358
+ ownershipPct?: number | null;
359
+ email?: string | null;
360
+ isCorporate?: boolean | null;
361
+ registrationNumber?: string | null;
362
+ }
363
+
364
+ /** What the register holds about a company, when it answered. Everything is
365
+ * nullable because no register answers all of it for every company. */
366
+ export interface BusinessCompanyRecord {
367
+ name: string | null;
368
+ registrationNumber: string;
369
+ registrationDate: string | null;
370
+ typeOfEntity: string | null;
371
+ companyStatus: string | null;
372
+ address: string | null;
373
+ email: string | null;
374
+ phone: string | null;
375
+ taxId: string | null;
376
+ vatNumber: string | null;
377
+ natureOfBusiness: string | null;
378
+ city: string | null;
379
+ state: string | null;
380
+ }
381
+
382
+ /** `POST /business/select` — the paid registry check at selection. */
383
+ export interface BusinessSelectResponse {
384
+ checked: boolean;
385
+ /** Why the pre-flight did not run (`checked: false`) — the submit-time lookup
386
+ * still happens, so this is informational, never an error. */
387
+ reason?: string;
388
+ found?: boolean;
389
+ charged?: boolean;
390
+ business?: (BusinessCompanyRecord & { keyPeople: RegistryOfficer[] }) | null;
391
+ }
392
+
393
+ /** One candidate from the FREE name search — picking one is what runs the paid check. */
394
+ export interface BusinessSearchHit {
395
+ name: string;
396
+ registrationNumber: string;
397
+ status?: string;
398
+ }
399
+
400
+ /** `GET /business/search` — find a business by name. */
401
+ export interface BusinessSearchResponse {
402
+ results: BusinessSearchHit[];
403
+ /** Which source answered; a degraded fallback names itself here. */
404
+ source: string;
405
+ }
406
+
407
+ /** `GET /business/regions` — the registry regions of a split register. */
408
+ export interface BusinessRegionsResponse {
409
+ regions: { code: string; name: string }[];
410
+ }
411
+
412
+ export * from './api-types-biometric';