@myazahq/kyc-sdk-react-native 2.5.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 (167) hide show
  1. package/README.md +151 -5
  2. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +6 -4
  3. package/app.plugin.js +89 -8
  4. package/ios/HybridMyazaFaceDetector.swift +8 -3
  5. package/package.json +14 -2
  6. package/src/MyazaBiometricAuth.tsx +119 -0
  7. package/src/components/CountryField.tsx +8 -0
  8. package/src/components/CountryRegionPicker.tsx +184 -0
  9. package/src/components/DialCodePicker.tsx +47 -68
  10. package/src/components/DialCodeRow.tsx +102 -0
  11. package/src/components/FramedMapPicker.tsx +122 -0
  12. package/src/components/GeoBadge.tsx +34 -0
  13. package/src/components/Icon.tsx +2 -185
  14. package/src/components/KycFlow.tsx +60 -32
  15. package/src/components/LineSkeleton.tsx +100 -0
  16. package/src/components/MapChrome.tsx +67 -0
  17. package/src/components/MapPinMarker.tsx +54 -0
  18. package/src/components/MapPinPicker.tsx +179 -0
  19. package/src/components/MilestoneTrack.tsx +155 -0
  20. package/src/components/MyazaInput.tsx +17 -1
  21. package/src/components/OptionRow.tsx +22 -3
  22. package/src/components/PhoneNumberInput.tsx +8 -0
  23. package/src/components/PoweredBy.tsx +5 -5
  24. package/src/components/PresenceBlocks.tsx +153 -0
  25. package/src/components/StepView.tsx +17 -0
  26. package/src/components/StickyActions.tsx +56 -0
  27. package/src/components/dialCodeRows.ts +64 -0
  28. package/src/components/icon-map.ts +176 -0
  29. package/src/components/stepHeaderMeta.tsx +42 -4
  30. package/src/config/addressCollection.ts +125 -0
  31. package/src/config/biometricOptions.ts +100 -0
  32. package/src/config/business.ts +20 -1
  33. package/src/config/businessSteps.ts +8 -2
  34. package/src/config/consentStep.ts +19 -0
  35. package/src/config/proofOfAddress.ts +54 -11
  36. package/src/config/regions.ts +24 -0
  37. package/src/config/stepOrder.ts +79 -5
  38. package/src/config/uploadLimits.ts +35 -0
  39. package/src/config/workflowMerge.ts +11 -5
  40. package/src/index.ts +54 -1
  41. package/src/lib/address-current-location.ts +171 -0
  42. package/src/lib/address-field-modes.ts +119 -0
  43. package/src/lib/address-flow.ts +172 -0
  44. package/src/lib/address-helpers.ts +48 -0
  45. package/src/lib/address-line.ts +76 -0
  46. package/src/lib/address-pin-move.ts +97 -0
  47. package/src/lib/address-step-recovery.ts +63 -0
  48. package/src/lib/authed-image.ts +78 -0
  49. package/src/lib/biometric-auth.ts +52 -0
  50. package/src/lib/biometric-copy.ts +51 -0
  51. package/src/lib/captureRing.ts +83 -0
  52. package/src/lib/country-adoption.ts +89 -0
  53. package/src/lib/inferred-country.ts +65 -0
  54. package/src/lib/livenessLayout.ts +49 -0
  55. package/src/lib/map-frame.ts +159 -0
  56. package/src/lib/map-tiles.ts +143 -0
  57. package/src/lib/poa-country-gate.ts +27 -0
  58. package/src/lib/result-copy.ts +116 -0
  59. package/src/lib/result-wait.ts +53 -0
  60. package/src/lib/review-map-surface.ts +26 -0
  61. package/src/lib/scope.ts +31 -0
  62. package/src/lib/selfie-upload-wait.ts +79 -0
  63. package/src/lib/street-view-fov.ts +42 -0
  64. package/src/lib/webview-available.ts +47 -0
  65. package/src/liveness/useLiveness.ts +6 -1
  66. package/src/presence/background-math.ts +100 -0
  67. package/src/presence/background-store.ts +82 -0
  68. package/src/presence/background.ts +165 -0
  69. package/src/presence/foreground-service.ts +193 -0
  70. package/src/presence/fs.ts +51 -0
  71. package/src/presence/geofence.ts +41 -0
  72. package/src/presence/math.ts +44 -0
  73. package/src/presence/post.ts +40 -0
  74. package/src/presence/report.ts +87 -0
  75. package/src/presence/sampler.ts +110 -0
  76. package/src/presence/status.ts +92 -0
  77. package/src/presence/store.ts +94 -0
  78. package/src/presence/tier.ts +36 -0
  79. package/src/presence/watch-wait.ts +81 -0
  80. package/src/screens/AddressCountryControl.tsx +115 -0
  81. package/src/screens/BusinessDetailsFields.tsx +3 -0
  82. package/src/screens/BusinessDetailsStep.tsx +2 -0
  83. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  84. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  85. package/src/screens/CompanyInfoFields.tsx +6 -1
  86. package/src/screens/ConsentStep.tsx +6 -17
  87. package/src/screens/ContactActions.tsx +54 -0
  88. package/src/screens/ContactDestinationField.tsx +4 -0
  89. package/src/screens/ContactEntryPanel.tsx +75 -0
  90. package/src/screens/ContactVerificationStep.tsx +26 -56
  91. package/src/screens/CountrySelectStep.tsx +13 -147
  92. package/src/screens/LivenessAvatar.tsx +15 -3
  93. package/src/screens/LivenessStep.tsx +154 -52
  94. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  95. package/src/screens/ProofOfAddressParts.tsx +151 -0
  96. package/src/screens/ProofOfAddressStep.tsx +58 -99
  97. package/src/screens/SubmittedBadge.tsx +25 -0
  98. package/src/screens/SubmittedError.tsx +64 -0
  99. package/src/screens/SubmittedResult.tsx +117 -0
  100. package/src/screens/SubmittedStep.tsx +71 -164
  101. package/src/screens/SubmittedSuccess.tsx +127 -0
  102. package/src/screens/SubmittedWaiting.tsx +45 -0
  103. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  104. package/src/screens/address/AddressIntroGate.tsx +148 -0
  105. package/src/screens/address/AddressMapStub.tsx +50 -0
  106. package/src/screens/address/AddressPinStep.tsx +178 -0
  107. package/src/screens/address/AddressReviewStep.tsx +162 -0
  108. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  109. package/src/screens/address/AddressSearchField.tsx +115 -0
  110. package/src/screens/address/AddressSearchStep.tsx +94 -0
  111. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  112. package/src/screens/address/DetailsSheet.tsx +175 -0
  113. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  114. package/src/screens/address/EntranceDropzone.tsx +178 -0
  115. package/src/screens/address/EntranceFraming.tsx +66 -0
  116. package/src/screens/address/EntrancePills.tsx +60 -0
  117. package/src/screens/address/FramedStreetView.tsx +155 -0
  118. package/src/screens/address/IntroDisclosures.tsx +210 -0
  119. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  120. package/src/screens/address/PinSummaryRow.tsx +105 -0
  121. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  122. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  123. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  124. package/src/screens/address/SearchResults.tsx +105 -0
  125. package/src/screens/address/SearchScreen.tsx +176 -0
  126. package/src/screens/address/SkipForNow.tsx +43 -0
  127. package/src/screens/address/StreetViewChrome.tsx +81 -0
  128. package/src/screens/address/detail-values.ts +22 -0
  129. package/src/screens/address/fix-source.ts +27 -0
  130. package/src/screens/address/index.ts +8 -0
  131. package/src/screens/address/meta.ts +44 -0
  132. package/src/screens/address/use-address-flow.ts +200 -0
  133. package/src/screens/address/use-label-pin.ts +80 -0
  134. package/src/screens/address/use-pin-actions.ts +192 -0
  135. package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
  136. package/src/screens/consent/model.ts +79 -10
  137. package/src/screens/liveness/CaptureRing.tsx +91 -0
  138. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  139. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  140. package/src/screens/liveness/LivenessOutcome.tsx +4 -3
  141. package/src/screens/liveness/SelfiePreview.tsx +13 -4
  142. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  143. package/src/screens/liveness/index.ts +4 -0
  144. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  145. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  146. package/src/screens/useAddressPhotoAttach.ts +77 -0
  147. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  148. package/src/screens/usePoaAttach.ts +12 -15
  149. package/src/services/api-biometric.ts +37 -0
  150. package/src/services/api-types-biometric.ts +41 -0
  151. package/src/services/api-types.ts +93 -1
  152. package/src/services/api-verify-types.ts +37 -0
  153. package/src/services/api.ts +133 -3
  154. package/src/services/deviceMetadata.ts +1 -1
  155. package/src/services/location.ts +234 -0
  156. package/src/services/uploadErrors.ts +30 -0
  157. package/src/services/workflowGate.ts +7 -0
  158. package/src/store/address.ts +97 -0
  159. package/src/store/derive.ts +29 -4
  160. package/src/store/kycStore.ts +112 -3
  161. package/src/store/serverConfig.ts +18 -0
  162. package/src/store/session.ts +23 -4
  163. package/src/store/state.ts +134 -0
  164. package/src/store/submit.ts +18 -1
  165. package/src/types/config.ts +48 -1
  166. package/src/types/verification.ts +12 -0
  167. package/src/types/workflow.ts +97 -1
@@ -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
@@ -142,6 +144,80 @@ export interface SdkConfigResponse {
142
144
  * carries the same lookup as a RISK signal, and the two must not be confused.
143
145
  */
144
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;
145
221
  }
146
222
 
147
223
  /**
@@ -182,6 +258,20 @@ export interface WorkflowResolutionResponse {
182
258
  * carries the same lookup as a RISK signal, and the two must not be confused.
183
259
  */
184
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;
185
275
  /** KYB only: the mapped applicant workflow, when configured and resolvable. */
186
276
  applicantWorkflow?: ApplicantWorkflowPayload | null;
187
277
  }
@@ -318,3 +408,5 @@ export interface BusinessSearchResponse {
318
408
  export interface BusinessRegionsResponse {
319
409
  regions: { code: string; name: string }[];
320
410
  }
411
+
412
+ export * from './api-types-biometric';
@@ -45,9 +45,46 @@ export interface VerifyRequest {
45
45
  documentBackVideo?: string;
46
46
  livenessVideo?: string;
47
47
  proofOfAddress?: string;
48
+ /** Address Intelligence door photo (individual flows only). */
49
+ addressPhoto?: string;
48
50
  };
49
51
  /** Which kind of document the user said they uploaded as proof of address. */
50
52
  proofOfAddressType?: string;
53
+ /**
54
+ * Address Intelligence: the map pin the applicant dropped (+ the optional
55
+ * one-shot device fix taken at Continue). The server corroborates it; the
56
+ * SDK only collects. Sent only when the step ran and a pin was confirmed.
57
+ */
58
+ address?: {
59
+ lat: number;
60
+ lng: number;
61
+ accuracy?: number;
62
+ /**
63
+ * The line the applicant CONFIRMED (a search pick, or a reverse geocode
64
+ * they accepted). The server prefers it for the composed address over its
65
+ * own derivation, whose OSM coverage drops whole streets in our markets.
66
+ */
67
+ label?: string;
68
+ directions?: string;
69
+ propertyName?: string;
70
+ propertyNumber?: string;
71
+ /** A street the applicant typed because no map source knew it. */
72
+ street?: string;
73
+ /** The edit-details claims: unit + area/region corrections. */
74
+ unit?: string;
75
+ neighbourhood?: string;
76
+ city?: string;
77
+ state?: string;
78
+ postcode?: string;
79
+ /** Street View entrance frame — coordinates only; the server fetches the
80
+ * image with its own key. Never written by this SDK (no panorama on
81
+ * mobile), but a session begun on a hosted page can carry one. */
82
+ streetView?: { panoId: string; heading: number; pitch: number; fov: number };
83
+ deviceLat?: number;
84
+ deviceLng?: number;
85
+ deviceAccuracy?: number;
86
+ capturedAt?: string;
87
+ };
51
88
  /**
52
89
  * Business (KYB) registry details. Its presence is what makes this a business
53
90
  * submission — and the server REQUIRES a published KYB workflow for one, so
@@ -1,5 +1,7 @@
1
1
  import { SDK_VERSION } from './deviceMetadata';
2
2
  import type {
3
+ AddressReverseResult,
4
+ AddressSearchHit,
3
5
  BusinessRegionsResponse,
4
6
  BusinessSearchResponse,
5
7
  BusinessSelectResponse,
@@ -8,6 +10,8 @@ import type {
8
10
  ContactSendResponse,
9
11
  HealthResponse,
10
12
  MediaUploadType,
13
+ PlaceSuggestion,
14
+ ResolvedPlace,
11
15
  SdkConfigResponse,
12
16
  SessionStartResponse,
13
17
  SessionSummaryResponse,
@@ -18,6 +22,7 @@ import type {
18
22
  VerifyResponse,
19
23
  WorkflowResolutionResponse,
20
24
  } from './api-types';
25
+ import { biometricCalls } from './api-biometric';
21
26
 
22
27
  // The HTTP contract lives in ./api-types and is re-exported here, so importers
23
28
  // keep a single entry point for both the client and the shapes it exchanges.
@@ -70,9 +75,14 @@ function uriToBlob(uri: string, mimeType: string): Promise<Blob> {
70
75
  xhr.onload = () => {
71
76
  const blob = xhr.response as Blob | null;
72
77
  if (blob) {
73
- // Ensure the part carries the right content-type even if the platform
74
- // didn't infer one from the URI.
75
- resolve(blob.type ? blob : blob.slice(0, blob.size, mimeType));
78
+ // ALWAYS re-type the part, never only when the platform inferred
79
+ // nothing. Its guess comes from the URI's extension and describes the
80
+ // file we PICKED, not the one we are sending: an iPhone gallery photo
81
+ // arrives as HEIC, gets transcoded to JPEG on the way here, and the
82
+ // part still went out labelled image/heic — which the server refuses,
83
+ // so most camera-roll uploads failed with a message that blamed the
84
+ // document. `mimeType` is the normalised type the bytes actually are.
85
+ resolve(blob.type === mimeType ? blob : blob.slice(0, blob.size, mimeType));
76
86
  } else {
77
87
  reject(new Error(`Could not read file at ${uri}`));
78
88
  }
@@ -157,6 +167,8 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
157
167
  }
158
168
 
159
169
  return {
170
+ ...biometricCalls(request),
171
+
160
172
  /**
161
173
  * Single multipart upload: the local file is POSTed to our server, which
162
174
  * stores it and returns the `mediaId` referenced later by /verify.
@@ -279,6 +291,10 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
279
291
  workflowId?: string;
280
292
  /** Persistent device id — the anonymous-mount resume fallback. */
281
293
  deviceRef?: string;
294
+ /** The same device block the submission sends, so the dashboard's
295
+ * in-progress row shows the device and SDK from the moment the SDK
296
+ * loads rather than after the applicant finishes (2026-09-08). */
297
+ device?: Record<string, unknown>;
282
298
  }): Promise<SessionStartResponse> {
283
299
  return request<SessionStartResponse>('/session/start', {
284
300
  method: 'POST',
@@ -354,6 +370,120 @@ export function createKYCApi(baseUrl: string, apiKey: string) {
354
370
  });
355
371
  },
356
372
 
373
+ /**
374
+ * Forward address search for the address step's search box.
375
+ *
376
+ * EXPLICIT SUBMIT ONLY — never call this per keystroke. The server's map
377
+ * source forbids autocomplete, and the request budget has to be spent on
378
+ * the query the person actually meant rather than on every prefix of it.
379
+ */
380
+ async addressSearch(
381
+ query: string,
382
+ country?: string | null,
383
+ ): Promise<{ results: AddressSearchHit[] }> {
384
+ const qs = new URLSearchParams({ q: query });
385
+ if (country) qs.set('country', country);
386
+ return request<{ results: AddressSearchHit[] }>(`/address/search?${qs.toString()}`);
387
+ },
388
+
389
+ /**
390
+ * The framed Street View entrance, as an image SOURCE the review card can
391
+ * hand straight to <Image>.
392
+ *
393
+ * The browser key never reaches this SDK (the framed page holds it), so
394
+ * the picture comes through the server, which does. Returned as a URL plus
395
+ * the auth header rather than fetched bytes: React Native's Image carries
396
+ * headers itself, which keeps Blob, FileReader and base64 out of a path
397
+ * that only has to draw a thumbnail. Mirrors the web SDK's
398
+ * addressStreetViewPreview, which returns a Blob because a browser <img>
399
+ * cannot send an Authorization header.
400
+ */
401
+ /**
402
+ * The pinned location as a PICTURE, for the review card.
403
+ *
404
+ * A confirmation screen wants a photograph of the place, not a second
405
+ * instrument: a live map there invites a drag that goes nowhere and
406
+ * carries the vendor's own controls over the SDK's chrome. Same shape as
407
+ * the Street View source above, and the same reason for it: the key lives
408
+ * on the server. lib/authed-image fetches it with the header — an <Image>
409
+ * given `source.headers` drops them on Android.
410
+ */
411
+ staticMapSource(view: {
412
+ lat: number;
413
+ lng: number;
414
+ zoom?: number;
415
+ width?: number;
416
+ height?: number;
417
+ }): { uri: string; headers: Record<string, string> } {
418
+ const qs = new URLSearchParams({
419
+ lat: String(view.lat),
420
+ lng: String(view.lng),
421
+ zoom: String(view.zoom ?? 16),
422
+ width: String(Math.round(view.width ?? 640)),
423
+ height: String(Math.round(view.height ?? 360)),
424
+ });
425
+ return { uri: `${base}/address/static-map?${qs.toString()}`, headers };
426
+ },
427
+
428
+ streetViewPreviewSource(frame: {
429
+ panoId: string;
430
+ heading: number;
431
+ pitch: number;
432
+ fov: number;
433
+ }): { uri: string; headers: Record<string, string> } {
434
+ const qs = new URLSearchParams({
435
+ panoId: frame.panoId,
436
+ heading: String(frame.heading),
437
+ pitch: String(frame.pitch),
438
+ fov: String(frame.fov),
439
+ });
440
+ return {
441
+ uri: `${base}/address/street-view-preview?${qs.toString()}`,
442
+ headers,
443
+ };
444
+ },
445
+
446
+ /** The street line for a pin, for the summary card after a locate or a
447
+ * drag. Display only — it never decides anything. */
448
+ async addressReverse(lat: number, lng: number): Promise<AddressReverseResult> {
449
+ const qs = new URLSearchParams({ lat: String(lat), lng: String(lng) });
450
+ return request<AddressReverseResult>(`/address/reverse?${qs.toString()}`);
451
+ },
452
+
453
+ /**
454
+ * Places-backed as-you-type suggestions.
455
+ *
456
+ * `session` is ONE token per typing session: it is the billing unit, so
457
+ * Google bills per session rather than per keystroke. Mint it when the
458
+ * search screen opens, reuse it for every keystroke, and mint a fresh one
459
+ * after a details call — that call closes the session.
460
+ */
461
+ async addressAutocomplete(
462
+ query: string,
463
+ session: string,
464
+ country?: string | null,
465
+ near?: { lat: number; lng: number } | null,
466
+ ): Promise<{ suggestions: PlaceSuggestion[] }> {
467
+ const qs = new URLSearchParams({ q: query, session });
468
+ if (country) qs.set('country', country);
469
+ // The device fix, a RANKING bias so nearby streets come first: without
470
+ // it "Awolowo Road" in Calabar ranks against every Awolowo Road in the
471
+ // country. Mirrors the web SDK's api.addressAutocomplete.
472
+ if (near) {
473
+ qs.set('lat', String(near.lat));
474
+ qs.set('lng', String(near.lng));
475
+ }
476
+ return request<{ suggestions: PlaceSuggestion[] }>(`/address/autocomplete?${qs.toString()}`);
477
+ },
478
+
479
+ /** Resolve a picked suggestion to coordinates + structured pieces. */
480
+ async addressPlace(placeId: string, session: string): Promise<{ place: ResolvedPlace }> {
481
+ const qs = new URLSearchParams({ session });
482
+ return request<{ place: ResolvedPlace }>(
483
+ `/address/place/${encodeURIComponent(placeId)}?${qs.toString()}`,
484
+ );
485
+ },
486
+
357
487
  async health(): Promise<HealthResponse> {
358
488
  // Public endpoint — no auth needed, but the shared headers are harmless.
359
489
  return request<HealthResponse>('/health');
@@ -15,7 +15,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
15
15
  * Single source of truth for the SDK version — also used by `services/api.ts`
16
16
  * for the `X-SDK-Version` header. Keep in sync with `package.json`.
17
17
  */
18
- export const SDK_VERSION = '2.5.0';
18
+ export const SDK_VERSION = '2.6.0';
19
19
 
20
20
  export interface ReactNativeDeviceMetadata {
21
21
  sdkType: 'react-native';