@myazahq/kyc-sdk-react-native 2.0.2 → 2.1.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 (281) hide show
  1. package/README.md +111 -8
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/AndroidManifest.xml +14 -4
  4. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
  8. package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
  9. package/app.plugin.js +102 -13
  10. package/ios/HybridMyazaEmrtd.swift +447 -0
  11. package/ios/HybridMyazaFaceDetector.swift +104 -2
  12. package/ios/HybridMyazaRectDetector.swift +103 -0
  13. package/ios/HybridMyazaTextRecognizer.swift +111 -0
  14. package/nitrogen/generated/.gitattributes +1 -0
  15. package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
  16. package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
  17. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
  18. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
  19. package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
  20. package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
  21. package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
  22. package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
  23. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
  24. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
  25. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
  26. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
  27. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
  28. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
  30. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
  31. package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
  42. package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
  44. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
  45. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
  46. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
  47. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
  48. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
  49. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
  50. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
  52. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
  54. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
  56. package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
  57. package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
  58. package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
  59. package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  61. package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
  62. package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
  63. package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
  64. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  66. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
  67. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
  68. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
  70. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
  71. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
  72. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
  74. package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
  75. package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
  76. package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
  77. package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
  78. package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
  79. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
  80. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
  81. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
  82. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
  83. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
  84. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
  85. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
  86. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
  87. package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
  88. package/package.json +16 -4
  89. package/src/MyazaKYC.tsx +115 -22
  90. package/src/capture/documentIdentity.ts +153 -0
  91. package/src/capture/documentSignals.ts +177 -0
  92. package/src/capture/documentTextGate.ts +171 -0
  93. package/src/capture/hints.ts +57 -0
  94. package/src/capture/index.ts +24 -0
  95. package/src/capture/rectDetector.ts +84 -0
  96. package/src/capture/rectGate.ts +91 -0
  97. package/src/capture/useAutoCapture.ts +198 -0
  98. package/src/components/CameraViewfinder.tsx +226 -70
  99. package/src/components/ContactCodeEntry.tsx +139 -0
  100. package/src/components/CountryField.tsx +87 -0
  101. package/src/components/CountryFlag.tsx +15 -15
  102. package/src/components/DialCodePicker.tsx +186 -0
  103. package/src/components/DocumentCropper.tsx +1 -1
  104. package/src/components/DocumentReview.tsx +142 -0
  105. package/src/components/DocumentReviewSide.tsx +135 -0
  106. package/src/components/DocumentReviewZoom.tsx +119 -0
  107. package/src/components/ExpiryCountdown.tsx +52 -0
  108. package/src/components/FlashOverlay.tsx +156 -0
  109. package/src/components/Icon.tsx +67 -1
  110. package/src/components/KycFlow.tsx +66 -94
  111. package/src/components/KycSheet.tsx +148 -18
  112. package/src/components/MediaSourceSheet.tsx +163 -0
  113. package/src/components/MyazaButton.tsx +14 -2
  114. package/src/components/MyazaDateField.tsx +240 -0
  115. package/src/components/MyazaInput.tsx +73 -26
  116. package/src/components/MyazaSelect.tsx +283 -0
  117. package/src/components/MyazaWordmark.tsx +33 -0
  118. package/src/components/OptionRow.tsx +132 -0
  119. package/src/components/PhoneNumberInput.tsx +149 -0
  120. package/src/components/PoweredBy.tsx +89 -0
  121. package/src/components/ReadyPrimer.tsx +149 -0
  122. package/src/components/StepView.tsx +91 -0
  123. package/src/components/Typography.tsx +18 -3
  124. package/src/components/VerifiedNotice.tsx +38 -0
  125. package/src/components/WhatsAppIcon.tsx +29 -0
  126. package/src/components/WorkflowGate.tsx +128 -0
  127. package/src/components/brand-font.ts +113 -0
  128. package/src/components/flashHoleGeometry.ts +35 -0
  129. package/src/components/flow/FatalConfigError.tsx +56 -0
  130. package/src/components/fonts.ts +2 -19
  131. package/src/components/readyPrimerContent.ts +52 -0
  132. package/src/components/runtime.tsx +38 -43
  133. package/src/components/stepHeaderMeta.tsx +87 -0
  134. package/src/components/theme-provider.tsx +89 -0
  135. package/src/components/useWorkflowMount.ts +116 -0
  136. package/src/components/viewfinder/CardGuide.tsx +60 -0
  137. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  138. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  139. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  140. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  141. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  142. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  143. package/src/config/brand.ts +97 -0
  144. package/src/config/business.ts +179 -0
  145. package/src/config/businessSteps.ts +105 -0
  146. package/src/config/contact.ts +159 -0
  147. package/src/config/countryNames.g.ts +254 -0
  148. package/src/config/currencyFlags.ts +144 -0
  149. package/src/config/font-resolve.ts +78 -0
  150. package/src/config/idTypes.ts +96 -1
  151. package/src/config/keyPeople.ts +152 -0
  152. package/src/config/phone.ts +39 -0
  153. package/src/config/proofOfAddress.ts +75 -0
  154. package/src/config/questionnaire.ts +153 -0
  155. package/src/config/regions.ts +118 -0
  156. package/src/config/registrationHint.ts +87 -0
  157. package/src/config/stepOrder.ts +118 -0
  158. package/src/config/theme.ts +38 -2
  159. package/src/config/workflowMerge.ts +124 -0
  160. package/src/emrtd/bac.ts +128 -0
  161. package/src/emrtd/bytes.ts +127 -0
  162. package/src/emrtd/crypto.ts +163 -0
  163. package/src/emrtd/der.ts +192 -0
  164. package/src/emrtd/dg2.ts +111 -0
  165. package/src/emrtd/files.ts +215 -0
  166. package/src/emrtd/index.ts +30 -0
  167. package/src/emrtd/mrzKey.ts +54 -0
  168. package/src/emrtd/native.ts +79 -0
  169. package/src/emrtd/optionalRead.ts +42 -0
  170. package/src/emrtd/read.ts +224 -0
  171. package/src/emrtd/retry.ts +98 -0
  172. package/src/emrtd/secureMessaging.ts +178 -0
  173. package/src/emrtd/session.ts +200 -0
  174. package/src/emrtd/stages.ts +122 -0
  175. package/src/index.ts +110 -0
  176. package/src/lib/calendar.ts +71 -0
  177. package/src/lib/use-keyboard-inset.ts +34 -0
  178. package/src/liveness/challengeManager.ts +27 -1
  179. package/src/liveness/faceContinuity.ts +153 -0
  180. package/src/liveness/flashDetector.ts +145 -0
  181. package/src/liveness/flashReadyGate.ts +106 -0
  182. package/src/liveness/flashRunner.ts +118 -0
  183. package/src/liveness/integritySignals.ts +60 -0
  184. package/src/liveness/livenessState.ts +139 -0
  185. package/src/liveness/speech.ts +19 -1
  186. package/src/liveness/types.ts +48 -1
  187. package/src/liveness/useFlashPhase.ts +78 -0
  188. package/src/liveness/useLiveness.ts +260 -75
  189. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  190. package/src/mrz/extract.ts +101 -0
  191. package/src/mrz/parse.ts +178 -0
  192. package/src/mrz/textRecognizer.ts +149 -0
  193. package/src/screens/ApplicantRoleStep.tsx +331 -0
  194. package/src/screens/BusinessDetailsStep.tsx +212 -0
  195. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  196. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  197. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  198. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  199. package/src/screens/CompanyInfoFields.tsx +83 -0
  200. package/src/screens/ConsentStep.tsx +68 -68
  201. package/src/screens/ContactChannelChoice.tsx +133 -0
  202. package/src/screens/ContactCodeStep.tsx +130 -0
  203. package/src/screens/ContactDestinationField.tsx +70 -0
  204. package/src/screens/ContactFooterNote.tsx +34 -0
  205. package/src/screens/ContactVerificationStep.tsx +199 -0
  206. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  207. package/src/screens/CountrySelectStep.tsx +210 -0
  208. package/src/screens/DocumentCaptureStep.tsx +156 -132
  209. package/src/screens/IdInputStep.tsx +13 -5
  210. package/src/screens/IdTypeStep.tsx +74 -33
  211. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  212. package/src/screens/LivenessStep.tsx +155 -247
  213. package/src/screens/MrzScanView.tsx +173 -0
  214. package/src/screens/NfcStep.tsx +290 -0
  215. package/src/screens/ProofOfAddressStep.tsx +238 -0
  216. package/src/screens/QuestionnaireField.tsx +219 -0
  217. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  218. package/src/screens/QuestionnaireStep.tsx +76 -0
  219. package/src/screens/SubmittedStep.tsx +5 -0
  220. package/src/screens/consent/model.ts +109 -0
  221. package/src/screens/document/CameraPhase.tsx +150 -0
  222. package/src/screens/document/RequiredPill.tsx +60 -0
  223. package/src/screens/liveness/LightingBanner.tsx +50 -0
  224. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  225. package/src/screens/liveness/ProgressDots.tsx +67 -0
  226. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  227. package/src/screens/liveness/constants.ts +31 -0
  228. package/src/screens/liveness/index.ts +8 -0
  229. package/src/screens/liveness/resolveGuidance.ts +27 -0
  230. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  231. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  232. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  233. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  234. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  235. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  236. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  237. package/src/screens/nfc/availability.ts +73 -0
  238. package/src/screens/nfc/readErrorMessage.ts +93 -0
  239. package/src/screens/nfc/useChipPortrait.ts +58 -0
  240. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  241. package/src/screens/useFlashHole.ts +105 -0
  242. package/src/screens/useFlashSequence.ts +96 -0
  243. package/src/screens/usePoaAttach.ts +121 -0
  244. package/src/services/api-types.ts +164 -0
  245. package/src/services/api-verify-types.ts +99 -0
  246. package/src/services/api.ts +72 -102
  247. package/src/services/cardCrop.ts +25 -10
  248. package/src/services/contactErrors.ts +43 -0
  249. package/src/services/deviceMetadata.ts +1 -1
  250. package/src/services/documentPicker.ts +20 -0
  251. package/src/services/fingerprint-sources.ts +129 -0
  252. package/src/services/fingerprint.ts +101 -0
  253. package/src/services/haptics.ts +38 -0
  254. package/src/services/mediaCompress.ts +7 -2
  255. package/src/services/resolveUrl.ts +2 -2
  256. package/src/services/workflowGate.ts +138 -0
  257. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  258. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  259. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  260. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  261. package/src/store/derive.ts +225 -0
  262. package/src/store/kycStore.ts +250 -149
  263. package/src/store/state.ts +253 -0
  264. package/src/store/submit.ts +101 -0
  265. package/src/store/submitApplicant.ts +81 -0
  266. package/src/types/appearance.ts +113 -0
  267. package/src/types/business.ts +131 -0
  268. package/src/types/config.ts +150 -124
  269. package/src/types/id-types.ts +57 -0
  270. package/src/types/verification.ts +5 -0
  271. package/src/types/workflow.ts +141 -0
  272. package/src/utils/amountFormat.ts +73 -0
  273. package/src/utils/tokens.ts +4 -2
  274. package/src/__tests__/cardCrop.test.ts +0 -39
  275. package/src/__tests__/deviceMetadata.test.ts +0 -34
  276. package/src/__tests__/errors.test.ts +0 -34
  277. package/src/__tests__/flow.test.ts +0 -61
  278. package/src/__tests__/gestureDetector.test.ts +0 -37
  279. package/src/__tests__/liveness.test.ts +0 -112
  280. package/src/__tests__/resolveUrl.test.ts +0 -64
  281. package/src/__tests__/validators.test.ts +0 -38
@@ -0,0 +1,156 @@
1
+ import React, { useState } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { useStore } from 'zustand';
4
+ import Svg, { Path, Rect } from 'react-native-svg';
5
+
6
+ import { holePath, type FlashHole } from './flashHoleGeometry';
7
+ import { useKycStore } from './runtime';
8
+ import { MyazaText } from './Typography';
9
+ import { spacing } from '../config/theme';
10
+
11
+ export type { FlashHole } from './flashHoleGeometry';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // The flash overlay: a full-screen colour with a hole over the camera preview.
15
+ //
16
+ // The screen IS the light source. How much of it is lit is not cosmetic — it
17
+ // decides how much light reaches the face, and therefore whether the reflection
18
+ // is measurable at all. An overlay confined to the step's own body only lit the
19
+ // inset sheet, leaving the rest of the display dark; that is a fraction of the
20
+ // available light and it made flashes read as inconclusive.
21
+ //
22
+ // So it paints at the SHEET ROOT — over the header, the body and the footer,
23
+ // but inside the same tree as the preview.
24
+ //
25
+ // Not in a modal of its own, which is the obvious way to reach the physical
26
+ // screen edges and does not work: iOS scales a presenting sheet back when a
27
+ // modal appears over it, and RN's `measureInWindow` reports Yoga LAYOUT
28
+ // coordinates, which never reflect that UIKit transform. The cutout came back
29
+ // as exactly the preview's layout size (300pt) while the preview was rendering
30
+ // at ~232pt — off by the transform, and no re-measuring can converge on a value
31
+ // the measurement API cannot observe.
32
+ //
33
+ // Painting in the same tree makes the hole and the preview scale TOGETHER, so
34
+ // they agree by construction rather than by measurement. The cost is the strip
35
+ // above an iOS page sheet, which stays unlit — a bounded, predictable loss,
36
+ // where a misaligned hole is an unbounded one.
37
+ //
38
+ // A hole is not required for correctness: light lost through it is constant
39
+ // across the neutral and lit windows, so it cancels in the comparison either
40
+ // way. If measurement fails the overlay paints solid — the user loses sight of
41
+ // their own framing for about a second, which is worth far less than failing
42
+ // the check outright. Flutter takes the same fallback.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Rendered once at the sheet root.
47
+ *
48
+ * It subscribes to the paint slot ITSELF rather than taking props, so a colour
49
+ * change re-renders this leaf alone. Threading the colour down through the sheet
50
+ * would re-render the step on every flash — and the camera with it, mid-capture.
51
+ */
52
+ export function FlashOverlay(): React.ReactElement | null {
53
+ const store = useKycStore();
54
+ const paint = useStore(store, (s) => s.flashPaint);
55
+ const [size, setSize] = useState<{ width: number; height: number } | null>(null);
56
+
57
+ if (!paint) return null;
58
+ const { color, hole } = paint;
59
+
60
+ return (
61
+ <View
62
+ testID="kyc.liveness.flash"
63
+ pointerEvents="none"
64
+ onLayout={(e) => {
65
+ const { width, height } = e.nativeEvent.layout;
66
+ if (size?.width !== width || size?.height !== height) setSize({ width, height });
67
+ }}
68
+ // zIndex covers iOS sibling stacking; elevation covers Android, where a
69
+ // sibling WITH elevation (the glass header) otherwise paints above any
70
+ // absolute overlay that has none.
71
+ style={[StyleSheet.absoluteFill, { zIndex: 100, elevation: 100 }]}
72
+ >
73
+ {color && size ? (
74
+ <Panels color={color} hole={hole} width={size.width} height={size.height} />
75
+ ) : null}
76
+ </View>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * The colour, with a CIRCULAR bite taken out of it.
82
+ *
83
+ * Four rectangles around the preview were cheaper, but they can only ever
84
+ * describe a rectangle — against a circular preview that leaves the corners
85
+ * painted over the user's face, which is precisely the part they need to see to
86
+ * stay framed.
87
+ *
88
+ * A single even-odd path is the actual shape: the screen rectangle and the
89
+ * preview circle as two subpaths, so the circle is subtracted rather than drawn.
90
+ * One node, no mask, no per-frame compositing.
91
+ */
92
+ function Panels({
93
+ color,
94
+ hole,
95
+ width,
96
+ height,
97
+ }: {
98
+ color: string;
99
+ hole: FlashHole | null;
100
+ width: number;
101
+ height: number;
102
+ }): React.ReactElement {
103
+ return (
104
+ <>
105
+ <Svg width={width} height={height} style={{ position: 'absolute', top: 0, left: 0 }}>
106
+ {hole ? (
107
+ <Path
108
+ // Screen rect, then the circle as two arcs. `evenodd` is what makes
109
+ // the second subpath a hole instead of a second filled shape.
110
+ d={holePath(width, height, hole)}
111
+ fill={color}
112
+ fillRule="evenodd"
113
+ />
114
+ ) : (
115
+ // Measurement failed — paint solid. The face is still lit, which is
116
+ // what the check needs; only the framing preview is lost.
117
+ <Rect x={0} y={0} width={width} height={height} fill={color} />
118
+ )}
119
+ </Svg>
120
+
121
+ <HoldStill hole={hole} width={width} height={height} />
122
+ </>
123
+ );
124
+ }
125
+
126
+ /**
127
+ * The one instruction shown during a flash.
128
+ *
129
+ * Anchored near the BOTTOM of the sheet, deliberately not hugging the cutout:
130
+ * text directly above and below the preview crowded the only part of the screen
131
+ * the user is meant to be looking at, and read as two competing messages.
132
+ *
133
+ * Clamped to sit below the cutout as well, so on a short screen — where the
134
+ * preview sits low — it moves down rather than landing on the face.
135
+ */
136
+ function HoldStill({
137
+ hole,
138
+ width,
139
+ height,
140
+ }: {
141
+ hole: FlashHole | null;
142
+ width: number;
143
+ height: number;
144
+ }): React.ReactElement {
145
+ const belowHole = hole ? hole.y + hole.size + spacing.xl : 0;
146
+ const top = Math.min(Math.max(belowHole, height * 0.78), height - spacing.xl * 2);
147
+ return (
148
+ <View style={{ position: 'absolute', top, left: 0, width, alignItems: 'center' }}>
149
+ {/* Dark text: every palette colour is high-luminance, so white would be
150
+ unreadable on cyan and green. */}
151
+ <MyazaText variant="heading3" color="rgba(0,0,0,0.8)" style={{ textAlign: 'center' }}>
152
+ Hold still
153
+ </MyazaText>
154
+ </View>
155
+ );
156
+ }
@@ -3,8 +3,13 @@ import Ionicons from '@expo/vector-icons/Ionicons';
3
3
  import {
4
4
  BadgeCheck,
5
5
  BookUser,
6
+ Building2,
7
+ Calendar,
6
8
  Camera,
7
9
  CameraOff,
10
+ ChevronDown,
11
+ ChevronLeft,
12
+ ChevronRight,
8
13
  Car,
9
14
  Check,
10
15
  CircleAlert,
@@ -13,19 +18,35 @@ import {
13
18
  Fingerprint,
14
19
  FileText,
15
20
  IdCard,
21
+ Image as ImageIcon,
16
22
  Landmark,
17
23
  Lightbulb,
18
24
  Globe,
25
+ Mail,
26
+ Maximize2,
19
27
  Lock,
28
+ MessageCircle,
29
+ MessageSquare,
20
30
  Moon,
21
31
  MoveLeft,
32
+ Nfc,
33
+ Smartphone,
22
34
  RefreshCw,
23
35
  ScanFace,
24
36
  ScanLine,
37
+ Search,
38
+ Zap,
39
+ ZapOff,
25
40
  ShieldCheck,
26
41
  Sun,
42
+ Timer,
27
43
  Upload,
28
44
  UserRound,
45
+ UserRoundPlus,
46
+ Link2,
47
+ Copy,
48
+ Share2,
49
+ UsersRound,
29
50
  VideoOff,
30
51
  Vote,
31
52
  X,
@@ -47,12 +68,29 @@ export type IconName =
47
68
  | 'lock'
48
69
  | 'alert'
49
70
  | 'refresh'
71
+ | 'maximize'
72
+ | 'calendar'
73
+ | 'image'
74
+ | 'chevron-left'
75
+ | 'chevron-right'
76
+ | 'chevron-down'
50
77
  // consent hero + process steps
51
78
  | 'shield'
52
79
  | 'badge-check'
80
+ | 'building-2'
53
81
  | 'user'
82
+ | 'user-plus'
83
+ | 'users'
54
84
  | 'scan-line'
55
85
  | 'scan-face'
86
+ | 'nfc'
87
+ | 'search'
88
+ | 'zap'
89
+ | 'link'
90
+ | 'copy'
91
+ | 'share'
92
+ | 'zap-off'
93
+ | 'timer'
56
94
  // id-type glyphs
57
95
  | 'fingerprint'
58
96
  | 'credit-card'
@@ -68,7 +106,13 @@ export type IconName =
68
106
  | 'camera-off'
69
107
  | 'video-off'
70
108
  | 'upload'
71
- | 'lightbulb';
109
+ | 'x'
110
+ | 'lightbulb'
111
+ // contact verification (OTP channels + step footer)
112
+ | 'mail'
113
+ | 'smartphone'
114
+ | 'message-square'
115
+ | 'message-circle';
72
116
 
73
117
  // Maps each name to its Lucide component. Names mirror the Flutter SDK exactly:
74
118
  // shieldCheck · badgeCheck · userRound · scanLine · scanFace · fingerprint ·
@@ -83,11 +127,28 @@ const ICONS: Record<IconName, LucideIcon> = {
83
127
  lock: Lock,
84
128
  alert: CircleAlert,
85
129
  refresh: RefreshCw,
130
+ maximize: Maximize2,
131
+ calendar: Calendar,
132
+ image: ImageIcon,
133
+ 'chevron-left': ChevronLeft,
134
+ 'chevron-right': ChevronRight,
135
+ 'chevron-down': ChevronDown,
86
136
  shield: ShieldCheck,
87
137
  'badge-check': BadgeCheck,
138
+ 'building-2': Building2,
88
139
  user: UserRound,
140
+ 'user-plus': UserRoundPlus,
141
+ link: Link2,
142
+ copy: Copy,
143
+ share: Share2,
144
+ users: UsersRound,
89
145
  'scan-line': ScanLine,
90
146
  'scan-face': ScanFace,
147
+ nfc: Nfc,
148
+ search: Search,
149
+ zap: Zap,
150
+ 'zap-off': ZapOff,
151
+ timer: Timer,
91
152
  fingerprint: Fingerprint,
92
153
  'credit-card': CreditCard,
93
154
  globe: Globe,
@@ -102,7 +163,12 @@ const ICONS: Record<IconName, LucideIcon> = {
102
163
  'camera-off': CameraOff,
103
164
  'video-off': VideoOff,
104
165
  upload: Upload,
166
+ x: X,
105
167
  lightbulb: Lightbulb,
168
+ mail: Mail,
169
+ smartphone: Smartphone,
170
+ 'message-square': MessageSquare,
171
+ 'message-circle': MessageCircle,
106
172
  };
107
173
 
108
174
  export interface IconProps {
@@ -2,32 +2,27 @@ import React, { useEffect, useMemo, useRef } from 'react';
2
2
  import { View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
- import { ID_TYPES, requiresDocumentCapture } from '../config/idTypes';
5
+ import { buildStepOrder } from '../config/stepOrder';
6
+ import { stepOrderOptions } from '../store/kycStore';
6
7
  import { KYCError } from '../types/verification';
7
8
  import { safeReportError } from '../services/errors';
8
9
  import type { KYCStep, SupportedCountry } from '../types/config';
9
- import { useKyc, useKycConfig, useKycStore, useTheme } from './runtime';
10
+ import { useEffectiveCountry, useKyc, useKycConfig, useKycStore, useTheme } from './runtime';
11
+ import { COUNTRY_SEARCH_THRESHOLD } from '../screens/CountrySelectStep';
10
12
  import { KycSheet } from './KycSheet';
13
+ import { FatalConfigError } from './flow/FatalConfigError';
11
14
  import { ToastProvider } from './toast';
12
15
  import { MyazaText } from './Typography';
13
16
  import { MyazaButton } from './MyazaButton';
14
17
  import { Icon } from './Icon';
15
- import { ConsentStep } from '../screens/ConsentStep';
16
- import { IdTypeStep } from '../screens/IdTypeStep';
17
- import { IdInputStep } from '../screens/IdInputStep';
18
- import { DocumentCaptureStep, documentCaptureMeta } from '../screens/DocumentCaptureStep';
19
- import { LivenessStep } from '../screens/LivenessStep';
20
- import { SubmittedStep } from '../screens/SubmittedStep';
18
+ import { StepView } from './StepView';
19
+ import { stepHeaderMeta } from './stepHeaderMeta';
20
+ import { countrySelectOptions } from '../store/derive';
21
21
 
22
22
  // The step router — 1:1 with the Flutter SDK's _KycFlowWidget. Computes per-step
23
23
  // header title/description, the 4-step indicator info, back/country, fetches the
24
24
  // server config on mount, and gates the flow on fatal auth failures.
25
25
 
26
- function labelFor(country: SupportedCountry, idType: string | null): string {
27
- if (!idType) return 'Document';
28
- return Object.values(ID_TYPES).flat().find((t) => t.key === idType)?.label ?? 'Document';
29
- }
30
-
31
26
  /**
32
27
  * Result of a back request handled inside the flow:
33
28
  * 'navigated' — went back a step; the caller should NOT close.
@@ -47,11 +42,14 @@ export function KycFlow({
47
42
  }): React.ReactElement {
48
43
  const { colors } = useTheme();
49
44
  const config = useKycConfig();
45
+ const country = useEffectiveCountry();
50
46
  const store = useKycStore();
51
47
  const currentStep = useKyc((s) => s.currentStep);
52
48
  const selectedIdType = useKyc((s) => s.selectedIdType);
53
49
  const documentCapturePhase = useKyc((s) => s.documentCapturePhase);
50
+ const contactChallenge = useKyc((s) => s.contactChallenge);
54
51
  const serverConfig = useKyc((s) => s.serverConfig);
52
+ const immersiveCapture = useKyc((s) => s.immersiveCapture);
55
53
 
56
54
  const startedRef = useRef(false);
57
55
  const reportedRef = useRef(false);
@@ -60,7 +58,11 @@ export function KycFlow({
60
58
  if (!startedRef.current) {
61
59
  startedRef.current = true;
62
60
  config.onStart?.();
63
- void store.getState().loadServerConfig();
61
+ // A resolved workflow already delivered the allowlist + branding, so
62
+ // there is nothing to fetch.
63
+ if (store.getState().serverConfig.status !== 'ready') {
64
+ void store.getState().loadServerConfig();
65
+ }
64
66
  }
65
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
66
68
  }, []);
@@ -75,41 +77,42 @@ export function KycFlow({
75
77
  }, [isFatal, serverConfig.statusCode, serverConfig.message, config.onError]);
76
78
 
77
79
  // ── Per-step header meta ──────────────────────────────────────────────────
78
- const meta = useMemo(() => {
79
- const label = labelFor(config.country, selectedIdType);
80
- switch (currentStep) {
81
- case 'consent':
82
- return { title: '', description: null as string | null };
83
- case 'id-type':
84
- return { title: 'Select ID Type', description: "Choose the type of identification document you'd like to use." };
85
- case 'id-input':
86
- return { title: 'Enter Your Details', description: `Provide your ${label} for verification.` };
87
- case 'document-capture':
88
- // Phase-aware title/description live in the header (synced from the
89
- // capture screen via `documentCapturePhase`) — mirrors Flutter.
90
- return documentCaptureMeta(documentCapturePhase, label);
91
- case 'liveness':
92
- return { title: 'Face Verification', description: 'Follow the on-screen instructions' };
93
- case 'submitted':
94
- default:
95
- return { title: '', description: null };
96
- }
97
- }, [currentStep, config.country, selectedIdType, documentCapturePhase]);
80
+ const meta = useMemo(
81
+ () =>
82
+ stepHeaderMeta(currentStep, {
83
+ config,
84
+ country,
85
+ selectedIdType,
86
+ documentCapturePhase,
87
+ contactChallenge,
88
+ }),
89
+ [currentStep, config, country, selectedIdType, documentCapturePhase, contactChallenge],
90
+ );
98
91
 
99
- // ── Step indicator info (consent, id-type, capture|input, liveness?) ───────
92
+ // ── Step indicator info ───────────────────────────────────────────────────
93
+ // Read from the SAME ordered list navigation uses, so the dots can never
94
+ // describe a different flow than the one the user is walking. This used to be
95
+ // its own hand-written copy of the sequence, which meant a step added to the
96
+ // flow silently didn't count towards progress.
97
+ //
98
+ // 'submitted' is excluded on purpose — it is the terminal screen, not a step
99
+ // to make progress towards, and the indicator is hidden there.
100
+ //
101
+ // The derivation is memoised rather than done inside the selector: zustand
102
+ // compares snapshots by identity, so a selector that mints a fresh object on
103
+ // every call re-renders forever.
104
+ const state = useKyc((s) => s);
100
105
  const stepInfo = useMemo(() => {
101
- if (currentStep === 'submitted') return null;
102
- const hasCapture =
103
- config.enableDocumentCapture && (selectedIdType ? requiresDocumentCapture(selectedIdType) : true);
104
- const hasLiveness = config.enableLiveness !== false && config.enableSelfie !== false;
105
- const steps: KYCStep[] = ['consent', 'id-type', hasCapture ? 'document-capture' : 'id-input'];
106
- if (hasLiveness) steps.push('liveness');
107
- const idx = steps.indexOf(currentStep);
106
+ if (state.currentStep === 'submitted') return null;
107
+ const steps = buildStepOrder(stepOrderOptions(state)).filter((s) => s !== 'submitted');
108
+ const idx = steps.indexOf(state.currentStep);
108
109
  if (idx < 0) return null;
109
110
  return { progress: (idx + 1) / steps.length, stepCount: steps.length };
110
- }, [currentStep, config.enableDocumentCapture, config.enableLiveness, config.enableSelfie, selectedIdType]);
111
+ }, [state]);
111
112
 
112
- const country = currentStep === 'id-type' || currentStep === 'id-input' ? config.country : null;
113
+ // The flag beside the title names the country whose IDs are on screen.
114
+ const headerCountry =
115
+ currentStep === 'id-type' || currentStep === 'id-input' ? country : null;
113
116
  const onBack = currentStep === 'consent' || currentStep === 'submitted' ? null : () => store.getState().previousStep();
114
117
 
115
118
  // Android hardware back arrives via the <Modal>'s onRequestClose. Expose a
@@ -136,36 +139,7 @@ export function KycFlow({
136
139
  }, [backRef, canGoBack, disableClose, store]);
137
140
 
138
141
  if (isFatal) {
139
- return (
140
- <KycSheet title="" onClose={onClose} hideBrand>
141
- <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
142
- <View
143
- style={{
144
- width: 80,
145
- height: 80,
146
- borderRadius: radius.full,
147
- backgroundColor: `${colors.error}1A`,
148
- alignItems: 'center',
149
- justifyContent: 'center',
150
- }}
151
- >
152
- <Icon name="alert" size={40} color={colors.error} />
153
- </View>
154
- <View style={{ height: spacing.lg }} />
155
- <MyazaText variant="heading2" style={{ textAlign: 'center' }}>
156
- Unable to start verification
157
- </MyazaText>
158
- <View style={{ height: spacing.xs }} />
159
- <MyazaText variant="bodyMedium" color={colors.textSecondary} style={{ textAlign: 'center' }}>
160
- {serverConfig.message ?? 'Please check your API key and try again.'}
161
- </MyazaText>
162
- <View style={{ height: spacing.lg }} />
163
- <View style={{ width: '100%' }}>
164
- <MyazaButton label="Close" onPress={onClose} />
165
- </View>
166
- </View>
167
- </KycSheet>
168
- );
142
+ return <FatalConfigError message={serverConfig.message} onClose={onClose} />;
169
143
  }
170
144
 
171
145
  return (
@@ -175,31 +149,29 @@ export function KycFlow({
175
149
  description={meta.description}
176
150
  progress={stepInfo?.progress ?? null}
177
151
  stepCount={stepInfo?.stepCount ?? null}
178
- country={country}
152
+ country={headerCountry}
179
153
  onBack={onBack}
180
154
  onClose={onClose}
155
+ // The searchable country picker pins its search box above a list that
156
+ // can run to ~240 rows. Only the multi-country variant needs it: a
157
+ // short flat list is happier in the normal scroll body.
158
+ fillsViewport={
159
+ currentStep === 'country-select' &&
160
+ countrySelectOptions({ config, serverConfig }).length > COUNTRY_SEARCH_THRESHOLD
161
+ }
162
+ // A live camera asks for the whole screen: the sheet's header, padding
163
+ // and scroll view are exactly what force a small viewfinder on a short
164
+ // phone, and a camera you have to scroll to is a broken camera.
165
+ //
166
+ // Passed as a PROP rather than rendered as its own tree. The earlier
167
+ // shape early-returned a different element tree, which changed the
168
+ // step's parent MID-STEP — React unmounted and remounted it, wiping the
169
+ // acknowledged primer and bouncing straight back to it. Scoped to the
170
+ // step as well as the flag so the next step cannot inherit it.
171
+ immersive={immersiveCapture && currentStep === 'document-capture'}
181
172
  >
182
173
  <StepView step={currentStep} onClose={onClose} />
183
174
  </KycSheet>
184
175
  </ToastProvider>
185
176
  );
186
177
  }
187
-
188
- function StepView({ step, onClose }: { step: KYCStep; onClose: () => void }): React.ReactElement {
189
- switch (step) {
190
- case 'consent':
191
- return <ConsentStep />;
192
- case 'id-type':
193
- return <IdTypeStep />;
194
- case 'id-input':
195
- return <IdInputStep />;
196
- case 'document-capture':
197
- return <DocumentCaptureStep />;
198
- case 'liveness':
199
- return <LivenessStep />;
200
- case 'submitted':
201
- return <SubmittedStep onClose={onClose} />;
202
- default:
203
- return <ConsentStep />;
204
- }
205
- }