@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,107 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ import { MAX_VIDEO_BYTES } from '../../config/captureSettings';
4
+ import { compressVideo } from '../../services/mediaCompress';
5
+ import { withRetry } from '../../services/retry';
6
+ import { mapToKycError, safeReportError } from '../../services/errors';
7
+ import { useKyc, useKycConfig } from '../../components/runtime';
8
+ import { useToast } from '../../components/toast';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The selfie and its liveness video, uploaded as soon as they exist.
12
+ //
13
+ // Split out of LivenessStep (200-line rule). It is a genuine boundary, not a
14
+ // slice for size: everything here is about getting two files to the server and
15
+ // reporting how that went, and none of it touches the camera or the state
16
+ // machine.
17
+ //
18
+ // The two uploads are deliberately NOT equal. The selfie is the verification —
19
+ // its failure is surfaced, retried and reported. The video is supporting
20
+ // evidence the server re-scores, so a failure there is swallowed: losing it must
21
+ // never block a user who has already done everything asked of them.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface SelfieUpload {
25
+ selfieUri: string | null;
26
+ setSelfieUri: (uri: string | null) => void;
27
+ uploading: boolean;
28
+ /** Which retry attempt is in flight, for the "retrying (2/3)" line. */
29
+ retryInfo: { attempt: number; total: number } | null;
30
+ uploadError: string | null;
31
+ setUploadError: (message: string | null) => void;
32
+ /** The uploaded selfie's media id — Continue is gated on it. */
33
+ selfieIdRef: React.MutableRefObject<string | null>;
34
+ /**
35
+ * The finished liveness-video path, kept so the review screen's "Try Again"
36
+ * can re-upload it. The recorder itself lives in useVideoRecorder.
37
+ */
38
+ videoPathRef: React.MutableRefObject<string | null>;
39
+ uploadSelfieAndVideo: (selfie: string, videoPath: string | null) => Promise<void>;
40
+ }
41
+
42
+ export function useSelfieUpload(): SelfieUpload {
43
+ const toast = useToast();
44
+ const config = useKycConfig();
45
+ const api = useKyc((s) => s.api);
46
+ const setMediaId = useKyc((s) => s.setMediaId);
47
+
48
+ const [selfieUri, setSelfieUri] = useState<string | null>(null);
49
+ const [uploading, setUploading] = useState(false);
50
+ const [retryInfo, setRetryInfo] = useState<{ attempt: number; total: number } | null>(null);
51
+ const [uploadError, setUploadError] = useState<string | null>(null);
52
+ const selfieIdRef = useRef<string | null>(null);
53
+ const videoPathRef = useRef<string | null>(null);
54
+
55
+ const uploadSelfieAndVideo = useCallback(
56
+ async (selfie: string, videoPath: string | null) => {
57
+ setUploading(true);
58
+ setUploadError(null);
59
+ setRetryInfo(null);
60
+ const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
61
+ try {
62
+ const selfieId = await withRetry(
63
+ () => api.upload({ uri: selfie, type: 'image/jpeg' }, 'selfie'),
64
+ { onRetry },
65
+ );
66
+ selfieIdRef.current = selfieId;
67
+ setMediaId('selfie', selfieId);
68
+ // Liveness video is best-effort — a failure here must not block the user.
69
+ if (videoPath) {
70
+ try {
71
+ // Transcode the raw recording down to a small evidence clip first.
72
+ const small = await compressVideo(videoPath);
73
+ const videoId = await withRetry(
74
+ () => api.upload({ uri: small, type: 'video/mp4' }, 'liveness_video', MAX_VIDEO_BYTES),
75
+ { onRetry },
76
+ );
77
+ setMediaId('livenessVideo', videoId);
78
+ } catch {
79
+ /* keep the selfie, drop the video (incl. if it exceeded the 5MB cap) */
80
+ }
81
+ }
82
+ setRetryInfo(null);
83
+ setUploading(false);
84
+ } catch (err) {
85
+ setRetryInfo(null);
86
+ setUploading(false);
87
+ const kycError = mapToKycError(err, 'upload');
88
+ setUploadError(kycError.message);
89
+ toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
90
+ safeReportError(config.onError, kycError);
91
+ }
92
+ },
93
+ [api, setMediaId, config.onError, toast],
94
+ );
95
+
96
+ return {
97
+ selfieUri,
98
+ setSelfieUri,
99
+ uploading,
100
+ retryInfo,
101
+ uploadError,
102
+ setUploadError,
103
+ selfieIdRef,
104
+ videoPathRef,
105
+ uploadSelfieAndVideo,
106
+ };
107
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { MyazaText } from '../../components/Typography';
6
+ import { MyazaButton } from '../../components/MyazaButton';
7
+ import { MrzScanView } from '../MrzScanView';
8
+ import type { MrzScan } from '../../mrz/parse';
9
+
10
+ /**
11
+ * The second chance at the chip's key.
12
+ *
13
+ * Without an MRZ there is nothing to unlock the chip with, and this step used
14
+ * to dead-end there — which made the chip unreachable whenever the document
15
+ * photo happened to read badly. Scanning the strip live costs the user one
16
+ * more aim of the camera and recovers the whole step.
17
+ */
18
+ export function NfcMrzPrompt({
19
+ onScanned,
20
+ onSkip,
21
+ }: {
22
+ onScanned: (scan: MrzScan) => void;
23
+ /** Null hides the escape hatch (the flow set `allowSkip: false`). */
24
+ onSkip: (() => void) | null;
25
+ }): React.ReactElement {
26
+ return (
27
+ <View>
28
+ <MyazaText variant="bodyMedium">
29
+ Point your camera at the photo page of your document. We’ll read the
30
+ printed code that unlocks its chip.
31
+ </MyazaText>
32
+ <View style={{ height: spacing.md }} />
33
+ <MrzScanView onScanned={onScanned} />
34
+ <View style={{ height: spacing.md }} />
35
+ {onSkip ? (
36
+ <MyazaButton label="Continue without the chip" variant="ghost" onPress={onSkip} />
37
+ ) : null}
38
+ </View>
39
+ );
40
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { NfcReadProgress } from './NfcReadProgress';
8
+ import { NfcScannedSummary } from './NfcScannedSummary';
9
+ import { NfcScanIllustration } from './NfcScanIllustration';
10
+ import type { NfcReadStage } from '../../emrtd';
11
+ import type { MrzScan } from '../../mrz/parse';
12
+
13
+ /**
14
+ * The chip step's body: the document illustration, the read narration, and
15
+ * what the MRZ scan found.
16
+ *
17
+ * The narration is the part that matters. iOS gets a system sheet describing
18
+ * the read for free; Android gets nothing, so without this the same read is a
19
+ * silent spinner — and silence is exactly what makes a user lift the document,
20
+ * which aborts it.
21
+ */
22
+ export function NfcReadBody({
23
+ reading,
24
+ stage,
25
+ scan,
26
+ }: {
27
+ reading: boolean;
28
+ stage: NfcReadStage;
29
+ scan: MrzScan;
30
+ }): React.ReactElement {
31
+ const { colors } = useTheme();
32
+ return (
33
+ <>
34
+ <NfcScanIllustration />
35
+ <View style={{ height: spacing.lg }} />
36
+
37
+ {reading ? (
38
+ <>
39
+ <MyazaText variant="bodyMedium" style={{ textAlign: 'center' }}>
40
+ {stage === 'waiting'
41
+ ? 'Hold the top edge of your document flat against the back of your phone and keep still…'
42
+ : 'Chip detected — keep the document still until this finishes.'}
43
+ </MyazaText>
44
+ <View style={{ height: spacing.md }} />
45
+ <NfcReadProgress stage={stage} />
46
+ </>
47
+ ) : (
48
+ <>
49
+ <MyazaText variant="bodySmall" color={colors.textMuted} style={{ textAlign: 'center' }}>
50
+ Hold the top edge of your document flat against the back of your
51
+ phone and keep still…
52
+ </MyazaText>
53
+ {/* Shown BEFORE the read, so the wrong document is obvious now rather
54
+ than after an opaque BAC failure. */}
55
+ <View style={{ height: spacing.md }} />
56
+ <View style={{ alignSelf: 'stretch' }}>
57
+ <NfcScannedSummary scan={scan} />
58
+ </View>
59
+ </>
60
+ )}
61
+ </>
62
+ );
63
+ }
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { Icon } from '../../components/Icon';
8
+ import { NFC_STAGE_ORDER, nfcStageLabel, type NfcReadStage } from '../../emrtd';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // NFC read progress.
12
+ //
13
+ // iOS puts a system sheet over the screen for the whole chip read and narrates
14
+ // it. Android has NO system NFC UI whatsoever, so the same read is otherwise a
15
+ // bare spinner: the user cannot tell whether the chip has even been detected,
16
+ // let alone that three data groups are being pulled off it — and the natural
17
+ // response to that silence is to lift the document, which aborts the read.
18
+ //
19
+ // This draws the narration the platform does not. Same wording as the iOS
20
+ // sheet, so both platforms describe the read identically. Ported from the
21
+ // Flutter SDK's NfcReadProgress.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * The steps worth showing. `waiting` is NOT one of them — it is the state
26
+ * before any step starts, and it gets its own prominent line above.
27
+ */
28
+ const STEPS: NfcReadStage[] = [
29
+ 'authenticating',
30
+ 'readingData',
31
+ 'readingSecurity',
32
+ 'readingPhoto',
33
+ ];
34
+
35
+ export function NfcReadProgress({ stage }: { stage: NfcReadStage }): React.ReactElement {
36
+ const { colors } = useTheme();
37
+
38
+ // Waiting is the state users misread as "nothing is happening", so it says
39
+ // what the phone is doing rather than leaving a silent spinner.
40
+ if (stage === 'waiting') {
41
+ return (
42
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
43
+ <ActivityIndicator size="small" color={colors.primary} />
44
+ <View style={{ width: 8 }} />
45
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
46
+ {nfcStageLabel('waiting')}
47
+ </MyazaText>
48
+ </View>
49
+ );
50
+ }
51
+
52
+ const currentIndex = NFC_STAGE_ORDER.indexOf(stage);
53
+
54
+ return (
55
+ <View style={{ alignSelf: 'stretch' }}>
56
+ {STEPS.map((step) => {
57
+ // `done` completes EVERY step: the photo group is skipped when the
58
+ // security object didn't come back, and a step left spinning after a
59
+ // successful read would read as a failure.
60
+ const complete = stage === 'done' || currentIndex > NFC_STAGE_ORDER.indexOf(step);
61
+ const active = stage === step;
62
+ return (
63
+ <View
64
+ key={step}
65
+ style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }}
66
+ >
67
+ <View style={{ width: 18, alignItems: 'center' }}>
68
+ {complete ? (
69
+ <Icon name="badge-check" size={16} color={colors.success} />
70
+ ) : active ? (
71
+ <ActivityIndicator size="small" color={colors.primary} />
72
+ ) : (
73
+ <View
74
+ style={{
75
+ width: 8,
76
+ height: 8,
77
+ borderRadius: 4,
78
+ backgroundColor: colors.gray300,
79
+ }}
80
+ />
81
+ )}
82
+ </View>
83
+ <View style={{ width: 10 }} />
84
+ <MyazaText
85
+ variant="bodySmall"
86
+ color={complete || active ? colors.textDark : colors.textMuted}
87
+ style={{ fontWeight: active ? '600' : '400', flex: 1 }}
88
+ >
89
+ {nfcStageLabel(step)}
90
+ </MyazaText>
91
+ </View>
92
+ );
93
+ })}
94
+ </View>
95
+ );
96
+ }
@@ -0,0 +1,244 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { AccessibilityInfo, Animated, Easing, View } from 'react-native';
3
+ import Svg, { Circle, Defs, G, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
4
+
5
+ import { useTheme } from '../../components/runtime';
6
+
7
+ /**
8
+ * The NFC scan illustration, ported 1:1 from the web SDK's
9
+ * `NfcScanIllustration` — same 320×240 geometry, same three beats: a document
10
+ * with a CHIP, a phone held over it, a FIELD between them. The web component
11
+ * is the design source of truth; every coordinate here matches it digit for
12
+ * digit so the builder preview (web) and the live mobile screen stay the same
13
+ * picture. Change the web one first, then mirror.
14
+ *
15
+ * Exactly ONE element is `primary`: the field. Everything else is neutral
16
+ * theme tokens, so it inherits an org's palette and works in both modes.
17
+ */
18
+
19
+ /** Where the field originates: on the phone's edge, at antenna height. */
20
+ const COUPLING = { x: 173, y: 88 } as const;
21
+
22
+ /** Arc radii, innermost first — each is one "ripple" of the field. */
23
+ const WAVES = [
24
+ { r: 16, opacity: 0.9, delay: 0 },
25
+ { r: 28, opacity: 0.65, delay: 150 },
26
+ { r: 40, opacity: 0.42, delay: 300 },
27
+ { r: 50, opacity: 0.25, delay: 450 },
28
+ ] as const;
29
+
30
+ /** Half-angle of the fan, in degrees. */
31
+ const SPREAD = 56;
32
+
33
+ /** Web keyframes: 0% .25 → 45% 1 → 100% .25 over 1.8s, staggered. */
34
+ const PERIOD_MS = 1800;
35
+ const RISE_MS = PERIOD_MS * 0.45;
36
+
37
+ function wavePath(r: number): string {
38
+ const rad = (SPREAD * Math.PI) / 180;
39
+ const dx = Math.cos(rad) * r;
40
+ const dy = Math.sin(rad) * r;
41
+ const x = COUPLING.x + dx;
42
+ return `M ${x.toFixed(1)} ${(COUPLING.y - dy).toFixed(1)} A ${r} ${r} 0 0 1 ${x.toFixed(1)} ${(COUPLING.y + dy).toFixed(1)}`;
43
+ }
44
+
45
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
46
+
47
+ /**
48
+ * Web-parity alpha suffixes for the theme's hex colours (the web draws with
49
+ * `token/NN` Tailwind opacities; RN colours are plain hex strings).
50
+ */
51
+ const A10 = '1A';
52
+ const A20 = '33';
53
+ const A25 = '40';
54
+ const A40 = '66';
55
+ const A50 = '80';
56
+ const A70 = 'B3';
57
+
58
+ export function NfcScanIllustration(): React.ReactElement {
59
+ const { colors } = useTheme();
60
+
61
+ // One value per ripple; the element opacity animates while strokeOpacity
62
+ // holds the per-arc base — exactly the web's two-layer opacity.
63
+ const pulses = useRef(WAVES.map(() => new Animated.Value(1))).current;
64
+ const [reduceMotion, setReduceMotion] = useState(false);
65
+
66
+ useEffect(() => {
67
+ let cancelled = false;
68
+ void AccessibilityInfo.isReduceMotionEnabled().then((reduced) => {
69
+ if (!cancelled) setReduceMotion(reduced);
70
+ });
71
+ return () => {
72
+ cancelled = true;
73
+ };
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ if (reduceMotion) {
78
+ // Same behaviour as the web's reduced-motion rule: the animation stops
79
+ // and the arcs stay VISIBLE at full element opacity.
80
+ pulses.forEach((v) => v.setValue(1));
81
+ return undefined;
82
+ }
83
+ const runs = pulses.map((value, i) => {
84
+ value.setValue(0.25);
85
+ const run = Animated.sequence([
86
+ Animated.delay(WAVES[i]!.delay),
87
+ Animated.loop(
88
+ Animated.sequence([
89
+ Animated.timing(value, {
90
+ toValue: 1,
91
+ duration: RISE_MS,
92
+ easing: Easing.inOut(Easing.ease),
93
+ // SVG props can't ride the native driver.
94
+ useNativeDriver: false,
95
+ }),
96
+ Animated.timing(value, {
97
+ toValue: 0.25,
98
+ duration: PERIOD_MS - RISE_MS,
99
+ easing: Easing.inOut(Easing.ease),
100
+ useNativeDriver: false,
101
+ }),
102
+ ]),
103
+ ),
104
+ ]);
105
+ run.start();
106
+ return run;
107
+ });
108
+ return () => runs.forEach((r) => r.stop());
109
+ }, [pulses, reduceMotion]);
110
+
111
+ return (
112
+ // Same footprint as the web: full width up to 384 (`max-w-sm`), 320:240.
113
+ <View style={{ width: '100%', maxWidth: 384, aspectRatio: 320 / 240, alignSelf: 'center' }}>
114
+ <Svg width="100%" height="100%" viewBox="0 0 320 240">
115
+ <Defs>
116
+ {/* The field falls off with distance — the reason the document has
117
+ to be held CLOSE, said as a gradient instead of a caption. */}
118
+ <RadialGradient
119
+ id="kyc-nfc-field"
120
+ cx={COUPLING.x}
121
+ cy={COUPLING.y}
122
+ r={90}
123
+ gradientUnits="userSpaceOnUse"
124
+ >
125
+ <Stop offset="0" stopColor={colors.primary} stopOpacity={0.18} />
126
+ <Stop offset="0.55" stopColor={colors.primary} stopOpacity={0.06} />
127
+ <Stop offset="1" stopColor={colors.primary} stopOpacity={0} />
128
+ </RadialGradient>
129
+ </Defs>
130
+
131
+ {/* ── The document, tucked BEHIND the phone: portrait, chip, MRZ. ── */}
132
+ <G rotation={7} origin="212, 112">
133
+ <Rect
134
+ x={154}
135
+ y={30}
136
+ width={114}
137
+ height={164}
138
+ rx={10}
139
+ fill={`${colors.backgroundSecondary}${A50}`}
140
+ stroke={colors.border}
141
+ strokeWidth={2}
142
+ />
143
+
144
+ {/* Portrait, top-right — an outline glyph floating in its frame. */}
145
+ <Rect
146
+ x={224}
147
+ y={46}
148
+ width={32}
149
+ height={42}
150
+ rx={6}
151
+ fill={`${colors.textDark}${A10}`}
152
+ stroke={colors.border}
153
+ strokeWidth={1.5}
154
+ />
155
+ <G
156
+ stroke={`${colors.textSecondary}${A50}`}
157
+ strokeWidth={2}
158
+ strokeLinecap="round"
159
+ fill="none"
160
+ >
161
+ <Circle cx={240} cy={61} r={5.5} />
162
+ <Path d="M231.5 77.5a8.5 8.5 0 0 1 17 0" />
163
+ </G>
164
+
165
+ {/* The chip — the familiar card contact pad, centred on the card,
166
+ below the fan's lowest reach so the arcs can never touch it. */}
167
+ <G stroke={`${colors.textSecondary}${A70}`} strokeWidth={1.5} fill="none">
168
+ <Rect
169
+ x={200.5}
170
+ y={133}
171
+ width={21}
172
+ height={16}
173
+ rx={3}
174
+ fill={`${colors.textDark}${A10}`}
175
+ />
176
+ <Path d="M200.5 138.3h21M200.5 143.7h21M211 138.3v5.4" />
177
+ </G>
178
+
179
+ {/* The MRZ, low on the card, running edge to edge behind the phone.
180
+ Border tone: background texture, not an icon. */}
181
+ <G stroke={colors.border} strokeWidth={3} strokeLinecap="round" fill="none">
182
+ <Path d="M170 174h84" strokeDasharray="6 4" />
183
+ <Path d="M166 184h88" strokeDasharray="4 5" strokeOpacity={0.7} />
184
+ </G>
185
+ </G>
186
+
187
+ {/* ── The phone, in front, screen toward us — pressed in close. ── */}
188
+ <G x={12}>
189
+ <G rotation={-5} origin="112, 126">
190
+ {/* Volume buttons, on the side away from the field. */}
191
+ <Rect x={57} y={84} width={3} height={14} rx={1.5} fill={`${colors.textDark}${A25}`} />
192
+ <Rect x={57} y={103} width={3} height={14} rx={1.5} fill={`${colors.textDark}${A25}`} />
193
+
194
+ {/* Opaque base + tint: the tint token alone is an alpha fill and
195
+ let the card ghost THROUGH the glass. */}
196
+ <Rect x={60} y={30} width={104} height={192} rx={24} fill={colors.background} />
197
+ <Rect
198
+ x={60}
199
+ y={30}
200
+ width={104}
201
+ height={192}
202
+ rx={24}
203
+ fill={`${colors.backgroundSecondary}${A40}`}
204
+ stroke={`${colors.textDark}${A25}`}
205
+ strokeWidth={2}
206
+ />
207
+ <Rect x={99} y={46} width={26} height={8} rx={4} fill={`${colors.textDark}${A25}`} />
208
+
209
+ {/* Ghost caption bars where the instruction copy sits in the app. */}
210
+ <Rect x={87} y={170} width={50} height={5} rx={2.5} fill={colors.border} />
211
+ <Rect
212
+ x={96}
213
+ y={181}
214
+ width={32}
215
+ height={4}
216
+ rx={2}
217
+ fill={colors.border}
218
+ opacity={0.6}
219
+ />
220
+
221
+ {/* Home indicator — the strongest "this is the front" cue. */}
222
+ <Rect x={97} y={205} width={30} height={3.5} rx={1.75} fill={`${colors.textDark}${A20}`} />
223
+ </G>
224
+ </G>
225
+
226
+ {/* ── The field, over both objects. ── */}
227
+ <Circle cx={COUPLING.x} cy={COUPLING.y} r={90} fill="url(#kyc-nfc-field)" />
228
+ <Circle cx={COUPLING.x} cy={COUPLING.y} r={4.5} fill={colors.primary} />
229
+ {WAVES.map((w, i) => (
230
+ <AnimatedPath
231
+ key={w.r}
232
+ d={wavePath(w.r)}
233
+ stroke={colors.primary}
234
+ strokeWidth={3.25}
235
+ strokeLinecap="round"
236
+ fill="none"
237
+ strokeOpacity={w.opacity}
238
+ opacity={pulses[i]!}
239
+ />
240
+ ))}
241
+ </Svg>
242
+ </View>
243
+ );
244
+ }
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../../config/theme';
5
+ import { useTheme } from '../../components/runtime';
6
+ import { MyazaText } from '../../components/Typography';
7
+ import { Icon } from '../../components/Icon';
8
+ import type { MrzScan } from '../../mrz/parse';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Scanned identity card.
12
+ //
13
+ // What the MRZ scan read, shown back for confirmation — so the wrong document is
14
+ // obvious BEFORE the chip read rather than after an opaque BAC failure.
15
+ //
16
+ // NAME and DOCUMENT NUMBER only, 1:1 with the Flutter SDK's NfcScannedSummary.
17
+ // Those two are what a user actually checks against the printed page; the dates
18
+ // were noise on a screen whose job is "is this the right document, yes or no".
19
+ //
20
+ // Labels are uppercase, 10pt, 0.8 letter-spacing — the same treatment Flutter
21
+ // gives them, so the two SDKs read as one product.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function NfcScannedSummary({ scan }: { scan: MrzScan }): React.ReactElement {
25
+ const { colors } = useTheme();
26
+ const name = [scan.firstName, scan.lastName].filter(Boolean).join(' ');
27
+
28
+ const Label = ({ children }: { children: string }): React.ReactElement => (
29
+ <MyazaText
30
+ variant="bodySmall"
31
+ color={colors.textMuted}
32
+ style={{ fontSize: 10, fontWeight: '600', letterSpacing: 0.8 }}
33
+ >
34
+ {children.toUpperCase()}
35
+ </MyazaText>
36
+ );
37
+
38
+ return (
39
+ <View
40
+ style={{
41
+ padding: spacing.md,
42
+ backgroundColor: colors.primary50,
43
+ borderRadius: radius.md,
44
+ borderWidth: 1,
45
+ borderColor: `${colors.primary}40`,
46
+ flexDirection: 'row',
47
+ alignItems: 'flex-start',
48
+ }}
49
+ >
50
+ {/* Optically aligned with the first line of text rather than the block's
51
+ centre, which drifts as the name wraps. */}
52
+ <View style={{ paddingTop: 2 }}>
53
+ <Icon name="badge-check" size={18} color={colors.primary} />
54
+ </View>
55
+ <View style={{ width: spacing.sm }} />
56
+ <View style={{ flex: 1 }}>
57
+ {name ? (
58
+ <View style={{ marginBottom: spacing.sm }}>
59
+ <Label>Name</Label>
60
+ <MyazaText variant="bodyMedium" color={colors.textDark} style={{ fontWeight: '600' }}>
61
+ {name}
62
+ </MyazaText>
63
+ </View>
64
+ ) : null}
65
+ <View>
66
+ <Label>Document number</Label>
67
+ <MyazaText variant="bodyMedium" color={colors.textDark} style={{ fontWeight: '600' }}>
68
+ {scan.documentNumber}
69
+ </MyazaText>
70
+ </View>
71
+ </View>
72
+ </View>
73
+ );
74
+ }