@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,79 @@
1
+ // ─── Waiting for the selfie upload from a later step ────────────────────────
2
+ //
3
+ // With the selfie review hidden (the biometric scopes' default), the liveness
4
+ // step hands over the moment the capture ring has closed rather than when the
5
+ // upload lands, so the person sees ONE loading screen from the shutter to the
6
+ // verdict instead of three. The upload keeps running in the background and
7
+ // reports to the store (`selfieUpload`, written by liveness/useSelfieUpload);
8
+ // the submitted step waits on that record before it submits. Pure and
9
+ // injectable, like the result wait: the screen owns nothing but the rendering.
10
+
11
+ export type SelfieUploadStatus = 'idle' | 'uploading' | 'done' | 'failed';
12
+
13
+ export interface SelfieUploadState {
14
+ status: SelfieUploadStatus;
15
+ /** The failure message, set only on `failed`. */
16
+ message: string | null;
17
+ }
18
+
19
+ export const IDLE_SELFIE_UPLOAD: SelfieUploadState = { status: 'idle', message: null };
20
+
21
+ export type SelfieUploadWait = { ok: true } | { ok: false; message: string };
22
+
23
+ export interface SelfieUploadSnapshot {
24
+ selfieUpload: SelfieUploadState;
25
+ /** `mediaIds.selfie`: the durable proof the selfie is on the server. */
26
+ selfieMediaId: string | undefined;
27
+ }
28
+
29
+ export const SELFIE_UPLOAD_WAIT_MS = 90 * 1000;
30
+
31
+ const TIMED_OUT = 'Your selfie could not be sent. Check your connection and try again.';
32
+
33
+ /**
34
+ * Whether the upload has settled, and how. A restored session carries the
35
+ * media id with the status still `idle` (nothing uploaded this visit), which
36
+ * counts as settled; an `idle` record with no media id is an upload that has
37
+ * not started yet, so the caller keeps waiting.
38
+ */
39
+ export function selfieUploadSettled(snapshot: SelfieUploadSnapshot): SelfieUploadWait | null {
40
+ const { status, message } = snapshot.selfieUpload;
41
+ if (status === 'failed') return { ok: false, message: message ?? TIMED_OUT };
42
+ if (status === 'done' || (status === 'idle' && !!snapshot.selfieMediaId)) return { ok: true };
43
+ return null;
44
+ }
45
+
46
+ export interface AwaitSelfieUploadDeps {
47
+ read: () => SelfieUploadSnapshot;
48
+ /** Fires the listener on every store change; returns the unsubscribe. */
49
+ subscribe: (listener: () => void) => () => void;
50
+ timeoutMs?: number;
51
+ setTimer?: (fn: () => void, ms: number) => unknown;
52
+ clearTimer?: (handle: unknown) => void;
53
+ }
54
+
55
+ /** Resolve once the upload has settled, or with a failure at the deadline. */
56
+ export function awaitSelfieUpload(deps: AwaitSelfieUploadDeps): Promise<SelfieUploadWait> {
57
+ const settled = selfieUploadSettled(deps.read());
58
+ if (settled) return Promise.resolve(settled);
59
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
60
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
61
+ return new Promise((resolve) => {
62
+ let done = false;
63
+ const finish = (result: SelfieUploadWait) => {
64
+ if (done) return;
65
+ done = true;
66
+ unsubscribe();
67
+ clearTimer(timer);
68
+ resolve(result);
69
+ };
70
+ const unsubscribe = deps.subscribe(() => {
71
+ const next = selfieUploadSettled(deps.read());
72
+ if (next) finish(next);
73
+ });
74
+ const timer = setTimer(() => finish({ ok: false, message: TIMED_OUT }), deps.timeoutMs ?? SELFIE_UPLOAD_WAIT_MS);
75
+ // The store may have moved between the first read and the subscription.
76
+ const again = selfieUploadSettled(deps.read());
77
+ if (again) finish(again);
78
+ });
79
+ }
@@ -11,6 +11,20 @@
11
11
  export interface StepLogEntry {
12
12
  step: string;
13
13
  at: string;
14
+ /**
15
+ * Which multi-ID check the applicant was on (1-based). Absent on ordinary
16
+ * runs and on the first check.
17
+ *
18
+ * The SERVER cannot tell a slot advance from a back-press: a multi-ID run
19
+ * legitimately returns to the ID picker for its next ID, and by step name
20
+ * that is identical to pressing Back. Only the client knows a slot was
21
+ * committed, so only the client can say.
22
+ */
23
+ slot?: number;
24
+ /** The ID type selected AT THAT MOMENT — a slot's final pick is not what the
25
+ * applicant was doing earlier in it (pick one ID, go back, settle on
26
+ * another, and the earlier step would be named for an ID not yet chosen). */
27
+ idType?: string;
14
28
  }
15
29
 
16
30
  export interface StepLog {
@@ -29,10 +43,17 @@ export function resetStepLog(): void {
29
43
 
30
44
  /** Records a step visit. Consecutive duplicates are collapsed; back-and-forth
31
45
  * navigation is kept — repeat visits are honest journey data. */
32
- export function recordStep(step: string): void {
46
+ export function recordStep(step: string, slot?: number, idType?: string): void {
33
47
  if (entries.length >= MAX_ENTRIES) return;
34
- if (entries[entries.length - 1]?.step === step) return;
35
- entries.push({ step, at: new Date().toISOString() });
48
+ const last = entries[entries.length - 1];
49
+ // A revisit on a NEW check, or on a DIFFERENT ID, is a different visit.
50
+ if (last?.step === step && last.slot === slot && last.idType === idType) return;
51
+ entries.push({
52
+ step,
53
+ at: new Date().toISOString(),
54
+ ...(slot ? { slot } : {}),
55
+ ...(idType ? { idType } : {}),
56
+ });
36
57
  }
37
58
 
38
59
  /** Snapshot attached to the verify submission. Null when nothing was recorded
@@ -0,0 +1,42 @@
1
+ // The Street View entrance frame's maths. A mirror of the web SDK's
2
+ // StreetViewFramer `frameFov` and the Flutter street_view_fov.dart; keep the
3
+ // three in lockstep.
4
+
5
+ export const clamp = (v: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, v));
6
+
7
+ /** The frame the applicant captured: coordinates only, so the server fetches
8
+ * the image with its own key. */
9
+ export interface StreetViewFrame {
10
+ panoId: string;
11
+ heading: number;
12
+ pitch: number;
13
+ fov: number;
14
+ }
15
+
16
+ /**
17
+ * The field of view a centred sub-frame of the viewport actually subtends.
18
+ * The frame is entrance-sized guidance, so the STORED image must be what the
19
+ * frame showed, not the whole panorama — otherwise "fit your gate in the
20
+ * frame" captures a streetscape with the gate somewhere in it. Exact
21
+ * projection maths (a perspective view is a flat plane, so a width fraction
22
+ * maps through tan, not linearly).
23
+ */
24
+ export function frameFov(viewportFovDeg: number, widthFraction: number): number {
25
+ const fraction = clamp(widthFraction, 0.1, 1);
26
+ const half = (viewportFovDeg * Math.PI) / 360;
27
+ return (2 * Math.atan(fraction * Math.tan(half)) * 180) / Math.PI;
28
+ }
29
+
30
+ /** The frame to store for a reported view: the slice the frame subtends when
31
+ * both widths are known, else the whole view; fov 10..120, pitch ±90. */
32
+ export function captureStreetViewFrame(
33
+ pov: { panoId: string; heading: number; pitch: number; viewFov: number },
34
+ frameWidth: number,
35
+ viewWidth: number,
36
+ ): StreetViewFrame {
37
+ const fov =
38
+ frameWidth > 0 && viewWidth > 0
39
+ ? clamp(frameFov(pov.viewFov, frameWidth / viewWidth), 10, 120)
40
+ : clamp(pov.viewFov, 10, 120);
41
+ return { panoId: pov.panoId, heading: pov.heading, pitch: clamp(pov.pitch, -90, 90), fov };
42
+ }
@@ -0,0 +1,29 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { multiIdPlan, type MultiIdPlan } from './multi-id';
4
+ import { useEffectiveCountry, useKyc, useKycConfig } from '../components/runtime';
5
+
6
+ /**
7
+ * The active multi-ID plan for the current state, or null on an ordinary run.
8
+ *
9
+ * One hook so the picker and the position strip cannot disagree about which
10
+ * check the applicant is on or what it may offer — the server validates the
11
+ * pick sequence, so two answers here means submissions it rejects.
12
+ */
13
+ export function useMultiIdPlan(): MultiIdPlan | null {
14
+ const config = useKycConfig();
15
+ const country = useEffectiveCountry();
16
+ const multiIdSlotIndex = useKyc((s) => s.multiIdSlotIndex);
17
+ const multiIdSlots = useKyc((s) => s.multiIdSlots);
18
+ const serverConfig = useKyc((s) => s.serverConfig);
19
+
20
+ return useMemo(
21
+ () =>
22
+ multiIdPlan(
23
+ { ...config, country },
24
+ { multiIdSlotIndex, multiIdSlots },
25
+ serverConfig.status === 'ready' ? serverConfig.idTypes : [],
26
+ ),
27
+ [config, country, multiIdSlotIndex, multiIdSlots, serverConfig],
28
+ );
29
+ }
@@ -0,0 +1,47 @@
1
+ import type React from 'react';
2
+
3
+ import { tryRequire } from '../services/fingerprint-sources';
4
+
5
+ // `react-native-webview` is an OPTIONAL peer. Absent, every surface that
6
+ // would ride it (the framed Google map, the framed Street View) degrades to
7
+ // its dependency-free path (the OSM picker, the entrance photo), and the
8
+ // FLOW MODEL must know that too, or it would offer a step the phone cannot
9
+ // render. Resolved once and cached: a require that failed will fail again.
10
+
11
+ export interface WebViewLike {
12
+ injectJavaScript(script: string): void;
13
+ }
14
+
15
+ export type WebViewComponent = React.ComponentType<{
16
+ ref?: React.Ref<WebViewLike>;
17
+ source: { uri: string };
18
+ style?: object;
19
+ javaScriptEnabled?: boolean;
20
+ /** Android: ask a parent ScrollView not to intercept the WebView's drags. */
21
+ nestedScrollEnabled?: boolean;
22
+ onMessage?: (event: { nativeEvent: { data: string } }) => void;
23
+ onError?: () => void;
24
+ }>;
25
+
26
+ let resolved: WebViewComponent | null | undefined;
27
+
28
+ export function loadWebView(): WebViewComponent | null {
29
+ if (resolved === undefined) {
30
+ const mod = tryRequire<{ WebView?: WebViewComponent; default?: WebViewComponent }>(() =>
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ require('react-native-webview'),
33
+ );
34
+ resolved = mod?.WebView ?? mod?.default ?? null;
35
+ }
36
+ return resolved;
37
+ }
38
+
39
+ /** Whether a WebView can be rendered on this install. */
40
+ export function webViewAvailable(): boolean {
41
+ return loadWebView() != null;
42
+ }
43
+
44
+ /** Test seam: forget the cached answer. */
45
+ export function __resetWebViewAvailability(): void {
46
+ resolved = undefined;
47
+ }
@@ -562,7 +562,12 @@ export function useLiveness(opts: UseLivenessOptions = {}): UseLivenessReturn {
562
562
  setState((s) => ({
563
563
  ...s,
564
564
  phase: 'complete',
565
- instruction: 'Capture complete',
565
+ // No line at all. The ring closing green on the same frame as the
566
+ // shutter IS the completion signal, and the review screen follows within
567
+ // the beat — narrating the upload over it said nothing the user needed
568
+ // and read as a delay. The instruction also drives the voice guidance,
569
+ // so an empty one is silent as well as blank.
570
+ instruction: '',
566
571
  activeChallenge: null,
567
572
  positionGuidance: null,
568
573
  }));
@@ -0,0 +1,65 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ import { isFaceModelReady, primeFaceModel } from './visionCameraFaceDetector';
4
+
5
+ // ─── Face-model readiness gate ────────────────────────────────────────────────
6
+ //
7
+ // Android fetches ML Kit's face model through Play Services rather than bundling
8
+ // it, which keeps ~8 MB per device out of the APK but leaves a window where
9
+ // detection cannot run: first launch before the download lands, or a device with
10
+ // no Google Play Services at all.
11
+ //
12
+ // This has to be answered BEFORE the camera opens. `detectFace` reports through
13
+ // `FaceResult`, where a missing model and an empty frame are both
14
+ // `faceCount: 0` — so gating on the per-frame result would strand the user on
15
+ // "position your face" indefinitely, with the SDK unable to say why. That is the
16
+ // single failure mode this whole gate exists to prevent.
17
+ //
18
+ // iOS is always ready (Apple Vision is a system framework), so this resolves on
19
+ // the first tick there and costs nothing.
20
+
21
+ export type ModelReadyState = 'ready' | 'preparing' | 'unavailable';
22
+
23
+ /** How long to wait for the model before calling it unavailable. */
24
+ const MODEL_WAIT_MS = 20_000;
25
+ /** Gap between readiness polls while the download is in flight. */
26
+ const POLL_INTERVAL_MS = 500;
27
+
28
+ /**
29
+ * Tracks whether on-device face detection can run.
30
+ *
31
+ * Returns `'preparing'` while Play Services fetches the model, `'ready'` once it
32
+ * can run, and `'unavailable'` when it could not be obtained within
33
+ * {@link MODEL_WAIT_MS} — no Play Services, no network, or a declined install.
34
+ *
35
+ * The flow primes the download at open (see `MyazaKYC.tsx`), so by the time the
36
+ * user reaches liveness this is normally already `'ready'` and no waiting screen
37
+ * is ever shown.
38
+ */
39
+ export function useFaceModelReady(): ModelReadyState {
40
+ const [state, setState] = useState<ModelReadyState>(() =>
41
+ isFaceModelReady() ? 'ready' : 'preparing',
42
+ );
43
+ const startedAt = useRef(Date.now());
44
+
45
+ useEffect(() => {
46
+ if (state !== 'preparing') return;
47
+
48
+ // Re-prime rather than assume the open-time call ran: the step can be
49
+ // reached directly in a resumed flow, and prepareModel() is a no-op once
50
+ // the model is present.
51
+ primeFaceModel();
52
+
53
+ const id = setInterval(() => {
54
+ if (isFaceModelReady()) {
55
+ setState('ready');
56
+ } else if (Date.now() - startedAt.current > MODEL_WAIT_MS) {
57
+ setState('unavailable');
58
+ }
59
+ }, POLL_INTERVAL_MS);
60
+
61
+ return () => clearInterval(id);
62
+ }, [state]);
63
+
64
+ return state;
65
+ }
@@ -45,6 +45,41 @@ const boxedDetector: BoxedHybridObject<MyazaFaceDetector> | null = detector
45
45
  ? NitroModules.box(detector)
46
46
  : null;
47
47
 
48
+ /**
49
+ * Start fetching the face model, so it is warm by the time liveness runs.
50
+ *
51
+ * Android fetches ML Kit's models through Play Services rather than bundling
52
+ * them, which keeps ~18.5 MB per device out of the APK but leaves a window on
53
+ * first launch where detection cannot work. Priming at flow start puts that
54
+ * download behind the consent and ID-type screens instead of in front of the
55
+ * camera.
56
+ *
57
+ * Best-effort and non-blocking, exactly like the web SDK's `primeFaceMesh()`:
58
+ * a failure here is not fatal, because {@link isFaceModelReady} is what the
59
+ * liveness step actually gates on. iOS resolves immediately (Apple Vision is a
60
+ * system framework).
61
+ */
62
+ export function primeFaceModel(): void {
63
+ detector?.prepareModel().catch(() => {
64
+ /* best-effort: isFaceModelReady() is the real gate */
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Whether face detection can run right now.
70
+ *
71
+ * Checked BEFORE the camera opens. `detectFace` cannot answer this — a missing
72
+ * model and an empty frame are both `faceCount: 0`, so gating on the per-frame
73
+ * result would strand the user on "position your face" with no explanation.
74
+ *
75
+ * `true` when the native module is absent entirely (Expo Go), because that path
76
+ * already degrades to the existing camera-unavailable screen and should not be
77
+ * reported as a model problem.
78
+ */
79
+ export function isFaceModelReady(): boolean {
80
+ return detector?.isModelReady() ?? true;
81
+ }
82
+
48
83
  /**
49
84
  * Maps the native {@link FaceResult} to a {@link LivenessFaceData}, or `null`
50
85
  * when no face is present (`faceCount === 0`). A worklet — runs on the camera
@@ -0,0 +1,100 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Pure maths for the background (OS geofence) presence tier. A geofence hands
3
+ // us ENTER/EXIT moments; the watch wants per-day aggregates — so a dwell SPAN
4
+ // is folded into the local calendar days it touched, each with its minutes
5
+ // and whether the span overlapped that day's night hours (20:00 to 05:59,
6
+ // the same definition the foreground reporter and the server use).
7
+ //
8
+ // THREE implementations of this fold exist — this file, the Flutter plugin's
9
+ // PresenceFold.kt (Android) and PresenceFold.swift (iOS) — because the
10
+ // killed-app flush must fold natively. All three are pinned to ONE vector
11
+ // file, packages/kyc-sdk-flutter/test/presence_fold_vectors.json, which is
12
+ // why the maths is integer arithmetic on an EXPLICIT UTC offset rather than
13
+ // platform date APIs: identical inputs must fold identically in every
14
+ // language. The offset is captured once per fold (east-positive minutes), a
15
+ // fixed-offset approximation that ignores a DST transition inside one span —
16
+ // at most an hour of dwell attribution shifts, and cross-platform parity is
17
+ // worth more than that edge.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface DayAggregate {
21
+ day: string;
22
+ dwellMinutes: number;
23
+ nightPresent: boolean;
24
+ }
25
+
26
+ /**
27
+ * The longest span one fold will credit. A missed EXIT event (the OS dropped
28
+ * it, the app was killed mid-fence) would otherwise fabricate days of dwell
29
+ * from one stale ENTER timestamp — a fence can fail to close, so the credit
30
+ * from any single span is bounded to one day's worth.
31
+ */
32
+ export const MAX_SPAN_MS = 24 * 60 * 60 * 1000;
33
+
34
+ const DAY_MS = 24 * 60 * 60 * 1000;
35
+ const HOUR_MS = 60 * 60 * 1000;
36
+ const NIGHT_END_H = 6;
37
+ const NIGHT_START_H = 20;
38
+
39
+ /** The device's UTC offset in east-positive minutes (Lagos = +60). JS's
40
+ * getTimezoneOffset() is west-positive, hence the sign flip. */
41
+ export const deviceUtcOffsetMinutes = (atMs: number): number =>
42
+ -new Date(atMs).getTimezoneOffset();
43
+
44
+ /** Civil date for a day index (days since 1970-01-01), as YYYY-MM-DD.
45
+ * Howard Hinnant's civil_from_days — pure integers, no date API, so the
46
+ * Kotlin and Swift mirrors are line-for-line identical. */
47
+ export function civilFromDays(z: number): string {
48
+ const zz = z + 719468;
49
+ const era = Math.floor(zz / 146097);
50
+ const doe = zz - era * 146097;
51
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);
52
+ const y = yoe + era * 400;
53
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
54
+ const mp = Math.floor((5 * doy + 2) / 153);
55
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
56
+ const m = mp < 10 ? mp + 3 : mp - 9;
57
+ const year = m <= 2 ? y + 1 : y;
58
+ const pad = (n: number) => String(n).padStart(2, '0');
59
+ return `${year}-${pad(m)}-${pad(d)}`;
60
+ }
61
+
62
+ /**
63
+ * Fold one dwell span into per-local-day aggregates. `offsetMinutes` is the
64
+ * device's UTC offset (east-positive); it defaults to the device's own so
65
+ * ordinary callers never think about it. An inverted span yields nothing.
66
+ */
67
+ export function foldSpanIntoDays(
68
+ enterMs: number,
69
+ exitMs: number,
70
+ offsetMinutes: number = deviceUtcOffsetMinutes(enterMs),
71
+ ): DayAggregate[] {
72
+ if (!Number.isFinite(enterMs) || !Number.isFinite(exitMs)) return [];
73
+ if (exitMs <= enterMs) return [];
74
+ const offsetMs = offsetMinutes * 60_000;
75
+ const cappedExit = Math.min(exitMs, enterMs + MAX_SPAN_MS);
76
+
77
+ const out: DayAggregate[] = [];
78
+ let cursor = enterMs;
79
+ while (cursor < cappedExit) {
80
+ const shifted = cursor + offsetMs;
81
+ const dayIndex = Math.floor(shifted / DAY_MS);
82
+ // The real-clock moment this local day ends.
83
+ const dayEnd = (dayIndex + 1) * DAY_MS - offsetMs;
84
+ const sliceEnd = Math.min(dayEnd, cappedExit);
85
+ // Night = [00:00, 06:00) plus [20:00, 24:00) of this local day. Exact
86
+ // interval overlap — no sampling — so all three languages agree at the
87
+ // boundaries: a slice ENDING exactly at 20:00 has not touched the night.
88
+ const dayStartShifted = dayIndex * DAY_MS;
89
+ const nightPresent =
90
+ shifted < dayStartShifted + NIGHT_END_H * HOUR_MS ||
91
+ sliceEnd + offsetMs > dayStartShifted + NIGHT_START_H * HOUR_MS;
92
+ out.push({
93
+ day: civilFromDays(dayIndex),
94
+ dwellMinutes: Math.max(1, Math.round((sliceEnd - cursor) / 60_000)),
95
+ nightPresent,
96
+ });
97
+ cursor = sliceEnd;
98
+ }
99
+ return out;
100
+ }
@@ -0,0 +1,82 @@
1
+ // Persistence for the background geofence tier: the open ENTER timestamp,
2
+ // the unflushed per-day queue, and the wire config the headless task needs
3
+ // (it runs with no React context, so apiKey/devUrl are persisted beside the
4
+ // pin — the key is publishable by design). Kept apart from the pin store so
5
+ // each file stays small and the pin's own lifecycle rules stay untangled
6
+ // from flush bookkeeping.
7
+
8
+ import { readJsonFile, writeJsonFile } from './fs';
9
+ import type { WireObservation } from './post';
10
+
11
+ const FILE_NAME = 'myaza-kyc-presence-bg.json';
12
+
13
+ interface BgRecord {
14
+ enterAt?: number | null;
15
+ pending?: WireObservation[];
16
+ config?: { apiKey: string; devUrl?: string };
17
+ serviceUser?: string;
18
+ }
19
+
20
+ /** The foreground-service tier serves ONE user at a time (one notification,
21
+ * one location stream), and its task carries no region identifier the way a
22
+ * geofence event does — so the user it samples for is remembered under this
23
+ * reserved key, which no org user reference can collide with. */
24
+ const SERVICE_USER_KEY = '__service_user__';
25
+
26
+ function readAll(): Record<string, BgRecord> {
27
+ return readJsonFile(FILE_NAME) as Record<string, BgRecord>;
28
+ }
29
+
30
+ function write(userId: string, patch: Partial<BgRecord>): void {
31
+ const all = readAll();
32
+ all[userId] = { ...all[userId], ...patch };
33
+ writeJsonFile(FILE_NAME, all as Record<string, unknown>);
34
+ }
35
+
36
+ export function saveEnterAt(userId: string, at: number | null): void {
37
+ write(userId, { enterAt: at });
38
+ }
39
+
40
+ export function loadEnterAt(userId: string): number | null {
41
+ const at = readAll()[userId]?.enterAt;
42
+ return typeof at === 'number' ? at : null;
43
+ }
44
+
45
+ export function pendingObservations(userId: string): WireObservation[] {
46
+ const pending = readAll()[userId]?.pending;
47
+ return Array.isArray(pending) ? pending : [];
48
+ }
49
+
50
+ export function replacePending(userId: string, next: WireObservation[]): void {
51
+ write(userId, { pending: next });
52
+ }
53
+
54
+ /** Append fresh aggregates through the caller's merge (same-day spans fold). */
55
+ export function queueObservations(
56
+ userId: string,
57
+ fresh: WireObservation[],
58
+ merge: (existing: WireObservation[], fresh: WireObservation[]) => WireObservation[],
59
+ ): void {
60
+ write(userId, { pending: merge(pendingObservations(userId), fresh) });
61
+ }
62
+
63
+ export function saveReporterConfig(userId: string, config: { apiKey: string; devUrl?: string }): void {
64
+ write(userId, { config });
65
+ }
66
+
67
+ export function loadReporterConfig(userId: string): { apiKey: string; devUrl?: string } | null {
68
+ const cfg = readAll()[userId]?.config;
69
+ return cfg && typeof cfg.apiKey === 'string' ? cfg : null;
70
+ }
71
+
72
+ export function saveServiceUser(userId: string | null): void {
73
+ const all = readAll();
74
+ if (userId == null) delete all[SERVICE_USER_KEY];
75
+ else all[SERVICE_USER_KEY] = { serviceUser: userId };
76
+ writeJsonFile(FILE_NAME, all as Record<string, unknown>);
77
+ }
78
+
79
+ export function loadServiceUser(): string | null {
80
+ const id = readAll()[SERVICE_USER_KEY]?.serviceUser;
81
+ return typeof id === 'string' && id.length > 0 ? id : null;
82
+ }