@myazahq/kyc-sdk-react-native 2.5.0 → 3.0.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 (219) hide show
  1. package/README.md +277 -7
  2. package/android/build.gradle +30 -16
  3. package/android/consumer-rules.pro +22 -0
  4. package/android/src/main/AndroidManifest.xml +17 -6
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -70
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
  8. package/app.plugin.js +146 -10
  9. package/ios/HybridMyazaFaceDetector.swift +8 -3
  10. package/ios/HybridMyazaTextRecognizer.swift +8 -0
  11. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
  12. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
  14. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
  15. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
  16. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
  17. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
  18. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
  19. package/package.json +30 -9
  20. package/src/MyazaKYC.tsx +4 -11
  21. package/src/assets/fonts/Karla_400Regular.ttf +0 -0
  22. package/src/assets/fonts/Karla_500Medium.ttf +0 -0
  23. package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
  24. package/src/assets/fonts/Karla_700Bold.ttf +0 -0
  25. package/src/assets/fonts/OFL-Karla.txt +93 -0
  26. package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
  27. package/src/assets/fonts/README.md +12 -0
  28. package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
  29. package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
  30. package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
  31. package/src/capture/useAutoCapture.ts +8 -1
  32. package/src/components/CountryField.tsx +8 -0
  33. package/src/components/CountryRegionPicker.tsx +184 -0
  34. package/src/components/DialCodePicker.tsx +47 -68
  35. package/src/components/DialCodeRow.tsx +102 -0
  36. package/src/components/DocumentCropper.tsx +30 -24
  37. package/src/components/DocumentReview.tsx +11 -1
  38. package/src/components/DocumentReviewSide.tsx +6 -2
  39. package/src/components/DocumentReviewZoom.tsx +5 -1
  40. package/src/components/FramedMapPicker.tsx +122 -0
  41. package/src/components/GeoBadge.tsx +34 -0
  42. package/src/components/Icon.tsx +10 -192
  43. package/src/components/KycFlow.tsx +69 -34
  44. package/src/components/LineSkeleton.tsx +100 -0
  45. package/src/components/MapChrome.tsx +67 -0
  46. package/src/components/MapPinMarker.tsx +54 -0
  47. package/src/components/MapPinPicker.tsx +179 -0
  48. package/src/components/MilestoneTrack.tsx +155 -0
  49. package/src/components/MyazaButton.tsx +9 -0
  50. package/src/components/MyazaInput.tsx +17 -1
  51. package/src/components/OptionRow.tsx +22 -3
  52. package/src/components/PhoneNumberInput.tsx +8 -0
  53. package/src/components/PoweredBy.tsx +5 -5
  54. package/src/components/PresenceBlocks.tsx +153 -0
  55. package/src/components/RequiredDocumentPill.tsx +114 -0
  56. package/src/components/StepHeader.tsx +23 -3
  57. package/src/components/StepView.tsx +17 -0
  58. package/src/components/StickyActions.tsx +56 -0
  59. package/src/components/dialCodeRows.ts +64 -0
  60. package/src/components/documentReviewCopy.ts +39 -0
  61. package/src/components/fonts.ts +14 -17
  62. package/src/components/icon-map.ts +176 -0
  63. package/src/components/stepHeaderMeta.tsx +48 -5
  64. package/src/config/addressCollection.ts +125 -0
  65. package/src/config/biometricOptions.ts +100 -0
  66. package/src/config/business.ts +20 -1
  67. package/src/config/businessSteps.ts +8 -2
  68. package/src/config/consentStep.ts +19 -0
  69. package/src/config/documentCaptureMethods.ts +31 -0
  70. package/src/config/proofOfAddress.ts +54 -11
  71. package/src/config/regions.ts +24 -0
  72. package/src/config/stepOrder.ts +79 -5
  73. package/src/config/uploadLimits.ts +35 -0
  74. package/src/config/workflowMerge.ts +13 -5
  75. package/src/index.ts +49 -1
  76. package/src/lib/address-current-location.ts +171 -0
  77. package/src/lib/address-field-modes.ts +119 -0
  78. package/src/lib/address-flow.ts +172 -0
  79. package/src/lib/address-helpers.ts +48 -0
  80. package/src/lib/address-line.ts +76 -0
  81. package/src/lib/address-pin-move.ts +97 -0
  82. package/src/lib/address-step-recovery.ts +63 -0
  83. package/src/lib/authed-image.ts +78 -0
  84. package/src/lib/biometric-copy.ts +51 -0
  85. package/src/lib/captureRing.ts +83 -0
  86. package/src/lib/country-adoption.ts +89 -0
  87. package/src/lib/documentCaptureCheck.ts +136 -0
  88. package/src/lib/inferred-country.ts +65 -0
  89. package/src/lib/livenessLayout.ts +49 -0
  90. package/src/lib/map-frame.ts +159 -0
  91. package/src/lib/map-tiles.ts +143 -0
  92. package/src/lib/model-ready.ts +82 -0
  93. package/src/lib/poa-country-gate.ts +27 -0
  94. package/src/lib/prime-models.ts +50 -0
  95. package/src/lib/resubmit.ts +36 -7
  96. package/src/lib/result-copy.ts +116 -0
  97. package/src/lib/result-wait.ts +53 -0
  98. package/src/lib/review-map-surface.ts +26 -0
  99. package/src/lib/scope.ts +31 -0
  100. package/src/lib/selfie-sharpness.ts +211 -0
  101. package/src/lib/selfie-upload-wait.ts +79 -0
  102. package/src/lib/street-view-fov.ts +42 -0
  103. package/src/lib/webview-available.ts +47 -0
  104. package/src/liveness/avatarSource.ts +59 -0
  105. package/src/liveness/useLiveness.ts +6 -1
  106. package/src/liveness/useModelReady.ts +9 -55
  107. package/src/mrz/extract.ts +71 -4
  108. package/src/mrz/textRecognizer.ts +36 -0
  109. package/src/mrz/useTextModelReady.ts +21 -0
  110. package/src/presence/background-math.ts +100 -0
  111. package/src/presence/background-store.ts +82 -0
  112. package/src/presence/background.ts +165 -0
  113. package/src/presence/foreground-service.ts +193 -0
  114. package/src/presence/fs.ts +51 -0
  115. package/src/presence/geofence.ts +41 -0
  116. package/src/presence/math.ts +44 -0
  117. package/src/presence/post.ts +40 -0
  118. package/src/presence/report.ts +87 -0
  119. package/src/presence/sampler.ts +110 -0
  120. package/src/presence/status.ts +92 -0
  121. package/src/presence/store.ts +94 -0
  122. package/src/presence/tier.ts +36 -0
  123. package/src/presence/watch-wait.ts +81 -0
  124. package/src/screens/AddressCountryControl.tsx +115 -0
  125. package/src/screens/BusinessDetailsFields.tsx +3 -0
  126. package/src/screens/BusinessDetailsStep.tsx +2 -0
  127. package/src/screens/BusinessDocumentSlot.tsx +3 -2
  128. package/src/screens/BusinessDocumentsStep.tsx +4 -55
  129. package/src/screens/CompanyInfoFields.tsx +6 -1
  130. package/src/screens/ConsentStep.tsx +6 -17
  131. package/src/screens/ContactActions.tsx +54 -0
  132. package/src/screens/ContactDestinationField.tsx +4 -0
  133. package/src/screens/ContactEntryPanel.tsx +75 -0
  134. package/src/screens/ContactVerificationStep.tsx +26 -56
  135. package/src/screens/CountrySelectStep.tsx +13 -147
  136. package/src/screens/DocumentCaptureStep.tsx +177 -93
  137. package/src/screens/LivenessAvatar.tsx +33 -16
  138. package/src/screens/LivenessStep.tsx +188 -49
  139. package/src/screens/MrzScanView.tsx +33 -0
  140. package/src/screens/PoaDocumentTypeList.tsx +66 -0
  141. package/src/screens/ProofOfAddressParts.tsx +151 -0
  142. package/src/screens/ProofOfAddressStep.tsx +58 -99
  143. package/src/screens/SubmittedBadge.tsx +25 -0
  144. package/src/screens/SubmittedError.tsx +64 -0
  145. package/src/screens/SubmittedResult.tsx +117 -0
  146. package/src/screens/SubmittedStep.tsx +71 -164
  147. package/src/screens/SubmittedSuccess.tsx +127 -0
  148. package/src/screens/SubmittedWaiting.tsx +45 -0
  149. package/src/screens/address/AddressEntranceStep.tsx +161 -0
  150. package/src/screens/address/AddressIntroGate.tsx +148 -0
  151. package/src/screens/address/AddressMapStub.tsx +50 -0
  152. package/src/screens/address/AddressPinStep.tsx +178 -0
  153. package/src/screens/address/AddressReviewStep.tsx +162 -0
  154. package/src/screens/address/AddressSandboxTabs.tsx +158 -0
  155. package/src/screens/address/AddressSearchField.tsx +115 -0
  156. package/src/screens/address/AddressSearchStep.tsx +94 -0
  157. package/src/screens/address/CurrentLocationRow.tsx +135 -0
  158. package/src/screens/address/DetailsSheet.tsx +175 -0
  159. package/src/screens/address/DetailsSheetFields.tsx +184 -0
  160. package/src/screens/address/EntranceDropzone.tsx +178 -0
  161. package/src/screens/address/EntranceFraming.tsx +66 -0
  162. package/src/screens/address/EntrancePills.tsx +60 -0
  163. package/src/screens/address/FramedStreetView.tsx +155 -0
  164. package/src/screens/address/IntroDisclosures.tsx +210 -0
  165. package/src/screens/address/LabelDecisionRow.tsx +113 -0
  166. package/src/screens/address/PinSummaryRow.tsx +105 -0
  167. package/src/screens/address/ReviewAddressBand.tsx +111 -0
  168. package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
  169. package/src/screens/address/ReviewMapPicture.tsx +106 -0
  170. package/src/screens/address/SearchResults.tsx +105 -0
  171. package/src/screens/address/SearchScreen.tsx +176 -0
  172. package/src/screens/address/SkipForNow.tsx +43 -0
  173. package/src/screens/address/StreetViewChrome.tsx +81 -0
  174. package/src/screens/address/detail-values.ts +22 -0
  175. package/src/screens/address/fix-source.ts +27 -0
  176. package/src/screens/address/index.ts +8 -0
  177. package/src/screens/address/meta.ts +44 -0
  178. package/src/screens/address/use-address-flow.ts +200 -0
  179. package/src/screens/address/use-label-pin.ts +80 -0
  180. package/src/screens/address/use-pin-actions.ts +192 -0
  181. package/src/screens/consent/model.ts +79 -10
  182. package/src/screens/document/CaptureCheckNotice.tsx +77 -0
  183. package/src/screens/document/UploadPhase.tsx +177 -0
  184. package/src/screens/document/useDocumentCamera.ts +109 -0
  185. package/src/screens/liveness/CaptureRing.tsx +91 -0
  186. package/src/screens/liveness/LivenessCamera.tsx +63 -0
  187. package/src/screens/liveness/LivenessHandover.tsx +43 -0
  188. package/src/screens/liveness/LivenessOutcome.tsx +8 -4
  189. package/src/screens/liveness/SelfiePreview.tsx +31 -7
  190. package/src/screens/liveness/ShutterFlash.tsx +25 -0
  191. package/src/screens/liveness/index.ts +4 -0
  192. package/src/screens/liveness/useSelfieUpload.ts +23 -3
  193. package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
  194. package/src/screens/useAddressPhotoAttach.ts +77 -0
  195. package/src/screens/useBusinessDocumentAttach.ts +93 -0
  196. package/src/screens/usePoaAttach.ts +12 -15
  197. package/src/services/api-types.ts +119 -1
  198. package/src/services/api-verify-types.ts +37 -0
  199. package/src/services/api.ts +149 -3
  200. package/src/services/deviceMetadata.ts +1 -1
  201. package/src/services/location.ts +234 -0
  202. package/src/services/mediaCompress.ts +23 -5
  203. package/src/services/uploadErrors.ts +30 -0
  204. package/src/services/workflowGate.ts +7 -0
  205. package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
  206. package/src/store/address.ts +97 -0
  207. package/src/store/derive.ts +29 -4
  208. package/src/store/kycStore.ts +120 -5
  209. package/src/store/serverConfig.ts +18 -0
  210. package/src/store/session.ts +23 -4
  211. package/src/store/state.ts +134 -0
  212. package/src/store/submit.ts +18 -1
  213. package/src/types/config.ts +63 -1
  214. package/src/types/verification.ts +12 -0
  215. package/src/types/workflow.ts +97 -1
  216. package/src/assets/liveness/Blink.gif +0 -0
  217. package/src/assets/liveness/Nod.gif +0 -0
  218. package/src/assets/liveness/Smile.gif +0 -0
  219. package/src/assets/liveness/Turn.gif +0 -0
@@ -0,0 +1,25 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing } from 'react-native';
3
+ import { StyleAbsFill } from './constants';
4
+
5
+ // The shutter: a white flash over the camera circle as the still is taken,
6
+ // fading out over the beat the ring takes to close and go green. Mirrors the
7
+ // web SDK's animate-capture-flash (600ms ease-out). Core Animated on the
8
+ // native driver — opacity only — is the same recipe LightingBanner uses.
9
+ export function ShutterFlash() {
10
+ const opacity = useRef(new Animated.Value(0.85)).current;
11
+ useEffect(() => {
12
+ Animated.timing(opacity, {
13
+ toValue: 0,
14
+ duration: 600,
15
+ easing: Easing.out(Easing.cubic),
16
+ useNativeDriver: true,
17
+ }).start();
18
+ }, [opacity]);
19
+ return (
20
+ <Animated.View
21
+ pointerEvents="none"
22
+ style={[StyleAbsFill, { backgroundColor: '#FFFFFF', opacity }]}
23
+ />
24
+ );
25
+ }
@@ -3,6 +3,10 @@ export { StyleAbsFill, FRESH_FACE_TIMEOUT_MS, INSTRUCTION_HEIGHT } from './const
3
3
  export { LightingBanner } from './LightingBanner';
4
4
  export { resolveGuidance } from './resolveGuidance';
5
5
  export { ProgressDots } from './ProgressDots';
6
+ export { CaptureRing } from './CaptureRing';
7
+ export { ShutterFlash } from './ShutterFlash';
6
8
  export { SelfiePreview } from './SelfiePreview';
7
9
  export { useSelfieUpload } from './useSelfieUpload';
8
10
  export { LivenessComplete, LivenessFailed } from './LivenessOutcome';
11
+ export { LivenessCamera } from './LivenessCamera';
12
+ export { LivenessHandover, useSelfieAutoAdvance } from './LivenessHandover';
@@ -44,12 +44,29 @@ export function useSelfieUpload(): SelfieUpload {
44
44
  const config = useKycConfig();
45
45
  const api = useKyc((s) => s.api);
46
46
  const setMediaId = useKyc((s) => s.setMediaId);
47
+ const setSelfiePreview = useKyc((s) => s.setSelfiePreview);
48
+ // The store-side progress report. The liveness step may hand over before
49
+ // the upload lands (the biometric scopes hide the review), so the submitted
50
+ // step waits on THIS rather than on this hook's local state.
51
+ const setSelfieUpload = useKyc((s) => s.setSelfieUpload);
52
+ const storedPreview = useKyc((s) => s.selfiePreviewUri);
53
+ const storedSelfieId = useKyc((s) => s.mediaIds.selfie);
47
54
 
48
- const [selfieUri, setSelfieUri] = useState<string | null>(null);
55
+ // Hydrated from the STORE so leaving the step and returning lands on the
56
+ // review screen with the same selfie, exactly as document captures do. The
57
+ // setter writes through, keeping the store the source of truth.
58
+ const [selfieUri, setSelfieUriState] = useState<string | null>(storedPreview);
59
+ const setSelfieUri = useCallback(
60
+ (uri: string | null) => {
61
+ setSelfieUriState(uri);
62
+ setSelfiePreview(uri);
63
+ },
64
+ [setSelfiePreview],
65
+ );
49
66
  const [uploading, setUploading] = useState(false);
50
67
  const [retryInfo, setRetryInfo] = useState<{ attempt: number; total: number } | null>(null);
51
68
  const [uploadError, setUploadError] = useState<string | null>(null);
52
- const selfieIdRef = useRef<string | null>(null);
69
+ const selfieIdRef = useRef<string | null>(storedSelfieId ?? null);
53
70
  const videoPathRef = useRef<string | null>(null);
54
71
 
55
72
  const uploadSelfieAndVideo = useCallback(
@@ -57,6 +74,7 @@ export function useSelfieUpload(): SelfieUpload {
57
74
  setUploading(true);
58
75
  setUploadError(null);
59
76
  setRetryInfo(null);
77
+ setSelfieUpload({ status: 'uploading', message: null });
60
78
  const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
61
79
  try {
62
80
  const selfieId = await withRetry(
@@ -81,16 +99,18 @@ export function useSelfieUpload(): SelfieUpload {
81
99
  }
82
100
  setRetryInfo(null);
83
101
  setUploading(false);
102
+ setSelfieUpload({ status: 'done', message: null });
84
103
  } catch (err) {
85
104
  setRetryInfo(null);
86
105
  setUploading(false);
87
106
  const kycError = mapToKycError(err, 'upload');
88
107
  setUploadError(kycError.message);
108
+ setSelfieUpload({ status: 'failed', message: kycError.message });
89
109
  toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
90
110
  safeReportError(config.onError, kycError);
91
111
  }
92
112
  },
93
- [api, setMediaId, config.onError, toast],
113
+ [api, setMediaId, setSelfieUpload, config.onError, toast],
94
114
  );
95
115
 
96
116
  return {
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import { AccessibilityInfo, Animated, Easing, View } from 'react-native';
3
- import Svg, { Circle, Defs, G, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
3
+ import Svg, { Circle, G, Path, Rect } from 'react-native-svg';
4
4
 
5
5
  import { useTheme } from '../../components/runtime';
6
6
 
@@ -112,22 +112,6 @@ export function NfcScanIllustration(): React.ReactElement {
112
112
  // Same footprint as the web: full width up to 384 (`max-w-sm`), 320:240.
113
113
  <View style={{ width: '100%', maxWidth: 384, aspectRatio: 320 / 240, alignSelf: 'center' }}>
114
114
  <Svg width="100%" height="100%" viewBox="0 0 320 240">
115
- <Defs>
116
- {/* The field falls off with distance — the reason the document has
117
- to be held CLOSE, said as a gradient instead of a caption. */}
118
- <RadialGradient
119
- id="kyc-nfc-field"
120
- cx={COUPLING.x}
121
- cy={COUPLING.y}
122
- r={90}
123
- gradientUnits="userSpaceOnUse"
124
- >
125
- <Stop offset="0" stopColor={colors.primary} stopOpacity={0.18} />
126
- <Stop offset="0.55" stopColor={colors.primary} stopOpacity={0.06} />
127
- <Stop offset="1" stopColor={colors.primary} stopOpacity={0} />
128
- </RadialGradient>
129
- </Defs>
130
-
131
115
  {/* ── The document, tucked BEHIND the phone: portrait, chip, MRZ. ── */}
132
116
  <G rotation={7} origin="212, 112">
133
117
  <Rect
@@ -224,7 +208,12 @@ export function NfcScanIllustration(): React.ReactElement {
224
208
  </G>
225
209
 
226
210
  {/* ── The field, over both objects. ── */}
227
- <Circle cx={COUPLING.x} cy={COUPLING.y} r={90} fill="url(#kyc-nfc-field)" />
211
+ {/* The field falls off with distance — the reason the document has to
212
+ be held CLOSE. Flat concentric steps (no gradients in the flow,
213
+ house rule 2026-08-29). */}
214
+ <Circle cx={COUPLING.x} cy={COUPLING.y} r={90} fill={colors.primary} fillOpacity={0.04} />
215
+ <Circle cx={COUPLING.x} cy={COUPLING.y} r={56} fill={colors.primary} fillOpacity={0.07} />
216
+ <Circle cx={COUPLING.x} cy={COUPLING.y} r={28} fill={colors.primary} fillOpacity={0.12} />
228
217
  <Circle cx={COUPLING.x} cy={COUPLING.y} r={4.5} fill={colors.primary} />
229
218
  {WAVES.map((w, i) => (
230
219
  <AnimatedPath
@@ -0,0 +1,77 @@
1
+ import { useCallback, useState } from 'react';
2
+ import * as ImagePicker from 'expo-image-picker';
3
+
4
+ import { isAcceptedAddressPhotoMimeType } from '../config/addressCollection';
5
+ import { IMAGE_MAX_BYTES } from '../config/uploadLimits';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Getting the door photo off the device — the address step's slim sibling of
9
+ // usePoaAttach. Camera or photo library only: the door photo is a PICTURE of a
10
+ // place, so there is no PDF/file case, and each pick closes the source sheet
11
+ // before the picker opens.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** What the picker handed back, so the screen can preview it. */
15
+ export interface AddressPhotoPick {
16
+ uri: string;
17
+ mimeType: string | undefined;
18
+ name: string;
19
+ }
20
+
21
+ type UploadFn = (pick: AddressPhotoPick) => Promise<void>;
22
+
23
+ export function useAddressPhotoAttach(
24
+ upload: UploadFn,
25
+ setError: (message: string) => void,
26
+ ): {
27
+ pick: () => void;
28
+ sheetOpen: boolean;
29
+ closeSheet: () => void;
30
+ choosePhoto: () => Promise<void>;
31
+ takePhoto: () => Promise<void>;
32
+ } {
33
+ const [sheetOpen, setSheetOpen] = useState(false);
34
+ const pick = useCallback(() => setSheetOpen(true), []);
35
+ const closeSheet = useCallback(() => setSheetOpen(false), []);
36
+
37
+ const accept = useCallback(
38
+ async (asset: ImagePicker.ImagePickerAsset | undefined, fallbackName: string) => {
39
+ if (!asset) return;
40
+ if (typeof asset.fileSize === 'number' && asset.fileSize > IMAGE_MAX_BYTES) {
41
+ setError('Photo is too large (max 5 MB).');
42
+ return;
43
+ }
44
+ const mimeType = asset.mimeType ?? 'image/jpeg';
45
+ // The picker's media filter is advisory on some platforms, so what came
46
+ // back is re-checked rather than trusted.
47
+ if (!isAcceptedAddressPhotoMimeType(mimeType)) {
48
+ setError('Please choose a photo (JPEG, PNG or WebP).');
49
+ return;
50
+ }
51
+ await upload({ uri: asset.uri, mimeType, name: asset.fileName ?? fallbackName });
52
+ },
53
+ [setError, upload],
54
+ );
55
+
56
+ const takePhoto = useCallback(async () => {
57
+ setSheetOpen(false);
58
+ const permission = await ImagePicker.requestCameraPermissionsAsync();
59
+ if (!permission.granted) {
60
+ setError('Camera access is needed to photograph the entrance.');
61
+ return;
62
+ }
63
+ const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 1 });
64
+ await accept(result.canceled ? undefined : result.assets[0], 'address-photo.jpg');
65
+ }, [accept, setError]);
66
+
67
+ const choosePhoto = useCallback(async () => {
68
+ setSheetOpen(false);
69
+ const result = await ImagePicker.launchImageLibraryAsync({
70
+ mediaTypes: ['images'],
71
+ quality: 1,
72
+ });
73
+ await accept(result.canceled ? undefined : result.assets[0], 'address-photo.jpg');
74
+ }, [accept]);
75
+
76
+ return { pick, sheetOpen, closeSheet, choosePhoto, takePhoto };
77
+ }
@@ -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,
@@ -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
@@ -41,6 +43,34 @@ export interface UploadResponse {
41
43
  mediaId: string;
42
44
  }
43
45
 
46
+ /** Which side of a document a capture check is about. */
47
+ export type DocumentCaptureSide = 'front' | 'back';
48
+
49
+ /**
50
+ * Ask whether an uploaded document photo will be readable, using the same
51
+ * detectors the server later decides the verification with.
52
+ */
53
+ export interface DocumentCaptureCheckRequest {
54
+ mediaId: string;
55
+ side: DocumentCaptureSide;
56
+ country: string;
57
+ idType: string;
58
+ /** The workflow the submission will carry, when it carries one. */
59
+ workflowId?: string;
60
+ /** The attempt session from /session/start, when the flow has one. */
61
+ sessionId?: string;
62
+ }
63
+
64
+ /**
65
+ * `false` = looked and found nothing (ask for a retake); `true` = fine;
66
+ * `null` = not applicable, or the server could not look (treat as fine).
67
+ */
68
+ export interface DocumentCaptureCheckResponse {
69
+ side: DocumentCaptureSide;
70
+ face: boolean | null;
71
+ barcode: boolean | null;
72
+ }
73
+
44
74
  export interface KeyPersonInvite {
45
75
  keyPersonId: string;
46
76
  name: string;
@@ -142,6 +172,80 @@ export interface SdkConfigResponse {
142
172
  * carries the same lookup as a RISK signal, and the two must not be confused.
143
173
  */
144
174
  geoCountry?: string | null;
175
+ /** Whether the platform has a forward-search backend. Gates the whole
176
+ * address-search step; absent means no search screen, never an error. */
177
+ addressSearch?: boolean;
178
+ /** Which backend: 'autocomplete' (Places, as-you-type) or 'basic'
179
+ * (explicit submit). Absent when addressSearch is false. */
180
+ addressSearchMode?: 'autocomplete' | 'basic';
181
+ /**
182
+ * The framed Google-map picker page (OUR hosted /embed/map + a signed APP
183
+ * grant), for a WebView. Absent when the platform holds no Maps key — the
184
+ * built-in OSM picker is the fallback every map failure degrades to.
185
+ */
186
+ mapsFrameUrl?: string | null;
187
+ }
188
+
189
+ // ── Address search / reverse geocoding ──────────────────────────────────────
190
+ //
191
+ // Every one of these may fail, and every failure degrades to "place the pin by
192
+ // hand" — never to a blocked flow. Shapes mirror the web SDK's services/api.ts.
193
+
194
+ /** The pin's address broken down — what the details sheet displays as rows. */
195
+ export interface AddressParts {
196
+ street?: string | null;
197
+ area?: string | null;
198
+ city?: string | null;
199
+ state?: string | null;
200
+ postcode?: string | null;
201
+ /**
202
+ * ISO-2 of the pin's OWN country, from the geocoder. On the address scope
203
+ * the declared country follows it (lib/country-adoption.ts); it is never a
204
+ * row in the sheet, which shows the country the flow declared.
205
+ */
206
+ country?: string | null;
207
+ }
208
+
209
+ export interface AddressReverseResult {
210
+ line: string | null;
211
+ road: string | null;
212
+ parts?: AddressParts | null;
213
+ }
214
+
215
+ /** One candidate from the basic (explicit-submit) forward search. */
216
+ export interface AddressSearchHit {
217
+ label: string;
218
+ lat: number;
219
+ lng: number;
220
+ houseNumber: string | null;
221
+ road: string | null;
222
+ /** ISO-2 of the hit's own country (the declaration derives from it). */
223
+ country?: string | null;
224
+ }
225
+
226
+ /** One Places autocomplete suggestion. */
227
+ export interface PlaceSuggestion {
228
+ placeId: string;
229
+ mainText: string;
230
+ secondaryText: string;
231
+ }
232
+
233
+ /** A picked suggestion, resolved to coordinates + structured pieces. */
234
+ export interface ResolvedPlace {
235
+ lat: number;
236
+ lng: number;
237
+ houseNumber: string | null;
238
+ road: string | null;
239
+ formatted: string | null;
240
+ area?: string | null;
241
+ city?: string | null;
242
+ state?: string | null;
243
+ postcode?: string | null;
244
+ /**
245
+ * ISO-2 of the picked address's own country — the declaration derives from
246
+ * it on the address scope (a pick is the applicant's own statement).
247
+ */
248
+ country?: string | null;
145
249
  }
146
250
 
147
251
  /**
@@ -182,6 +286,20 @@ export interface WorkflowResolutionResponse {
182
286
  * carries the same lookup as a RISK signal, and the two must not be confused.
183
287
  */
184
288
  geoCountry?: string | null;
289
+ /**
290
+ * The address-search availability flags, when the resolution route carries
291
+ * them.
292
+ *
293
+ * A `workflowId` mount skips `/config` entirely, so these are the only route
294
+ * by which such a mount could learn a search backend exists. The server does
295
+ * not serve them here yet (only `/config` and the hosted bootstrap do), which
296
+ * is why they degrade to "no search screen" rather than an error.
297
+ */
298
+ addressSearch?: boolean;
299
+ addressSearchMode?: 'autocomplete' | 'basic';
300
+ /** The framed Google-map picker page, when the route carries one (see
301
+ * SdkConfigResponse.mapsFrameUrl). */
302
+ mapsFrameUrl?: string | null;
185
303
  /** KYB only: the mapped applicant workflow, when configured and resolvable. */
186
304
  applicantWorkflow?: ApplicantWorkflowPayload | null;
187
305
  }
@@ -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