@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
package/src/emrtd/read.ts CHANGED
@@ -2,6 +2,7 @@ import { Platform } from 'react-native';
2
2
 
3
3
  import { nativeModule } from './native';
4
4
  import { readChip, type EmrtdReadResult, EmrtdSessionError } from './session';
5
+ import type { AaChallenge } from './activeAuth';
5
6
  import {
6
7
  MAX_CHIP_ATTEMPTS,
7
8
  chipRetryDelayMs,
@@ -37,6 +38,12 @@ export interface ChipReadOptions {
37
38
  successMessage?: string;
38
39
  /** Read progress, so the UI can narrate an otherwise invisible operation. */
39
40
  onStage?: (stage: NfcReadStage) => void;
41
+ /**
42
+ * The Active-Authentication challenge issued by the SERVER (8 bytes). Absent
43
+ * ⇒ the anti-clone step is skipped and the read is exactly what it was
44
+ * before — the whole capability null-degrades rather than failing.
45
+ */
46
+ aaChallenge?: AaChallenge;
40
47
  }
41
48
 
42
49
  /**
@@ -151,7 +158,7 @@ export async function readPassportChip(
151
158
 
152
159
  try {
153
160
  onStage('waiting');
154
- const result = await readChip(native, mrz, onStage);
161
+ const result = await readChip(native, mrz, onStage, options.aaChallenge);
155
162
  if (!result.sod && attempt < MAX_CHIP_ATTEMPTS && !abandoned()) {
156
163
  partial = result;
157
164
  devLog(attempt, 'security-file', new Error('EF.SOD missing — retrying for a complete read'));
@@ -1,6 +1,8 @@
1
1
  import { fromBase64, toBase64 } from './bytes';
2
2
  import { buildBacChallenge, completeBac } from './bac';
3
3
  import { primitivesFromNative, type EmrtdPrimitives, type MrzKeyFields } from './crypto';
4
+ import { readExtraGroups, type ExtraGroupReads } from './extras';
5
+ import { readActiveAuth, type AaChallenge, type ActiveAuthRead } from './activeAuth';
4
6
  import { EF, readFile, type Transceive } from './files';
5
7
  import { readOptionalFile } from './optionalRead';
6
8
  import { PaceError, PREFER_PACE, tryPace, type PaceOutcome } from './open';
@@ -45,6 +47,22 @@ export interface EmrtdReadResult {
45
47
  sod?: string;
46
48
  /** DG2 — the portrait. Best-effort; the largest file and the likeliest to drop. */
47
49
  dg2?: string;
50
+ /** DG7 — the displayed signature image. Best-effort, COM-gated (see extras.ts). */
51
+ dg7?: string;
52
+ /** DG11 — additional personal details. Best-effort; many issuers omit it. */
53
+ dg11?: string;
54
+ /** DG12 — additional document details. Best-effort; many issuers omit it. */
55
+ dg12?: string;
56
+ /** DG15 — the chip's Active-Authentication public key. Absent on the many
57
+ * chips that support no AA at all. */
58
+ dg15?: string;
59
+ /** The chip's signature over the SERVER's challenge — the anti-clone proof.
60
+ * Verified server-side only; a client that checked its own chip could be
61
+ * patched to say yes. */
62
+ aaSignature?: string;
63
+ /** Which server-issued challenge that signature answers — echoed back so the
64
+ * server can spend it. Not read off the chip; carried through with it. */
65
+ aaChallengeId?: string;
48
66
  /** How the chip was unlocked. Reported to the server on the submission. */
49
67
  chipAuth: 'bac' | 'pace';
50
68
  /**
@@ -141,6 +159,12 @@ export async function readChip(
141
159
  * enough that silence reads as failure and prompts them to lift the document.
142
160
  */
143
161
  onStage?: (stage: NfcReadStage) => void,
162
+ /**
163
+ * The Active-Authentication challenge the SERVER issued, if it could. Ours to
164
+ * carry, never ours to choose: a client-chosen nonce makes a captured
165
+ * signature replayable, which is exactly the clone AA exists to catch.
166
+ */
167
+ aaChallenge?: AaChallenge,
144
168
  ): Promise<EmrtdReadResult> {
145
169
  const p = primitivesFromNative(native);
146
170
 
@@ -178,9 +202,20 @@ export async function readChip(
178
202
  const sod = await readOptionalFile(sm, transceive, EF.SOD, 'EF.SOD');
179
203
 
180
204
  let dg2: Uint8Array | null = null;
205
+ let extras: ExtraGroupReads = { dg7: null, dg11: null, dg12: null };
206
+ let activeAuth: ActiveAuthRead = {};
181
207
  if (sod) {
182
208
  onStage?.('readingPhoto');
183
209
  dg2 = await readOptionalFile(sm, transceive, EF.DG2, 'DG2');
210
+ // The optional detail groups come LAST: by now everything that matters is
211
+ // banked, so a drop here costs only nice-to-have context. Gated on the SOD
212
+ // like DG2 — the server authenticates each group against it.
213
+ onStage?.('readingDetails');
214
+ extras = await readExtraGroups(sm, transceive);
215
+ // The anti-clone challenge LAST: it is the only step that asks the chip to
216
+ // compute rather than read, so it is the slowest per byte and the one worth
217
+ // losing if the document leaves contact.
218
+ activeAuth = await readActiveAuth(sm, transceive, aaChallenge);
184
219
  }
185
220
  onStage?.('done');
186
221
 
@@ -188,6 +223,12 @@ export async function readChip(
188
223
  dg1: toBase64(dg1),
189
224
  ...(sod ? { sod: toBase64(sod) } : {}),
190
225
  ...(dg2 ? { dg2: toBase64(dg2) } : {}),
226
+ ...(extras.dg7 ? { dg7: toBase64(extras.dg7) } : {}),
227
+ ...(extras.dg11 ? { dg11: toBase64(extras.dg11) } : {}),
228
+ ...(extras.dg12 ? { dg12: toBase64(extras.dg12) } : {}),
229
+ ...(activeAuth.dg15 ? { dg15: activeAuth.dg15 } : {}),
230
+ ...(activeAuth.signature ? { aaSignature: activeAuth.signature } : {}),
231
+ ...(activeAuth.signature && aaChallenge ? { aaChallengeId: aaChallenge.id } : {}),
191
232
  chipAuth: access.chipAuth,
192
233
  paceOutcome: access.outcome,
193
234
  ...(access.detail ? { paceDetail: access.detail } : {}),
@@ -23,6 +23,8 @@ export type NfcReadStage =
23
23
  | 'readingSecurity'
24
24
  /** Reading DG2 — the chip's portrait. */
25
25
  | 'readingPhoto'
26
+ /** Reading the optional detail groups (DG7/DG11/DG12) — small and quick. */
27
+ | 'readingDetails'
26
28
  /** Everything that could be read has been. */
27
29
  | 'done';
28
30
 
@@ -33,6 +35,7 @@ export const NFC_STAGE_ORDER: NfcReadStage[] = [
33
35
  'readingData',
34
36
  'readingSecurity',
35
37
  'readingPhoto',
38
+ 'readingDetails',
36
39
  'done',
37
40
  ];
38
41
 
@@ -49,6 +52,8 @@ export function nfcStageLabel(stage: NfcReadStage): string {
49
52
  return 'Reading security data';
50
53
  case 'readingPhoto':
51
54
  return 'Reading photo';
55
+ case 'readingDetails':
56
+ return 'Reading extra details';
52
57
  case 'done':
53
58
  return 'Chip read complete';
54
59
  }
@@ -73,6 +78,8 @@ export function nfcStageDetail(stage: NfcReadStage): string {
73
78
  return 'Downloading the chip’s digital signature. This is the largest part and takes the longest — keep holding.';
74
79
  case 'readingPhoto':
75
80
  return 'Copying the photo stored on the chip.';
81
+ case 'readingDetails':
82
+ return 'Copying the optional details stored on the chip. These are small and quick.';
76
83
  case 'done':
77
84
  return 'Everything was read successfully.';
78
85
  }
@@ -98,6 +105,8 @@ export function nfcSheetMessage(stage: NfcReadStage): string {
98
105
  return 'Verifying security data — keep holding…';
99
106
  case 'readingPhoto':
100
107
  return 'Reading your photo — keep holding…';
108
+ case 'readingDetails':
109
+ return 'Almost done — keep holding…';
101
110
  case 'done':
102
111
  return 'Chip read complete';
103
112
  }
@@ -116,6 +125,8 @@ export function nfcStageProgress(stage: NfcReadStage): number {
116
125
  return 0.6;
117
126
  case 'readingPhoto':
118
127
  return 0.85;
128
+ case 'readingDetails':
129
+ return 0.95;
119
130
  case 'done':
120
131
  return 1;
121
132
  }
package/src/index.ts CHANGED
@@ -11,6 +11,16 @@
11
11
  export { MyazaKYC, useMyazaKYC } from './MyazaKYC';
12
12
  export type { MyazaKYCProps, UseMyazaKYCReturn } from './MyazaKYC';
13
13
 
14
+ // Returning-user face re-authentication (the web SDK's MyazaBiometricAuth).
15
+ export { MyazaBiometricAuth } from './MyazaBiometricAuth';
16
+ export type { MyazaBiometricAuthProps } from './MyazaBiometricAuth';
17
+ export type {
18
+ BiometricAuthRequest,
19
+ BiometricAuthResponse,
20
+ BiometricStatusResponse,
21
+ BiometricLivenessClaim,
22
+ } from './services/api-types-biometric';
23
+
14
24
  // Public config + callback types
15
25
  export type {
16
26
  MyazaKYCConfig,
@@ -32,6 +42,7 @@ export type {
32
42
  PhoneVerificationConfig,
33
43
  ProofOfAddressConfig,
34
44
  PoaDocumentType,
45
+ AddressCollectionConfig,
35
46
  QuestionnaireConfig,
36
47
  QuestionnaireField,
37
48
  QuestionnaireFieldOption,
@@ -56,7 +67,8 @@ export type {
56
67
  WorkflowBusinessDocumentTypeConfig,
57
68
  WorkflowBusinessApplicantConfig,
58
69
  } from './types/business';
59
- export type { KYCSubmission, KYCErrorCode, KYCErrorDetails } from './types/verification';
70
+ export type { KYCSubmission, KYCResult, KYCErrorCode, KYCErrorDetails } from './types/verification';
71
+ export type { BiometricFlowConfig, BiometricCopy, BiometricCopyText } from './config/biometricOptions';
60
72
  export { KYCError } from './types/verification';
61
73
 
62
74
  // ID-type matrix + helpers
@@ -107,6 +119,47 @@ export {
107
119
  export { hasActiveQuestionnaire, questionnaireAnswerKeys } from './config/questionnaire';
108
120
  export { hasEmailVerificationStep, hasPhoneVerificationStep } from './config/contact';
109
121
  export { hasProofOfAddressStep, poaDocumentTypes, poaTypeLabel } from './config/proofOfAddress';
122
+ export { hasAddressCollectionStep } from './config/addressCollection';
123
+ // Presence verification (Address Intelligence Phase 2): the foreground
124
+ // reporter the HOST APP calls on app open, plus the on-device pin store.
125
+ export {
126
+ reportAddressPresence,
127
+ type ReportPresenceOptions,
128
+ type ReportPresenceResult,
129
+ } from './presence/report';
130
+ // The background (OS geofence) tier — the always-on model. register* must run
131
+ // at app-root module scope; enable* asks for the background permission.
132
+ export {
133
+ PRESENCE_GEOFENCE_TASK,
134
+ disableBackgroundPresence,
135
+ enableBackgroundPresence,
136
+ registerBackgroundPresence,
137
+ type EnableBackgroundResult,
138
+ } from './presence/background';
139
+ // The Android foreground-service tier (the OkHi reliability move): a
140
+ // persistent notification keeps the process alive on phones whose battery
141
+ // managers drop geofence transitions. Opt-in; the host words the notification.
142
+ export {
143
+ PRESENCE_LOCATION_TASK,
144
+ disableForegroundService,
145
+ enableForegroundService,
146
+ foregroundServiceRunning,
147
+ type EnableForegroundServiceResult,
148
+ type PresenceNotification,
149
+ } from './presence/foreground-service';
150
+ export {
151
+ openLocationSettings,
152
+ presenceStatus,
153
+ type PresenceStatus,
154
+ type PresenceTier,
155
+ } from './presence/status';
156
+ export { resolvePresenceTier, type PermissionState, type TierInputs } from './presence/tier';
157
+ // savePresencePin is the org-side handoff for STANDALONE address verification:
158
+ // when capture happened on a hosted web link (or the org's own backend already
159
+ // holds the address), the host app hands the SDK the pin so the foreground and
160
+ // background presence tiers can run — the pin otherwise only ever exists where
161
+ // OUR capture step stored it.
162
+ export { clearPresencePin, savePresencePin } from './presence/store';
110
163
 
111
164
  // Country grouping for multi-region flows
112
165
  export { groupCountriesByRegion, regionCountryName, type Region } from './config/regions';
@@ -0,0 +1,171 @@
1
+ import type { AddressParts } from '../services/api';
2
+ import type { LocationFailure } from '../services/location';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // The device's current location, fetched ONCE per verification and shared by
6
+ // every address step.
7
+ //
8
+ // Module-level on purpose: the steps mount and unmount as the person walks the
9
+ // flow, and re-prompting or re-fixing on every screen is exactly the hesitation
10
+ // this exists to remove. Every address step starts the prefetch on mount, so
11
+ // the GPS warms up while the person is still reading the SEARCH screen; by the
12
+ // pin step the fix — and its reverse-geocoded line — are usually already in
13
+ // hand, and the map lands right first time instead of showing a default view
14
+ // and then jumping.
15
+ //
16
+ // A MIRROR of the web SDK's steps/address/current-location.ts, with one
17
+ // deliberate difference: web scopes the cache to a PAGE session, which ends at
18
+ // the next reload. An app session spans many opens of the SDK, so a fix cached
19
+ // an hour ago would drop the pin at yesterday's address. Store CREATION clears
20
+ // it (one store per modal launch), making "this verification" the mobile
21
+ // equivalent of that scope. Clearing it only from the store's reset() is not
22
+ // enough: reset() is a consumer-facing restart, not part of the open path.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface CurrentFix {
26
+ lat: number;
27
+ lng: number;
28
+ accuracy: number | null;
29
+ /** The reverse-geocoded line ("11 Bassey Street, Calabar"), when known. */
30
+ label: string | null;
31
+ parts: AddressParts | null;
32
+ }
33
+
34
+ /** A GPS read: the fix, or WHY there is none (services/location.ts). */
35
+ export type FixOutcome =
36
+ | { fix: { lat: number; lng: number; accuracy: number | null } }
37
+ | { failure: LocationFailure };
38
+
39
+ /**
40
+ * What the cache needs to produce a fix.
41
+ *
42
+ * The GPS reader is INJECTED rather than imported: the store's reset clears
43
+ * this cache, and importing expo-location here would drag a native module into
44
+ * the store's graph for the sake of resetting three variables.
45
+ */
46
+ export interface FixSource {
47
+ takeFix(): Promise<FixOutcome>;
48
+ addressReverse(
49
+ lat: number,
50
+ lng: number,
51
+ ): Promise<{ line: string | null; parts?: AddressParts | null }>;
52
+ }
53
+
54
+ let resolved: CurrentFix | null = null;
55
+ let inflight: Promise<CurrentFix | null> | null = null;
56
+ let failed = false;
57
+ let lastFailure: LocationFailure | null = null;
58
+
59
+ /** Why the last attempt failed, so the message can say something true. */
60
+ export function currentFixFailure(): LocationFailure | null {
61
+ return lastFailure;
62
+ }
63
+
64
+ /**
65
+ * The message for a failed read. A refused permission, a phone that cannot
66
+ * place itself, and a fix that took too long are three different problems
67
+ * with three different remedies, and one line telling everybody to "allow
68
+ * location access" sent people to a permission that was already granted.
69
+ * Placing the pin by hand always works, so every line says so. Mirrors the
70
+ * web SDK's locationFailureMessage and Flutter's.
71
+ */
72
+ export function locationFailureMessage(reason: LocationFailure | null): string {
73
+ switch (reason) {
74
+ case 'denied':
75
+ return "Location access is blocked for this app. Allow it in your phone's Settings, then try again, or place the pin yourself.";
76
+ case 'unavailable':
77
+ return 'Your phone could not work out where it is right now. Check that location is switched on, then try again, or place the pin yourself.';
78
+ case 'timeout':
79
+ return 'Finding your location took too long. Try again, or place the pin yourself.';
80
+ default:
81
+ return 'This device cannot share its location. Place the pin yourself.';
82
+ }
83
+ }
84
+
85
+ /** The fix, when one has already resolved for this verification. */
86
+ export function currentFix(): CurrentFix | null {
87
+ return resolved;
88
+ }
89
+
90
+ /** Is a fix attempt still running? */
91
+ export function locating(): boolean {
92
+ return inflight !== null && resolved === null;
93
+ }
94
+
95
+ /**
96
+ * Start (or JOIN) the one location attempt. Safe to call from every address
97
+ * step's mount: the OS permission prompt fires at most once, and a second
98
+ * caller waits on the first attempt rather than starting its own.
99
+ *
100
+ * Resolves null on a denied or failed read — callers fall back to the manual
101
+ * pin, which is what every address failure degrades to.
102
+ */
103
+ export function prefetchCurrentFix(
104
+ source: FixSource,
105
+ opts?: { retry?: boolean },
106
+ ): Promise<CurrentFix | null> {
107
+ if (resolved) return Promise.resolve(resolved);
108
+ // A failed attempt (denied prompt, no fix) is never retried AUTOMATICALLY.
109
+ // Every step mount calls this, so re-arming on failure left the location row
110
+ // spinning forever and re-fired the OS prompt on each screen. An explicit
111
+ // TAP passes `retry` and gets a fresh attempt, since the person may have
112
+ // granted permission in the meantime.
113
+ if (failed && !inflight && !opts?.retry) return Promise.resolve(null);
114
+ if (!inflight) {
115
+ inflight = (async () => {
116
+ // No builder-preview branch: the preview is a web-only surface, and a
117
+ // canned fix here would be a code path no mobile caller can reach.
118
+ const outcome = await source.takeFix().catch((): FixOutcome => ({ failure: 'unsupported' }));
119
+ if (!('fix' in outcome)) {
120
+ inflight = null;
121
+ failed = true;
122
+ lastFailure = outcome.failure;
123
+ return null;
124
+ }
125
+ const fix = outcome.fix;
126
+ lastFailure = null;
127
+ let label: string | null = null;
128
+ let parts: AddressParts | null = null;
129
+ try {
130
+ const r = await source.addressReverse(fix.lat, fix.lng);
131
+ label = r.line ?? null;
132
+ parts = r.parts ?? null;
133
+ } catch {
134
+ /* the coordinates alone are still a fix */
135
+ }
136
+ resolved = { lat: fix.lat, lng: fix.lng, accuracy: fix.accuracy, label, parts };
137
+ failed = false;
138
+ return resolved;
139
+ })();
140
+ }
141
+ return inflight;
142
+ }
143
+
144
+ /** Forget the fix so the next verification takes its own. Called from the
145
+ * store's reset, beside the step log. */
146
+ export function resetCurrentFix(): void {
147
+ resolved = null;
148
+ inflight = null;
149
+ failed = false;
150
+ lastFailure = null;
151
+ }
152
+
153
+ // ── The pin step's automatic first locate ───────────────────────────────────
154
+ //
155
+ // Most people are verifying from home, so the map should land on them rather
156
+ // than a country-centre default. It fires ONCE: a dismissed or denied prompt
157
+ // must not re-fire every time the person passes back through the pin step, and
158
+ // the explicit control is there for retries.
159
+
160
+ let autoLocateAttempted = false;
161
+
162
+ /** True the FIRST time it is called for a verification, false after. */
163
+ export function claimAutoLocate(): boolean {
164
+ if (autoLocateAttempted) return false;
165
+ autoLocateAttempted = true;
166
+ return true;
167
+ }
168
+
169
+ export function resetAutoLocate(): void {
170
+ autoLocateAttempted = false;
171
+ }
@@ -0,0 +1,119 @@
1
+ import type { AddressCollectionConfig } from '../types/workflow';
2
+ import type { AddressState } from '../store/state';
3
+
4
+ // Per-field modes for the typed details-sheet fields — the CLIENT MIRROR of
5
+ // the server's lib/workflows/address-fields.ts (kyc-core) and of the web
6
+ // SDK's steps/address/address-field-modes.ts. Keep the resolution rule in
7
+ // lockstep: the legacy propertyFields group switch is the default for every
8
+ // typed field, propertyFields 'required' requires the house/flat NUMBER, and
9
+ // fields.<key> overrides per key. The three mirrors share one vector file
10
+ // (kyc-sdk-flutter/test/address_field_modes_vectors.json).
11
+ //
12
+ // Why it matters here: the server 422s a submission whose required fields
13
+ // never arrived. Without this mirror a mobile applicant walked the whole
14
+ // flow, and the refusal landed on the last screen with nothing to act on.
15
+
16
+ export const ADDRESS_FIELD_KEYS = [
17
+ 'propertyName',
18
+ 'propertyNumber',
19
+ 'street',
20
+ 'unit',
21
+ 'neighbourhood',
22
+ 'city',
23
+ 'state',
24
+ 'postcode',
25
+ ] as const;
26
+ export type AddressFieldKey = (typeof ADDRESS_FIELD_KEYS)[number];
27
+ export type AddressFieldMode = 'off' | 'optional' | 'required';
28
+
29
+ export const ADDRESS_FIELD_LABELS: Record<AddressFieldKey, string> = {
30
+ propertyName: 'Building name',
31
+ propertyNumber: 'House or flat number',
32
+ street: 'Street name',
33
+ unit: 'Unit',
34
+ neighbourhood: 'Neighbourhood',
35
+ city: 'City',
36
+ state: 'State',
37
+ postcode: 'Area code',
38
+ };
39
+
40
+ const MODES: readonly string[] = ['off', 'optional', 'required'];
41
+ function isAddressFieldMode(value: unknown): value is AddressFieldMode {
42
+ return typeof value === 'string' && MODES.includes(value);
43
+ }
44
+
45
+ export function addressFieldModes(
46
+ config: AddressCollectionConfig | undefined | null,
47
+ ): Record<AddressFieldKey, AddressFieldMode> {
48
+ const group = config?.propertyFields ?? 'optional';
49
+ const modes = {} as Record<AddressFieldKey, AddressFieldMode>;
50
+ for (const key of ADDRESS_FIELD_KEYS) {
51
+ const fallback: AddressFieldMode =
52
+ group === 'off' ? 'off' : group === 'required' && key === 'propertyNumber' ? 'required' : 'optional';
53
+ // An override outside the vocabulary falls back rather than being taken
54
+ // literally: the server validates writes, the client validates reads.
55
+ const override = config?.fields?.[key];
56
+ modes[key] = isAddressFieldMode(override) ? override : fallback;
57
+ }
58
+ return modes;
59
+ }
60
+
61
+ /** Which map-prefill part fills each field while untouched (the sheet's own
62
+ * `shown` rule). Property name/number and unit have no prefill — the
63
+ * applicant alone can know them. */
64
+ const PART_OF: Partial<Record<AddressFieldKey, 'street' | 'area' | 'city' | 'state' | 'postcode'>> = {
65
+ street: 'street',
66
+ neighbourhood: 'area',
67
+ city: 'city',
68
+ state: 'state',
69
+ postcode: 'postcode',
70
+ };
71
+
72
+ /** The value the sheet DISPLAYS for a field: typed wins (a cleared field
73
+ * stays cleared), else the map's prefill. */
74
+ export function displayedAddressValue(key: AddressFieldKey, address: AddressState): string {
75
+ const typed = (address[key] as string | undefined)?.trim();
76
+ if (typed !== undefined) return typed;
77
+ const part = PART_OF[key];
78
+ return (part ? address.parts?.[part] : null)?.trim() ?? '';
79
+ }
80
+
81
+ /** Required fields whose DISPLAYED value is blank — what holds Continue. */
82
+ export function missingRequiredAddressFields(
83
+ config: AddressCollectionConfig | undefined | null,
84
+ address: AddressState | null | undefined,
85
+ ): AddressFieldKey[] {
86
+ if (!address) return [];
87
+ const modes = addressFieldModes(config);
88
+ return ADDRESS_FIELD_KEYS.filter(
89
+ (key) => modes[key] === 'required' && displayedAddressValue(key, address) === '',
90
+ );
91
+ }
92
+
93
+ /** The nudge under Continue when required fields are still blank. */
94
+ export function missingFieldsNudge(missing: AddressFieldKey[]): string {
95
+ return `This flow needs: ${missing.map((k) => ADDRESS_FIELD_LABELS[k].toLowerCase()).join(', ')}.`;
96
+ }
97
+
98
+ /**
99
+ * The map-prefill values a REQUIRED field rides to the server when the
100
+ * applicant left it untouched: they saw it filled and confirmed it by
101
+ * continuing, so the wire must carry it — the server 422s a required field
102
+ * that never arrives. Typed values are absent here on purpose (the normal
103
+ * assembly already sends them), so spreading this LAST overrides nothing.
104
+ */
105
+ export function requiredPrefillSubmission(
106
+ config: AddressCollectionConfig | undefined | null,
107
+ address: AddressState,
108
+ ): Partial<Record<AddressFieldKey, string>> {
109
+ const modes = addressFieldModes(config);
110
+ const out: Partial<Record<AddressFieldKey, string>> = {};
111
+ for (const key of ADDRESS_FIELD_KEYS) {
112
+ if (modes[key] !== 'required') continue;
113
+ const typed = (address[key] as string | undefined)?.trim();
114
+ if (typed) continue;
115
+ const displayed = displayedAddressValue(key, address);
116
+ if (displayed) out[key] = displayed;
117
+ }
118
+ return out;
119
+ }