@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,172 @@
1
+ import type { KYCStep } from '../types/config';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // The address flow as REAL steps: find it (search) → confirm it (pin, with the
5
+ // details sheet) → show it (entrance) → commit it (review + attest fix). The
6
+ // progress bar advances through them and back/forward is ordinary step
7
+ // navigation, not a machine hidden inside one screen. KYB keeps the single
8
+ // premises step — its pin + directions ARE the capture, and the KYB flow
9
+ // already has its own section rhythm.
10
+ //
11
+ // 'address-collection' is the PIN step and keeps the original wire name, so
12
+ // session progress saved by older builds restores cleanly and the server's
13
+ // step-log titles stay meaningful. Do not "fix" it to 'address-pin'.
14
+ //
15
+ // A MIRROR of the web SDK's steps/address/flow-steps.ts and the Flutter port —
16
+ // the same numbers and the same decisions, or one platform strands an
17
+ // applicant the others do not. Keep all three in lockstep.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * SANDBOX mirrors the web SDK's stubbed vendor treatment (user decision
22
+ * 2026-09-03): placeholder map, canned labels, no search loads — the server's
23
+ * sandbox verdicts are canned anyway, so live vendor calls on test keys spend
24
+ * quota for nothing. DEVELOPMENT keeps the real surfaces (the platform's
25
+ * dev-is-real rule); an unknown environment counts as live.
26
+ */
27
+ export function addressVendorsStubbed(facts: { environment?: string | null }): boolean {
28
+ return facts.environment === 'SANDBOX';
29
+ }
30
+
31
+ /** The canned pin label every stubbed surface shows — obviously a sample,
32
+ * never a real place. Keep in lockstep with the web SDK. */
33
+ export const SAMPLE_ADDRESS_LINE = '12 Sample Street, Sample City';
34
+
35
+ export interface AddressFlowOptions {
36
+ /** A search backend is available AND we are not in builder preview. */
37
+ searchAvailable: boolean;
38
+ /** The entrance-photo mode ('off' hides that capture). */
39
+ photoMode: 'off' | 'optional' | 'required';
40
+ /** Street View framing is offered (workflow on + a browser key in-document). */
41
+ streetViewOffered: boolean;
42
+ }
43
+
44
+ /** The individual flow's address steps, in order. */
45
+ export function addressFlowSteps(o: AddressFlowOptions): KYCStep[] {
46
+ const steps: KYCStep[] = [];
47
+ if (o.searchAvailable) steps.push('address-search');
48
+ steps.push('address-collection');
49
+ if (o.photoMode !== 'off' || o.streetViewOffered) steps.push('address-entrance');
50
+ steps.push('address-review');
51
+ return steps;
52
+ }
53
+
54
+ const ADDRESS_STEPS: ReadonlySet<KYCStep> = new Set<KYCStep>([
55
+ 'address-search',
56
+ 'address-collection',
57
+ 'address-entrance',
58
+ 'address-review',
59
+ ]);
60
+
61
+ /** Whether `step` belongs to the address flow. */
62
+ export function isAddressStep(step: KYCStep): boolean {
63
+ return ADDRESS_STEPS.has(step);
64
+ }
65
+
66
+ export function nextAddressStep(steps: KYCStep[], current: KYCStep): KYCStep | null {
67
+ const i = steps.indexOf(current);
68
+ return i >= 0 && i + 1 < steps.length ? steps[i + 1]! : null;
69
+ }
70
+
71
+ export function prevAddressStep(steps: KYCStep[], current: KYCStep): KYCStep | null {
72
+ const i = steps.indexOf(current);
73
+ return i > 0 ? steps[i - 1]! : null;
74
+ }
75
+
76
+ /**
77
+ * Derive the flow options from raw config facts — ONE place, read by the step
78
+ * hook AND the flow-order builder, so the two can never disagree about which
79
+ * screens exist. A mirror of the web SDK's addressFlowOptions.
80
+ *
81
+ * `hasGoogleKey` is always false on mobile (the key describes an in-document
82
+ * browser surface). Street View reaches a phone the same way the framed map
83
+ * does: the hosted /embed/street-view page in a WebView on the app grant,
84
+ * which is `hasStreetViewFrame` — the server minted a maps frame URL AND
85
+ * `react-native-webview` is installed. Absent either, the entrance step is
86
+ * photo-only, exactly as it was before 2026-09-06.
87
+ */
88
+ export function addressFlowOptions(facts: {
89
+ photo?: 'off' | 'optional' | 'required';
90
+ streetView?: 'off' | 'optional' | 'required';
91
+ serverSearch: boolean;
92
+ previewMode: boolean;
93
+ hasGoogleKey: boolean;
94
+ /** A maps frame URL this install can render: the framed
95
+ * /embed/street-view page carries the panorama for it. */
96
+ hasStreetViewFrame: boolean;
97
+ }): AddressFlowOptions {
98
+ const photoMode = facts.photo ?? 'optional';
99
+ return {
100
+ searchAvailable: facts.serverSearch && !facts.previewMode,
101
+ photoMode,
102
+ // ON by default: offered unless the workflow opted out, and wherever a
103
+ // Google surface exists.
104
+ streetViewOffered:
105
+ facts.streetView !== 'off' && (facts.hasGoogleKey || facts.hasStreetViewFrame),
106
+ };
107
+ }
108
+
109
+ /** Great-circle metres between two points (small-distance haversine). */
110
+ export function metersBetween(
111
+ a: { lat: number; lng: number },
112
+ b: { lat: number; lng: number },
113
+ ): number {
114
+ const R = 6371000;
115
+ const toRad = (d: number) => (d * Math.PI) / 180;
116
+ const dLat = toRad(b.lat - a.lat);
117
+ const dLng = toRad(b.lng - a.lng);
118
+ const h =
119
+ Math.sin(dLat / 2) ** 2 +
120
+ Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLng / 2) ** 2;
121
+ return 2 * R * Math.asin(Math.sqrt(h));
122
+ }
123
+
124
+ /**
125
+ * A map settle within about a metre of the current pin is the tile roundtrip
126
+ * drifting, not a move. Acting on it rebuilt the address WITHOUT its label, and
127
+ * the reverse geocoder then overwrote a searched-and-picked address with the
128
+ * area line. BOTH axes must be inside the epsilon for a move to be ignored.
129
+ */
130
+ export const PIN_EPSILON = 1e-5;
131
+
132
+ /**
133
+ * How far a pin may move from the spot a label was PICKED for before the label
134
+ * stops credibly naming it. The pick names the property; the nudge refines
135
+ * where its roof is — same scale as the server's at-address tolerance. Beyond
136
+ * this, keeping the picked name would be a lie.
137
+ */
138
+ export const KEEP_PICKED_LABEL_RADIUS_M = 250;
139
+
140
+ /** Below this, a pin move is roof refinement — keep the picked label silently;
141
+ * asking would be noise. */
142
+ export const LABEL_PROMPT_MIN_MOVE_M = 25;
143
+
144
+ /** Debounce before reverse-geocoding a moved pin. */
145
+ export const REVERSE_DEBOUNCE_MS = 700;
146
+
147
+ /** Places autocomplete: debounce, and the floor below which nothing is asked. */
148
+ export const AUTOCOMPLETE_DEBOUNCE_MS = 300;
149
+ export const SEARCH_MIN_QUERY_LENGTH = 3;
150
+
151
+ /**
152
+ * Whether the pin screen should ASK "keep the selected address?" — the
153
+ * applicant decides the label's fate, never a silent discard. Ask once past the
154
+ * refinement threshold; a prior "keep" stands until the pin crosses the
155
+ * credibility radius, where setPin resets it so the question returns exactly
156
+ * once. A DERIVED label (no pickedAt anchor) is never questioned: those
157
+ * re-derive freely on every move.
158
+ */
159
+ export function shouldAskLabelDecision(address: {
160
+ lat: number;
161
+ lng: number;
162
+ label?: string;
163
+ pickedAt?: { lat: number; lng: number };
164
+ labelKept?: boolean;
165
+ }): boolean {
166
+ if (!address.pickedAt || !address.label || address.labelKept) return false;
167
+ return metersBetween(address.pickedAt, address) > LABEL_PROMPT_MIN_MOVE_M;
168
+ }
169
+
170
+ // The displayed address line lives next door (200-line rule), re-exported so
171
+ // every importer of this file keeps working.
172
+ export { displayAddressLine } from './address-line';
@@ -0,0 +1,48 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Address state shaping — the pure half of "the pin moved".
3
+ //
4
+ // Split from address-flow.ts (the step model + the label rules) per the
5
+ // 200-line rule, and a MIRROR of the web SDK's steps/address-helpers.ts.
6
+ //
7
+ // The device fixes themselves live in services/location.ts, which owns the
8
+ // expo-location calls; nothing here touches hardware.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * The address state after picking a search candidate: the pin lands on the
13
+ * hit, the applicant's typed fields survive, and the house number prefills
14
+ * ONLY when they have not typed one (their word always beats the map's).
15
+ *
16
+ * The shape is FIXED on purpose. Spreading it as the new address DROPS
17
+ * `label`, `parts`, `pickedAt`, `labelKept`, `streetView` and the device
18
+ * fields, so a caller that wants one of those back has to re-add it
19
+ * explicitly rather than carrying a stale label to a new spot by accident.
20
+ */
21
+ export function pickedAddressState(
22
+ prev:
23
+ | { directions?: string; propertyName?: string; propertyNumber?: string; street?: string }
24
+ | null
25
+ | undefined,
26
+ hit: { lat: number; lng: number; houseNumber: string | null },
27
+ ): {
28
+ lat: number;
29
+ lng: number;
30
+ accuracy: null;
31
+ directions: string;
32
+ propertyName: string;
33
+ propertyNumber: string;
34
+ street: string | undefined;
35
+ } {
36
+ return {
37
+ lat: hit.lat,
38
+ lng: hit.lng,
39
+ accuracy: null,
40
+ directions: prev?.directions ?? '',
41
+ propertyName: prev?.propertyName ?? '',
42
+ propertyNumber: prev?.propertyNumber?.trim() ? prev.propertyNumber : (hit.houseNumber ?? ''),
43
+ // Undefined, not '': the details sheet reads '' as "deliberately cleared"
44
+ // and undefined as "never touched" — initialising with '' suppressed the
45
+ // resolved-street prefill forever.
46
+ street: prev?.street,
47
+ };
48
+ }
@@ -0,0 +1,76 @@
1
+ // The address line the flow SHOWS. Split from address-flow.ts (200-line
2
+ // rule); a mirror of the web SDK's displayAddressLine and the Flutter
3
+ // address_flow.dart twin. Keep the three in lockstep.
4
+
5
+ /**
6
+ * The address line the flow SHOWS (pin summary + review heading) — the client
7
+ * mirror of the server's composed-line rules, so what the applicant confirms is
8
+ * what the org later reads. A typed number REPLACES a differing picked number
9
+ * (living at 8 when only 11 was listed is not "8, 11 Bassey Street"); a typed
10
+ * street the label does not carry leads the line.
11
+ */
12
+ export function displayAddressLine(address: {
13
+ lat: number;
14
+ lng: number;
15
+ label?: string | null;
16
+ propertyNumber?: string | null;
17
+ street?: string | null;
18
+ unit?: string | null;
19
+ neighbourhood?: string | null;
20
+ city?: string | null;
21
+ state?: string | null;
22
+ postcode?: string | null;
23
+ }): string {
24
+ const number = address.propertyNumber?.trim() || null;
25
+ const typed = address.street?.trim() || null;
26
+ const label = address.label?.trim() || null;
27
+ const unit = address.unit?.trim() || null;
28
+ // Edit-details corrections ride the tail through a part-wise dedupe,
29
+ // mirroring the server's composed line: identical values vanish, a
30
+ // correction appends beside the map's own answer.
31
+ const withClaims = (parts: string[]): string => {
32
+ const out = unit ? [unit, ...parts] : [...parts];
33
+ const seen = (v: string) => out.some((p) => p.toLowerCase() === v.toLowerCase());
34
+ for (const claim of [address.neighbourhood, address.city, address.state, address.postcode]) {
35
+ const t = claim?.trim();
36
+ if (t && !seen(t)) out.push(t);
37
+ }
38
+ return out.join(', ');
39
+ };
40
+ if (!label) {
41
+ if (typed) return withClaims([number ? `${number} ${typed}` : typed]);
42
+ const claimed = withClaims([]);
43
+ if (claimed) return claimed;
44
+ // NEVER coordinates. A moved pin has no line until the reverse geocode
45
+ // answers, and "4.93240, 8.32540" is not an address: it read as one, for
46
+ // the second or so before the real line arrived. Empty means "nothing
47
+ // human-readable yet", and the caller shows that it is still coming.
48
+ return '';
49
+ }
50
+ const segs = label
51
+ .split(', ')
52
+ .map((t) => t.trim())
53
+ .filter(Boolean);
54
+ if (typed && !label.toLowerCase().includes(typed.toLowerCase())) {
55
+ return withClaims([number ? `${number} ${typed}` : typed, ...segs]);
56
+ }
57
+ if (number) {
58
+ const first = segs[0] ?? '';
59
+ const firstTokens = first.toLowerCase().split(/\s+/);
60
+ const leading = firstTokens[0] ?? '';
61
+ if (/^\d+[a-z]?$/i.test(leading) && leading !== number.toLowerCase()) {
62
+ segs[0] = [number, ...first.split(/\s+/).slice(1)].join(' ');
63
+ } else if (!firstTokens.includes(number.toLowerCase())) {
64
+ return withClaims([number, ...segs]);
65
+ }
66
+ }
67
+ return withClaims(segs);
68
+ }
69
+
70
+ /** Read to assistive tech while the reverse geocode is out: the pin has a
71
+ * line coming, and a lat/lng pair is not it. Sighted users see a skeleton
72
+ * line in its place (components/LineSkeleton), never a spinner. Mirrored on
73
+ * the web and Flutter SDKs. */
74
+ export const ADDRESS_LINE_PENDING = 'Finding the address…';
75
+ /** Shown when the geocode came back with nothing. Still not coordinates. */
76
+ export const ADDRESS_LINE_UNAVAILABLE = 'No address found for this spot';
@@ -0,0 +1,97 @@
1
+ import { pickedAddressState } from './address-helpers';
2
+ import { KEEP_PICKED_LABEL_RADIUS_M, PIN_EPSILON, metersBetween } from './address-flow';
3
+ import type { AddressState } from '../store/state';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // What the address becomes when the pin moves.
7
+ //
8
+ // Pure, so the rules that decide whether a human-confirmed label survives can
9
+ // be read and tested without a map, a store, or a GPS. The hook does the
10
+ // dispatching; these decide.
11
+ //
12
+ // A MIRROR of the web SDK's setPin / applyCurrentFix / adoptPinAddress bodies.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Where a pin move lands. `ignore` means the map settled, it did not move. */
16
+ export type PinMove =
17
+ | { kind: 'ignore' }
18
+ /** The picked label stands: only the coordinates (and the keep flag) change,
19
+ * and NO reverse geocode is wanted. */
20
+ | { kind: 'keep-label'; address: AddressState }
21
+ /** The address is rebuilt around the new spot and wants a fresh label. */
22
+ | { kind: 'rebuild'; address: AddressState };
23
+
24
+ export function pinMove(
25
+ cur: AddressState | null,
26
+ next: { lat: number; lng: number },
27
+ accuracy: number | null,
28
+ ): PinMove {
29
+ // A map settle within about a metre of the current pin is the tile roundtrip
30
+ // drifting, not a move. Acting on it rebuilt the address WITHOUT its label,
31
+ // and the reverse geocoder then overwrote a searched-and-picked address with
32
+ // the area line. BOTH axes must be inside the epsilon.
33
+ if (
34
+ cur &&
35
+ Math.abs(cur.lat - next.lat) < PIN_EPSILON &&
36
+ Math.abs(cur.lng - next.lng) < PIN_EPSILON
37
+ ) {
38
+ return { kind: 'ignore' };
39
+ }
40
+
41
+ // A PICKED label names the property; the pin refines where its roof is, so
42
+ // the label survives the move and the applicant decides its fate. Crossing
43
+ // the credibility radius only resets a prior "keep", so the question is
44
+ // asked again exactly once out there.
45
+ if (cur?.pickedAt && cur.label) {
46
+ const beyond = metersBetween(cur.pickedAt, next) > KEEP_PICKED_LABEL_RADIUS_M;
47
+ return {
48
+ kind: 'keep-label',
49
+ address: {
50
+ ...cur,
51
+ lat: next.lat,
52
+ lng: next.lng,
53
+ accuracy,
54
+ ...(beyond && cur.labelKept ? { labelKept: false } : {}),
55
+ },
56
+ };
57
+ }
58
+
59
+ // Typed fields AND a captured Street View frame survive a re-pin; the
60
+ // coordinates always win. A DERIVED label dies with the old spot.
61
+ return {
62
+ kind: 'rebuild',
63
+ address: {
64
+ ...pickedAddressState(cur, { lat: next.lat, lng: next.lng, houseNumber: null }),
65
+ accuracy,
66
+ ...(cur?.streetView ? { streetView: cur.streetView } : {}),
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * The address after landing on the device's current fix — the bootstrap
73
+ * override, used while no address exists yet.
74
+ *
75
+ * It sets NO `pickedAt`, so the resulting label counts as DERIVED and
76
+ * re-derives freely on the next move. A fix is where the phone is, not a
77
+ * property the applicant named.
78
+ */
79
+ export function fixApplied(
80
+ cur: AddressState | null,
81
+ fix: { lat: number; lng: number; accuracy: number | null; label: string | null; parts?: unknown },
82
+ ): AddressState {
83
+ return {
84
+ ...pickedAddressState(cur, { lat: fix.lat, lng: fix.lng, houseNumber: null }),
85
+ accuracy: fix.accuracy,
86
+ ...(cur?.streetView ? { streetView: cur.streetView } : {}),
87
+ ...(fix.label ? { label: fix.label } : {}),
88
+ ...(fix.parts ? { parts: fix.parts as AddressState['parts'] } : {}),
89
+ };
90
+ }
91
+
92
+ /** The address with the human-confirmed label dropped, ready to be relabelled
93
+ * from the pin. */
94
+ export function withoutPickedLabel(cur: AddressState): AddressState {
95
+ const { label: _l, parts: _p, pickedAt: _a, labelKept: _k, ...rest } = cur;
96
+ return rest;
97
+ }
@@ -0,0 +1,63 @@
1
+ import { isAddressStep } from './address-flow';
2
+ import type { KYCStep } from '../types/config';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Recovering an attempt resumed onto an address step the flow no longer has.
6
+ //
7
+ // The address flow is the only part of the SDK whose SHAPE depends on something
8
+ // other than the workflow the applicant is walking: `searchAvailable` comes from
9
+ // a server flag that arrives after the session restore has already written the
10
+ // step it was saved on. Add a republish that drops the entrance photo, and a
11
+ // resumed applicant can be sitting on a screen the current order does not
12
+ // contain — where Continue has nothing to advance to and Back has no
13
+ // predecessor, so the flow is simply stuck.
14
+ //
15
+ // The rule is FORWARD: a step the flow no longer has is a capture it is no
16
+ // longer asking for, so the applicant carries on rather than being sent back to
17
+ // redo work they had already finished.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** The address flow's four screens, in the order they are always walked. */
21
+ const CANONICAL: readonly KYCStep[] = [
22
+ 'address-search',
23
+ 'address-collection',
24
+ 'address-entrance',
25
+ 'address-review',
26
+ ];
27
+
28
+ /**
29
+ * The step that follows the address region in an order that CONTAINS it — where
30
+ * an applicant belongs when the workflow has dropped address collection
31
+ * altogether. Computed from the real order rather than guessed, so it is the
32
+ * step they would have reached had they finished.
33
+ */
34
+ export function addressExitStep(orderWithAddress: readonly KYCStep[]): KYCStep | null {
35
+ for (let i = orderWithAddress.length - 1; i >= 0; i -= 1) {
36
+ if (isAddressStep(orderWithAddress[i]!)) return orderWithAddress[i + 1] ?? null;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Where to send an applicant sitting on an address step this flow does not
43
+ * contain, or null when there is nothing wrong.
44
+ *
45
+ * `inFlow` is the address steps the CURRENT order has, in order; `exit` is where
46
+ * to go when it has none left (see `addressExitStep`).
47
+ */
48
+ export function recoverAddressStep(
49
+ current: KYCStep,
50
+ inFlow: readonly KYCStep[],
51
+ exit: KYCStep | null,
52
+ ): KYCStep | null {
53
+ if (!isAddressStep(current) || inFlow.includes(current)) return null;
54
+ const from = CANONICAL.indexOf(current);
55
+ for (let i = from + 1; i < CANONICAL.length; i += 1) {
56
+ const step = CANONICAL[i]!;
57
+ if (inFlow.includes(step)) return step;
58
+ }
59
+ // Nothing after it either: the last screen the flow does have (KYB resumed
60
+ // onto a review it never renders belongs on its premises pin), else out of
61
+ // the address flow entirely.
62
+ return inFlow[inFlow.length - 1] ?? exit;
63
+ }
@@ -0,0 +1,78 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ // ─── Server-side pictures, fetched with the SDK's own bearer ────────────────
4
+ //
5
+ // The review card's two pictures (the static map, the framed Street View)
6
+ // come through OUR server, so the request has to carry the SDK's bearer.
7
+ // React Native's <Image> accepts `source.headers` for exactly this and honours
8
+ // it on iOS; on Android (RN 0.85, new architecture) the header never leaves
9
+ // the device: the server answered 401 to every thumbnail request and the card
10
+ // drew an empty bordered box where the entrance should have been (Galaxy S24,
11
+ // 2026-09-07). So the bytes are fetched with the same `fetch` the API client
12
+ // uses, which carries headers on both platforms, and <Image> is handed a data
13
+ // URI it cannot get wrong. The bytes are base64-encoded here rather than via
14
+ // Blob + FileReader, which RN's fetch cannot supply (see bytesToBase64).
15
+
16
+ export interface AuthedImageSource {
17
+ uri: string;
18
+ headers: Record<string, string>;
19
+ }
20
+
21
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
22
+
23
+ /** Plain base64, in JS. React Native's `Response.blob()` cannot build a Blob
24
+ * from the ArrayBuffer its own fetch holds ("Creating blobs from
25
+ * 'ArrayBuffer' … are not supported"), so Blob and FileReader are out; a
26
+ * thumbnail is tens of kilobytes and this loop is nothing. */
27
+ export function bytesToBase64(bytes: Uint8Array): string {
28
+ let out = '';
29
+ for (let i = 0; i < bytes.length; i += 3) {
30
+ const a = bytes[i] ?? 0;
31
+ const b = bytes[i + 1] ?? 0;
32
+ const c = bytes[i + 2] ?? 0;
33
+ const triple = (a << 16) | (b << 8) | c;
34
+ out += B64.charAt((triple >> 18) & 63) + B64.charAt((triple >> 12) & 63);
35
+ out += i + 1 < bytes.length ? B64.charAt((triple >> 6) & 63) : '=';
36
+ out += i + 2 < bytes.length ? B64.charAt(triple & 63) : '=';
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export async function fetchImageDataUri(source: AuthedImageSource, signal?: AbortSignal): Promise<string> {
42
+ const res = await fetch(source.uri, { headers: source.headers, signal });
43
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
44
+ const mime = res.headers.get('content-type')?.split(';')[0]?.trim() || 'image/jpeg';
45
+ const bytes = new Uint8Array(await res.arrayBuffer());
46
+ return `data:${mime};base64,${bytesToBase64(bytes)}`;
47
+ }
48
+
49
+ export interface AuthedImage {
50
+ /** The picture as a data URI once it has arrived. */
51
+ uri: string | null;
52
+ /** It never will: the request was refused or the bytes were unreadable. */
53
+ failed: boolean;
54
+ }
55
+
56
+ const IDLE: AuthedImage = { uri: null, failed: false };
57
+
58
+ /** One server picture, keyed on its URL; `label` names it in the dev log. */
59
+ export function useAuthedImage(source: AuthedImageSource | null, label: string): AuthedImage {
60
+ const [state, setState] = useState<AuthedImage>(IDLE);
61
+ const key = source?.uri ?? null;
62
+ useEffect(() => {
63
+ setState(IDLE);
64
+ if (!source) return undefined;
65
+ const controller = new AbortController();
66
+ fetchImageDataUri(source, controller.signal)
67
+ .then((uri) => setState({ uri, failed: false }))
68
+ .catch((err: unknown) => {
69
+ if (controller.signal.aborted) return;
70
+ if (__DEV__) console.warn(`${label} failed to load`, err);
71
+ setState({ uri: null, failed: true });
72
+ });
73
+ return () => controller.abort();
74
+ // The headers are the SDK's own and never change; the URL is the picture.
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ }, [key]);
77
+ return state;
78
+ }
@@ -0,0 +1,52 @@
1
+ import { KYCApiError } from '../services/api';
2
+ import { KYCError } from '../types/verification';
3
+ import type { BiometricAuthResponse } from '../services/api-types-biometric';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Biometric re-authentication — the pure half. Mirrors the web SDK's
7
+ // MyazaBiometricAuth error mapping and outcome shapes (keep in lockstep).
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type BiometricAuthOutcome =
11
+ | { kind: 'success'; result: BiometricAuthResponse }
12
+ | { kind: 'failed'; result: BiometricAuthResponse }
13
+ | { kind: 'error'; message: string };
14
+
15
+ export interface BiometricAuthenticated {
16
+ attemptId: string;
17
+ confidence: number | null;
18
+ /** Single-use proof; verify from your backend at `/biometric/verify-proof`. */
19
+ token?: string;
20
+ }
21
+
22
+ export interface BiometricAuthFailed {
23
+ status: BiometricAuthResponse['status'];
24
+ attemptId: string;
25
+ confidence: number | null;
26
+ }
27
+
28
+ /** A technical failure, in the SDK's typed vocabulary + a message a person
29
+ * can read. Never the server's own wording — that is an internal contract. */
30
+ export function mapAuthError(err: unknown): KYCError {
31
+ if (err instanceof KYCApiError) {
32
+ if (err.statusCode === 404 && err.code === 'not_enrolled') {
33
+ return new KYCError('unknown', "You're not set up for face verification yet.");
34
+ }
35
+ if (err.statusCode === 402) {
36
+ return new KYCError('insufficient_credits', 'Face verification is temporarily unavailable.');
37
+ }
38
+ if (err.statusCode === 401 || err.statusCode === 403) {
39
+ return new KYCError('invalid_api_key', 'This app is not authorised for face verification.');
40
+ }
41
+ }
42
+ return new KYCError('network_error', 'Something went wrong. Please try again.');
43
+ }
44
+
45
+ /** The verdict as the callbacks and the result screen read it. */
46
+ export function outcomeOf(result: BiometricAuthResponse): BiometricAuthOutcome {
47
+ return result.authenticated ? { kind: 'success', result } : { kind: 'failed', result };
48
+ }
49
+
50
+ export function defaultReauthLabel(companyName?: string): string {
51
+ return companyName ? `Verify it's you with ${companyName}` : "Verify it's you";
52
+ }
@@ -0,0 +1,51 @@
1
+ import { configScope } from './scope';
2
+ import { fillTokens } from '../utils/tokens';
3
+ import type { BiometricCopy, BiometricCopyText } from '../config/biometricOptions';
4
+ import type { MyazaKYCConfig } from '../types/config';
5
+
6
+ // ─── The org's copy on the biometric screens, ready to render ───────────────
7
+ //
8
+ // Pure. Resolves `config.biometric.copy` for the screens: tokens filled from
9
+ // `userData` exactly as the consent and success copy are, a field that empties
10
+ // out once its tokens resolve treated as absent (the default shows rather than
11
+ // a blank title), and everything null off the biometric scopes, where the
12
+ // block never applies. The screens hand the result to describeWaiting /
13
+ // describeOutcome (lib/result-copy.ts) as overrides. Mirrors the web SDK's
14
+ // lib/biometric-copy.ts and Flutter's config/biometric_copy.dart; keep the
15
+ // three in lockstep.
16
+
17
+ export interface ResolvedBiometricCopy {
18
+ waiting: BiometricCopyText | null;
19
+ verified: BiometricCopyText | null;
20
+ declined: BiometricCopyText | null;
21
+ }
22
+
23
+ export const NO_BIOMETRIC_COPY: ResolvedBiometricCopy = { waiting: null, verified: null, declined: null };
24
+
25
+ type CopyConfigLike = {
26
+ scope?: string;
27
+ biometric?: { copy?: BiometricCopy } | null;
28
+ userData?: MyazaKYCConfig['userData'];
29
+ };
30
+
31
+ export function biometricCopyFor(config: CopyConfigLike): ResolvedBiometricCopy {
32
+ const scope = configScope(config);
33
+ if (scope !== 'biometric-authentication' && scope !== 'biometric-enrollment') return NO_BIOMETRIC_COPY;
34
+ const copy = config.biometric?.copy;
35
+ if (!copy) return NO_BIOMETRIC_COPY;
36
+ const fill = (text: BiometricCopyText | undefined): BiometricCopyText | null => {
37
+ if (!text) return null;
38
+ const title = text.title ? fillTokens(text.title, config.userData) : '';
39
+ const description = text.description ? fillTokens(text.description, config.userData) : '';
40
+ const out: BiometricCopyText = {};
41
+ if (title) out.title = title;
42
+ if (description) out.description = description;
43
+ return Object.keys(out).length > 0 ? out : null;
44
+ };
45
+ return {
46
+ waiting: fill(copy.waiting),
47
+ // Enrolment shows no verdict, so a stray key there has no screen to land on.
48
+ verified: scope === 'biometric-authentication' ? fill(copy.verified) : null,
49
+ declined: scope === 'biometric-authentication' ? fill(copy.declined) : null,
50
+ };
51
+ }