@myazahq/kyc-sdk-react-native 2.0.1 → 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 (283) 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/CameraPermissionView.tsx +39 -3
  99. package/src/components/CameraViewfinder.tsx +226 -70
  100. package/src/components/ContactCodeEntry.tsx +139 -0
  101. package/src/components/CountryField.tsx +87 -0
  102. package/src/components/CountryFlag.tsx +15 -15
  103. package/src/components/DialCodePicker.tsx +186 -0
  104. package/src/components/DocumentCropper.tsx +1 -1
  105. package/src/components/DocumentReview.tsx +142 -0
  106. package/src/components/DocumentReviewSide.tsx +135 -0
  107. package/src/components/DocumentReviewZoom.tsx +119 -0
  108. package/src/components/ExpiryCountdown.tsx +52 -0
  109. package/src/components/FlashOverlay.tsx +156 -0
  110. package/src/components/Icon.tsx +67 -1
  111. package/src/components/KycFlow.tsx +66 -94
  112. package/src/components/KycSheet.tsx +148 -18
  113. package/src/components/MediaSourceSheet.tsx +163 -0
  114. package/src/components/MyazaButton.tsx +14 -2
  115. package/src/components/MyazaDateField.tsx +240 -0
  116. package/src/components/MyazaInput.tsx +73 -26
  117. package/src/components/MyazaSelect.tsx +283 -0
  118. package/src/components/MyazaWordmark.tsx +33 -0
  119. package/src/components/OptionRow.tsx +132 -0
  120. package/src/components/PhoneNumberInput.tsx +149 -0
  121. package/src/components/PoweredBy.tsx +89 -0
  122. package/src/components/ReadyPrimer.tsx +149 -0
  123. package/src/components/StepView.tsx +91 -0
  124. package/src/components/Typography.tsx +18 -3
  125. package/src/components/VerifiedNotice.tsx +38 -0
  126. package/src/components/WhatsAppIcon.tsx +29 -0
  127. package/src/components/WorkflowGate.tsx +128 -0
  128. package/src/components/brand-font.ts +113 -0
  129. package/src/components/flashHoleGeometry.ts +35 -0
  130. package/src/components/flow/FatalConfigError.tsx +56 -0
  131. package/src/components/fonts.ts +2 -19
  132. package/src/components/readyPrimerContent.ts +52 -0
  133. package/src/components/runtime.tsx +38 -43
  134. package/src/components/stepHeaderMeta.tsx +87 -0
  135. package/src/components/theme-provider.tsx +89 -0
  136. package/src/components/useWorkflowMount.ts +116 -0
  137. package/src/components/viewfinder/CardGuide.tsx +60 -0
  138. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  139. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  140. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  141. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  142. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  143. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  144. package/src/config/brand.ts +97 -0
  145. package/src/config/business.ts +179 -0
  146. package/src/config/businessSteps.ts +105 -0
  147. package/src/config/contact.ts +159 -0
  148. package/src/config/countryNames.g.ts +254 -0
  149. package/src/config/currencyFlags.ts +144 -0
  150. package/src/config/font-resolve.ts +78 -0
  151. package/src/config/idTypes.ts +98 -1
  152. package/src/config/keyPeople.ts +152 -0
  153. package/src/config/phone.ts +39 -0
  154. package/src/config/proofOfAddress.ts +75 -0
  155. package/src/config/questionnaire.ts +153 -0
  156. package/src/config/regions.ts +118 -0
  157. package/src/config/registrationHint.ts +87 -0
  158. package/src/config/stepOrder.ts +118 -0
  159. package/src/config/theme.ts +38 -2
  160. package/src/config/workflowMerge.ts +124 -0
  161. package/src/emrtd/bac.ts +128 -0
  162. package/src/emrtd/bytes.ts +127 -0
  163. package/src/emrtd/crypto.ts +163 -0
  164. package/src/emrtd/der.ts +192 -0
  165. package/src/emrtd/dg2.ts +111 -0
  166. package/src/emrtd/files.ts +215 -0
  167. package/src/emrtd/index.ts +30 -0
  168. package/src/emrtd/mrzKey.ts +54 -0
  169. package/src/emrtd/native.ts +79 -0
  170. package/src/emrtd/optionalRead.ts +42 -0
  171. package/src/emrtd/read.ts +224 -0
  172. package/src/emrtd/retry.ts +98 -0
  173. package/src/emrtd/secureMessaging.ts +178 -0
  174. package/src/emrtd/session.ts +200 -0
  175. package/src/emrtd/stages.ts +122 -0
  176. package/src/index.ts +110 -0
  177. package/src/lib/calendar.ts +71 -0
  178. package/src/lib/use-keyboard-inset.ts +34 -0
  179. package/src/liveness/challengeManager.ts +27 -1
  180. package/src/liveness/faceContinuity.ts +153 -0
  181. package/src/liveness/flashDetector.ts +145 -0
  182. package/src/liveness/flashReadyGate.ts +106 -0
  183. package/src/liveness/flashRunner.ts +118 -0
  184. package/src/liveness/integritySignals.ts +60 -0
  185. package/src/liveness/livenessState.ts +139 -0
  186. package/src/liveness/speech.ts +19 -1
  187. package/src/liveness/types.ts +48 -1
  188. package/src/liveness/useFlashPhase.ts +78 -0
  189. package/src/liveness/useLiveness.ts +260 -75
  190. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  191. package/src/mrz/extract.ts +101 -0
  192. package/src/mrz/parse.ts +178 -0
  193. package/src/mrz/textRecognizer.ts +149 -0
  194. package/src/screens/ApplicantRoleStep.tsx +331 -0
  195. package/src/screens/BusinessDetailsStep.tsx +212 -0
  196. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  197. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  198. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  199. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  200. package/src/screens/CompanyInfoFields.tsx +83 -0
  201. package/src/screens/ConsentStep.tsx +68 -68
  202. package/src/screens/ContactChannelChoice.tsx +133 -0
  203. package/src/screens/ContactCodeStep.tsx +130 -0
  204. package/src/screens/ContactDestinationField.tsx +70 -0
  205. package/src/screens/ContactFooterNote.tsx +34 -0
  206. package/src/screens/ContactVerificationStep.tsx +199 -0
  207. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  208. package/src/screens/CountrySelectStep.tsx +210 -0
  209. package/src/screens/DocumentCaptureStep.tsx +190 -145
  210. package/src/screens/IdInputStep.tsx +19 -9
  211. package/src/screens/IdTypeStep.tsx +74 -33
  212. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  213. package/src/screens/LivenessStep.tsx +181 -267
  214. package/src/screens/MrzScanView.tsx +173 -0
  215. package/src/screens/NfcStep.tsx +290 -0
  216. package/src/screens/ProofOfAddressStep.tsx +238 -0
  217. package/src/screens/QuestionnaireField.tsx +219 -0
  218. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  219. package/src/screens/QuestionnaireStep.tsx +76 -0
  220. package/src/screens/SubmittedStep.tsx +5 -0
  221. package/src/screens/consent/model.ts +109 -0
  222. package/src/screens/document/CameraPhase.tsx +150 -0
  223. package/src/screens/document/RequiredPill.tsx +60 -0
  224. package/src/screens/liveness/LightingBanner.tsx +50 -0
  225. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  226. package/src/screens/liveness/ProgressDots.tsx +67 -0
  227. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  228. package/src/screens/liveness/constants.ts +31 -0
  229. package/src/screens/liveness/index.ts +8 -0
  230. package/src/screens/liveness/resolveGuidance.ts +27 -0
  231. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  232. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  233. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  234. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  235. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  236. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  237. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  238. package/src/screens/nfc/availability.ts +73 -0
  239. package/src/screens/nfc/readErrorMessage.ts +93 -0
  240. package/src/screens/nfc/useChipPortrait.ts +58 -0
  241. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  242. package/src/screens/useFlashHole.ts +105 -0
  243. package/src/screens/useFlashSequence.ts +96 -0
  244. package/src/screens/usePoaAttach.ts +121 -0
  245. package/src/services/api-types.ts +164 -0
  246. package/src/services/api-verify-types.ts +99 -0
  247. package/src/services/api.ts +72 -102
  248. package/src/services/cardCrop.ts +25 -10
  249. package/src/services/contactErrors.ts +43 -0
  250. package/src/services/deviceMetadata.ts +1 -1
  251. package/src/services/documentPicker.ts +20 -0
  252. package/src/services/fingerprint-sources.ts +129 -0
  253. package/src/services/fingerprint.ts +101 -0
  254. package/src/services/haptics.ts +38 -0
  255. package/src/services/mediaCompress.ts +7 -2
  256. package/src/services/resolveUrl.ts +3 -2
  257. package/src/services/validators.ts +3 -0
  258. package/src/services/workflowGate.ts +138 -0
  259. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  260. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  261. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  262. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  263. package/src/store/derive.ts +225 -0
  264. package/src/store/kycStore.ts +250 -149
  265. package/src/store/state.ts +253 -0
  266. package/src/store/submit.ts +101 -0
  267. package/src/store/submitApplicant.ts +81 -0
  268. package/src/types/appearance.ts +113 -0
  269. package/src/types/business.ts +131 -0
  270. package/src/types/config.ts +150 -119
  271. package/src/types/id-types.ts +57 -0
  272. package/src/types/verification.ts +5 -0
  273. package/src/types/workflow.ts +141 -0
  274. package/src/utils/amountFormat.ts +73 -0
  275. package/src/utils/tokens.ts +4 -2
  276. package/src/__tests__/cardCrop.test.ts +0 -39
  277. package/src/__tests__/deviceMetadata.test.ts +0 -34
  278. package/src/__tests__/errors.test.ts +0 -34
  279. package/src/__tests__/flow.test.ts +0 -61
  280. package/src/__tests__/gestureDetector.test.ts +0 -37
  281. package/src/__tests__/liveness.test.ts +0 -112
  282. package/src/__tests__/resolveUrl.test.ts +0 -64
  283. package/src/__tests__/validators.test.ts +0 -38
@@ -16,7 +16,8 @@ import { withRetry } from '../services/retry';
16
16
  import { mapToKycError, safeReportError } from '../services/errors';
17
17
  import { compressSelfieImage, compressVideo } from '../services/mediaCompress';
18
18
  import { KYCError } from '../types/verification';
19
- import { useKyc, useKycConfig, useTheme } from '../components/runtime';
19
+ import { useStore } from 'zustand';
20
+ import { useKyc, useKycConfig, useKycStore, useTheme } from '../components/runtime';
20
21
  import { MyazaText } from '../components/Typography';
21
22
  import { MyazaButton } from '../components/MyazaButton';
22
23
  import { MyazaPulseLoader } from '../components/MyazaPulseLoader';
@@ -29,9 +30,27 @@ import {
29
30
  } from '../config/captureSettings';
30
31
  import { Icon } from '../components/Icon';
31
32
  import { useToast } from '../components/toast';
32
- import { CameraPermissionView, CameraUnavailableView } from '../components/CameraPermissionView';
33
+ import { CameraPermissionView, CameraUnavailableView, CameraPermissionPrimingView } from '../components/CameraPermissionView';
34
+ import { ReadyPrimer } from '../components/ReadyPrimer';
35
+ import { READY_LIVENESS } from '../components/readyPrimerContent';
33
36
  import { LivenessAvatar } from './LivenessAvatar';
34
37
  import { detectFaceOnFrame } from '../liveness/visionCameraFaceDetector';
38
+ import { buildLivenessIntegrity } from '../liveness/integritySignals';
39
+ import { DEFAULT_LIVENESS_CONFIG } from '../liveness/types';
40
+ import { useFlashSequence } from './useFlashSequence';
41
+ import { useFlashHole } from './useFlashHole';
42
+ import {
43
+ FRESH_FACE_TIMEOUT_MS,
44
+ INSTRUCTION_HEIGHT,
45
+ LightingBanner,
46
+ ProgressDots,
47
+ SelfiePreview,
48
+ StyleAbsFill,
49
+ resolveGuidance,
50
+ useSelfieUpload,
51
+ LivenessComplete,
52
+ LivenessFailed,
53
+ } from './liveness';
35
54
  import {
36
55
  lightingGuidanceText,
37
56
  positionGuidanceText,
@@ -63,6 +82,7 @@ export function LivenessStep(): React.ReactElement {
63
82
  const config = useKycConfig();
64
83
  const api = useKyc((s) => s.api);
65
84
  const setMediaId = useKyc((s) => s.setMediaId);
85
+ const setCaptureIntegrity = useKyc((s) => s.setCaptureIntegrity);
66
86
  const nextStep = useKyc((s) => s.nextStep);
67
87
 
68
88
  const device = useCameraDevice('front');
@@ -85,10 +105,14 @@ export function LivenessStep(): React.ReactElement {
85
105
  });
86
106
  const videoRecorder = useVideoRecorder(videoOutput, !!device);
87
107
 
88
- const [perm, setPerm] = useState<'checking' | 'granted' | 'denied'>(
89
- hasPermission ? 'granted' : 'checking',
108
+ // 'priming' shows the "Allow camera access" screen BEFORE the OS prompt
109
+ // (Stripe-style); the prompt only fires (→ 'requesting') once the user taps
110
+ // "Grant access". `perm` is driven by the async requestPermission result, not
111
+ // synchronously from `hasPermission` — otherwise the window while the OS prompt
112
+ // is open (hasPermission still false) would read as "denied" and fire onError.
113
+ const [perm, setPerm] = useState<'priming' | 'requesting' | 'granted' | 'denied'>(
114
+ hasPermission ? 'granted' : 'priming',
90
115
  );
91
- const askedRef = useRef(false);
92
116
  const permReportedRef = useRef(false);
93
117
 
94
118
  // Camera-availability grace (a simulator has no front camera).
@@ -98,20 +122,28 @@ export function LivenessStep(): React.ReactElement {
98
122
  return () => clearTimeout(t);
99
123
  }, []);
100
124
  const cameraUnavailable = cameraGrace && !device;
125
+ // Gate the whole camera path on the user acknowledging the primer.
126
+ const [ready, setReady] = useState(false);
127
+ const showPrimer = perm === 'priming' && !!device;
101
128
  const permissionDenied = perm === 'denied' && !!device;
102
129
 
130
+ // Reflect an externally-granted permission.
103
131
  useEffect(() => {
104
- if (hasPermission) {
105
- setPerm('granted');
106
- return;
107
- }
108
- if (askedRef.current) return;
109
- askedRef.current = true;
132
+ if (hasPermission) setPerm('granted');
133
+ }, [hasPermission]);
134
+
135
+ // Fire the real OS prompt only after the user taps "Grant access" (or retry).
136
+ useEffect(() => {
137
+ if (perm !== 'requesting') return;
138
+ let cancelled = false;
110
139
  void (async () => {
111
140
  const granted = await requestPermission();
112
- setPerm(granted ? 'granted' : 'denied');
141
+ if (!cancelled) setPerm(granted ? 'granted' : 'denied');
113
142
  })();
114
- }, [hasPermission, requestPermission]);
143
+ return () => {
144
+ cancelled = true;
145
+ };
146
+ }, [perm, requestPermission]);
115
147
 
116
148
  useEffect(() => {
117
149
  if (permissionDenied && !permReportedRef.current) {
@@ -124,56 +156,19 @@ export function LivenessStep(): React.ReactElement {
124
156
  if (!permissionDenied) permReportedRef.current = false;
125
157
  }, [permissionDenied, config.onError]);
126
158
 
127
- // ── Selfie capture + eager upload state (mirrors Flutter _handleComplete) ───
128
- const [selfieUri, setSelfieUri] = useState<string | null>(null);
129
- const [uploading, setUploading] = useState(false);
130
- const [retryInfo, setRetryInfo] = useState<{ attempt: number; total: number } | null>(null);
131
- const [uploadError, setUploadError] = useState<string | null>(null);
132
- const selfieIdRef = useRef<string | null>(null);
133
- // Holds the finished liveness-video path (from the recorder) so the review-screen
134
- // "Try Again" can re-upload it. The recorder itself lives in useVideoRecorder.
135
- const videoPathRef = useRef<string | null>(null);
136
-
137
- const uploadSelfieAndVideo = useCallback(
138
- async (selfie: string, videoPath: string | null) => {
139
- setUploading(true);
140
- setUploadError(null);
141
- setRetryInfo(null);
142
- const onRetry = (attempt: number, total: number) => setRetryInfo({ attempt, total });
143
- try {
144
- const selfieId = await withRetry(
145
- () => api.upload({ uri: selfie, type: 'image/jpeg' }, 'selfie'),
146
- { onRetry },
147
- );
148
- selfieIdRef.current = selfieId;
149
- setMediaId('selfie', selfieId);
150
- // Liveness video is best-effort — a failure here must not block the user.
151
- if (videoPath) {
152
- try {
153
- // Transcode the raw recording down to a small evidence clip first.
154
- const small = await compressVideo(videoPath);
155
- const videoId = await withRetry(
156
- () => api.upload({ uri: small, type: 'video/mp4' }, 'liveness_video', MAX_VIDEO_BYTES),
157
- { onRetry },
158
- );
159
- setMediaId('livenessVideo', videoId);
160
- } catch {
161
- /* keep the selfie, drop the video (incl. if it exceeded the 5MB cap) */
162
- }
163
- }
164
- setRetryInfo(null);
165
- setUploading(false);
166
- } catch (err) {
167
- setRetryInfo(null);
168
- setUploading(false);
169
- const kycError = mapToKycError(err, 'upload');
170
- setUploadError(kycError.message);
171
- toast.show({ variant: 'error', title: 'Upload failed', message: kycError.message });
172
- safeReportError(config.onError, kycError);
173
- }
174
- },
175
- [api, setMediaId, config.onError, toast],
176
- );
159
+ // Selfie + liveness-video upload (see ./liveness/useSelfieUpload).
160
+ const upload = useSelfieUpload();
161
+ const {
162
+ selfieUri,
163
+ setSelfieUri,
164
+ uploading,
165
+ retryInfo,
166
+ uploadError,
167
+ setUploadError,
168
+ selfieIdRef,
169
+ videoPathRef,
170
+ uploadSelfieAndVideo,
171
+ } = upload;
177
172
 
178
173
  // ── Liveness state machine ─────────────────────────────────────────────────
179
174
  // `handleCapture` is created after `liveness` (it calls liveness.markComplete),
@@ -181,7 +176,16 @@ export function LivenessStep(): React.ReactElement {
181
176
  const captureRef = useRef<() => void>(() => {});
182
177
  const liveness = useLiveness({
183
178
  voiceGuidance: config.voiceGuidance,
179
+ // Silent until the primer is dismissed and the camera is actually up. The
180
+ // machine is constructed behind the primer, so without this it talks to a
181
+ // user who has not started yet.
182
+ announce: ready && !showPrimer,
184
183
  onReadyToCapture: () => captureRef.current(),
184
+ config: {
185
+ ...DEFAULT_LIVENESS_CONFIG,
186
+ mode: config.livenessMode ?? 'gestures',
187
+ flashSequenceLength: config.flashSequenceLength,
188
+ },
185
189
  });
186
190
 
187
191
  const handleCapture = useCallback(async () => {
@@ -196,10 +200,46 @@ export function LivenessStep(): React.ReactElement {
196
200
  await new Promise((r) => setTimeout(r, SELFIE_SETTLE_MS));
197
201
  // v5: capture the selfie via the photo output (no camera ref). filePath is
198
202
  // a plain fs path — prepend file:// for the uploader/compressor.
203
+ // A different face appeared while the machine was in `capturing`. The
204
+ // verdict was recorded rather than shown (nothing may re-lay out mid
205
+ // capture), so it is acted on here — BEFORE the shutter, so no still of a
206
+ // stranger is ever taken, let alone uploaded.
207
+ // Live read. `liveness.integrityBroken` is a snapshot from the render
208
+ // this callback was created in — i.e. from BEFORE the flash — so a face
209
+ // swapped or added during the sequence was recorded and then never seen
210
+ // here.
211
+ if (liveness.isCompromised()) {
212
+ liveness.reportIntegrityFailure();
213
+ return;
214
+ }
215
+ // Nobody in frame? Do not photograph an empty room.
216
+ //
217
+ // The flash can leave the face briefly undetectable, so this waits for a
218
+ // FRESH detection now that the overlay is gone rather than trusting a
219
+ // timestamp from before it started — a stale stamp from just before the
220
+ // user walked away passes any recency check, which is precisely how a
221
+ // blank selfie got captured and submitted as a passing liveness result.
222
+ //
223
+ // Losing the face is not a failure, just an abandoned attempt: drop back
224
+ // to positioning with the camera still running, so the user re-frames and
225
+ // the gate fires again with no teardown and no permission re-prompt.
226
+ if (!(await liveness.awaitFreshFace(FRESH_FACE_TIMEOUT_MS))) {
227
+ liveness.reset();
228
+ return;
229
+ }
199
230
  const file = await photoOutput.capturePhotoToFile({ flashMode: 'off' }, {});
200
231
  const raw = `file://${file.filePath}`;
201
232
  const compressed = await compressSelfieImage(raw).catch(() => raw);
202
233
  setSelfieUri(compressed);
234
+ // Recorded at capture, not at submit: by then the liveness hook is gone
235
+ // and its flash result with it.
236
+ setCaptureIntegrity({
237
+ liveness: buildLivenessIntegrity(
238
+ config.livenessMode ?? 'gestures',
239
+ liveness.faceGlitches,
240
+ liveness.flashResult,
241
+ ),
242
+ });
203
243
  liveness.markComplete();
204
244
  // Stop the liveness video (best-effort) and start the eager upload. The
205
245
  // recorder resolves the finished file path (or null if nothing recorded);
@@ -210,7 +250,7 @@ export function LivenessStep(): React.ReactElement {
210
250
  } catch {
211
251
  liveness.reset();
212
252
  }
213
- }, [device, liveness, uploadSelfieAndVideo, photoOutput, videoRecorder]);
253
+ }, [device, liveness, uploadSelfieAndVideo, photoOutput, videoRecorder, setCaptureIntegrity, config.livenessMode]);
214
254
 
215
255
  useEffect(() => {
216
256
  captureRef.current = () => void handleCapture();
@@ -232,20 +272,61 @@ export function LivenessStep(): React.ReactElement {
232
272
  // with `runOnJS(...)` INSIDE the worklet (the canonical react-native-worklets
233
273
  // pattern) rather than pre-wrapped — pre-wrapping produced a "non-worklet
234
274
  // function" error when called from the frame-processor worklet.
275
+ // The most recent face-region RGB, for the flash sequence. A ref rather than
276
+ // state: this updates every frame, and re-rendering the camera preview at
277
+ // frame rate would be both wasteful and — during a flash — actively harmful,
278
+ // since the overlay's cutout is measured once.
279
+ const faceRgbRef = useRef<readonly [number, number, number] | null>(null);
280
+
235
281
  const handleFace = useCallback((data: LivenessFaceData | null) => {
236
282
  const l = livenessRef.current;
237
283
  if (!data) {
284
+ faceRgbRef.current = null;
238
285
  l.onNoFace();
239
286
  return;
240
287
  }
288
+ faceRgbRef.current = data.faceRgb ?? null;
241
289
  // Low-light gate (thresholds mirror Flutter's _BrightnessSampler: <62 dark,
242
290
  // >200 bright). Feeds the lighting warning + speech and blocks challenges so
243
291
  // gestures don't misbehave in poor light.
244
292
  l.setLighting(data.brightness < 62 ? 'dark' : data.brightness > 200 ? 'bright' : null);
245
293
  if (data.faceCount > 0) l.onFace(data);
246
- else l.onNoFace();
294
+ else {
295
+ faceRgbRef.current = null;
296
+ l.onNoFace();
297
+ }
247
298
  }, []);
248
299
 
300
+
301
+ // Hoisted above this component's several early returns — it is a hook, and
302
+ // calling it further down (next to the circle it measures, where it reads
303
+ // better) would make it conditional on which branch rendered.
304
+ const { ref: flashHoleRef, hole: flashHole } = useFlashHole(liveness.phase === 'flash');
305
+
306
+ const { flashColor } = useFlashSequence({
307
+ active: liveness.shouldFlash,
308
+ // Stop painting the moment a second face appears or the face is swapped:
309
+ // finishing the sequence would produce a reflection measurement for a face
310
+ // that is no longer the subject.
311
+ shouldContinue: () => !livenessRef.current.isCompromised(),
312
+ sequenceLength: config.flashSequenceLength,
313
+ readFaceRgb: () => faceRgbRef.current,
314
+ onComplete: (result) => livenessRef.current.completeFlash(result),
315
+ });
316
+
317
+ // Published to the sheet root rather than drawn in this step: an overlay here
318
+ // lights only the padded body, and the screen IS the light source. The sheet
319
+ // root is still the SAME tree as the preview, so the cutout and the preview
320
+ // scale together under any UIKit transform.
321
+ const flashStore = useKycStore();
322
+ const setFlashPaint = useStore(flashStore, (st) => st.setFlashPaint);
323
+ const isFlashing = liveness.phase === 'flash';
324
+ useEffect(() => {
325
+ setFlashPaint(isFlashing ? { color: flashColor, hole: flashHole } : null);
326
+ }, [isFlashing, flashColor, flashHole, setFlashPaint]);
327
+ // Clear on unmount so a colour can never outlive the step.
328
+ useEffect(() => () => setFlashPaint(null), [setFlashPaint]);
329
+
249
330
  const frameOutput = useFrameOutput({
250
331
  // ML Kit (Android) and Apple Vision both consume YUV efficiently.
251
332
  pixelFormat: 'yuv',
@@ -270,81 +351,40 @@ export function LivenessStep(): React.ReactElement {
270
351
  if (cameraUnavailable) {
271
352
  return <CameraUnavailableView />;
272
353
  }
354
+ // "Here's what happens next", BEFORE the OS prompt — and shown even when
355
+ // permission is already granted, because it is about what the step will ask
356
+ // of the user, not about access. Opening the camera unannounced is what makes
357
+ // people fumble the first attempt.
358
+ if (!ready) {
359
+ return <ReadyPrimer content={READY_LIVENESS} onReady={() => setReady(true)} />;
360
+ }
361
+ if (showPrimer) {
362
+ return <CameraPermissionPrimingView onGrant={() => setPerm('requesting')} />;
363
+ }
273
364
  if (permissionDenied) {
274
- return (
275
- <CameraPermissionView
276
- onRetry={() => {
277
- askedRef.current = false;
278
- setPerm('checking');
279
- }}
280
- />
281
- );
365
+ return <CameraPermissionView onRetry={() => setPerm('requesting')} />;
282
366
  }
283
367
 
284
368
  // ── Review (selfie captured) ─────────────────────────────────────────────────
285
369
  if (liveness.phase === 'complete' && selfieUri) {
286
370
  return (
287
- <View>
288
- <SelfiePreview uri={selfieUri} uploading={uploading && !uploadError} />
289
- {retryInfo && uploading ? (
290
- <MyazaText variant="bodySmall" color={colors.warning} style={{ textAlign: 'center', marginTop: spacing.sm }}>
291
- {`Upload failed — retrying (${retryInfo.attempt}/${retryInfo.total})…`}
292
- </MyazaText>
293
- ) : null}
294
- <View style={{ height: spacing.md }} />
295
- {uploadError ? (
296
- // The error message itself is shown as a top toast; keep a retry action.
297
- <MyazaButton
298
- label="Try Again"
299
- loading={uploading}
300
- onPress={() => void uploadSelfieAndVideo(selfieUri, videoPathRef.current)}
301
- />
302
- ) : (
303
- <View style={{ flexDirection: 'row', gap: spacing.md }}>
304
- <View style={{ flex: 1 }}>
305
- <MyazaButton
306
- label="Retake"
307
- variant="outline"
308
- leadingIcon="refresh"
309
- disabled={uploading}
310
- onPress={() => {
311
- setSelfieUri(null);
312
- setUploadError(null);
313
- selfieIdRef.current = null;
314
- videoPathRef.current = null;
315
- liveness.reset();
316
- }}
317
- />
318
- </View>
319
- <View style={{ flex: 1 }}>
320
- <MyazaButton
321
- label="Continue"
322
- loading={uploading}
323
- disabled={uploading || !selfieIdRef.current}
324
- onPress={nextStep}
325
- />
326
- </View>
327
- </View>
328
- )}
329
- </View>
371
+ <LivenessComplete
372
+ selfieUri={selfieUri}
373
+ upload={upload}
374
+ onRetake={() => {
375
+ setSelfieUri(null);
376
+ setUploadError(null);
377
+ selfieIdRef.current = null;
378
+ videoPathRef.current = null;
379
+ liveness.reset();
380
+ }}
381
+ onContinue={nextStep}
382
+ />
330
383
  );
331
384
  }
332
385
 
333
- // ── Failed (timeout / face lost) — mirrors the Flutter failed view: a centred
334
- // red message and a full-width "Try Again" button (no icon). ───────────────
335
386
  if (liveness.phase === 'failed') {
336
- return (
337
- <View style={{ alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xl, gap: spacing.lg }}>
338
- <MyazaText variant="bodyMedium" color={colors.error} style={{ textAlign: 'center', fontWeight: '500' }}>
339
- {liveness.failureReason === 'timeout'
340
- ? "Time's up. Let's try again."
341
- : 'Face lost. Please try again.'}
342
- </MyazaText>
343
- <View style={{ alignSelf: 'stretch' }}>
344
- <MyazaButton label="Try Again" onPress={liveness.reset} />
345
- </View>
346
- </View>
347
- );
387
+ return <LivenessFailed reason={liveness.failureReason} onRetry={liveness.reset} />;
348
388
  }
349
389
 
350
390
  // ── Live camera (circular) + ring state machine + guidance ──────────────────
@@ -385,15 +425,23 @@ export function LivenessStep(): React.ReactElement {
385
425
  const CIRCLE = Math.min(Dimensions.get('window').width - spacing.md * 4, 300);
386
426
  const lighting = liveness.lightingGuidance;
387
427
 
428
+ // Rendered INSIDE the sheet, so the SDK header and the "powered by" footer
429
+ // stay visible throughout — only the flash itself goes full-screen.
388
430
  return (
389
431
  <View style={{ alignItems: 'center', gap: spacing.md }}>
390
432
  {/* Instruction text — above the circle */}
391
- <MyazaText variant="heading3" style={{ textAlign: 'center', minHeight: 26 }} color={instrColor}>
392
- {guidance.text}
433
+ <MyazaText
434
+ variant="heading3"
435
+ style={{ textAlign: 'center', minHeight: INSTRUCTION_HEIGHT }}
436
+ color={instrColor}
437
+ >
438
+ {phase === 'flash' ? '' : guidance.text}
393
439
  </MyazaText>
394
440
 
395
441
  {/* Circular camera with a thick colour-state ring */}
396
442
  <View
443
+ ref={flashHoleRef}
444
+ collapsable={false}
397
445
  style={{
398
446
  width: CIRCLE,
399
447
  height: CIRCLE,
@@ -472,144 +520,10 @@ export function LivenessStep(): React.ReactElement {
472
520
  {lighting ? <LightingBanner text={lightingGuidanceText(lighting)} /> : null}
473
521
 
474
522
  {/* Numbered progress dots with connectors */}
475
- <ProgressDots total={liveness.totalCount} completed={liveness.completedCount} active={phase === 'challenge' || phase === 'positioning'} />
523
+ <ProgressDots total={liveness.totalCount} completed={liveness.completedCount} active={phase === 'challenge' || phase === 'positioning' || phase === 'flash'} />
476
524
 
477
525
  {/* Gesture demo avatar */}
478
526
  {liveness.activeChallenge ? <LivenessAvatar challenge={liveness.activeChallenge} /> : null}
479
527
  </View>
480
528
  );
481
529
  }
482
-
483
- const StyleAbsFill = { position: 'absolute' as const, top: 0, left: 0, right: 0, bottom: 0 };
484
-
485
- // Lighting warning — 1:1 with the Flutter `_LightingWarningBanner`: amber-50 bg,
486
- // amber-200 border, amber-800 lightbulb + text, fading/sliding in (~300ms).
487
- const AMBER_50 = '#FFFBEB';
488
- const AMBER_200 = '#FDE68A';
489
- const AMBER_800 = '#92400E';
490
-
491
- function LightingBanner({ text }: { text: string }): React.ReactElement {
492
- const anim = useRef(new Animated.Value(0)).current;
493
- useEffect(() => {
494
- Animated.timing(anim, { toValue: 1, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
495
- }, [anim]);
496
- return (
497
- <Animated.View
498
- style={{
499
- alignSelf: 'stretch',
500
- opacity: anim,
501
- transform: [{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }],
502
- }}
503
- >
504
- <View
505
- style={{
506
- flexDirection: 'row',
507
- alignItems: 'flex-start',
508
- gap: spacing.sm,
509
- borderRadius: radius.sm,
510
- borderWidth: 1,
511
- borderColor: AMBER_200,
512
- backgroundColor: AMBER_50,
513
- paddingHorizontal: spacing.sm + 4,
514
- paddingVertical: 10,
515
- }}
516
- >
517
- <Icon name="lightbulb" size={16} color={AMBER_800} />
518
- <MyazaText variant="bodySmall" color={AMBER_800} style={{ flexShrink: 1, lineHeight: 17 }}>
519
- {text}
520
- </MyazaText>
521
- </View>
522
- </Animated.View>
523
- );
524
- }
525
-
526
- /** The single guidance string + tone to show under the camera. */
527
- function resolveGuidance(l: ReturnType<typeof useLiveness>): { text: string; tone: 'normal' | 'error' } {
528
- if (l.multipleFaces) return { text: l.instruction, tone: 'error' };
529
- // Lighting is shown in its own banner (below), not as the main instruction.
530
- if (l.phase === 'positioning' && !l.faceDetected) {
531
- return { text: 'Position your face in the circle', tone: 'normal' };
532
- }
533
- if (l.positionGuidance) return { text: positionGuidanceText(l.positionGuidance), tone: 'error' };
534
- if (l.wrongGesture) return { text: 'Wrong gesture — follow the prompt', tone: 'error' };
535
- return { text: l.instruction, tone: 'normal' };
536
- }
537
-
538
- // Numbered progress steps with connectors — mirrors the web/Flutter SDKs:
539
- // passed = solid green + checkmark, active = green-outlined number, pending =
540
- // grey number. `active` marks the current (completed-th) step as in-progress.
541
- function ProgressDots({
542
- total,
543
- completed,
544
- active,
545
- }: {
546
- total: number;
547
- completed: number;
548
- active: boolean;
549
- }): React.ReactElement {
550
- const { colors } = useTheme();
551
- return (
552
- <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
553
- {Array.from({ length: total }).map((_, i) => {
554
- const state: 'passed' | 'active' | 'pending' =
555
- i < completed ? 'passed' : i === completed && active ? 'active' : 'pending';
556
- return (
557
- <React.Fragment key={i}>
558
- <View
559
- style={{
560
- width: 28,
561
- height: 28,
562
- borderRadius: 14,
563
- alignItems: 'center',
564
- justifyContent: 'center',
565
- backgroundColor: state === 'passed' ? colors.success : state === 'active' ? colors.background : colors.gray300,
566
- borderWidth: state === 'active' ? 2 : 0,
567
- borderColor: colors.success,
568
- }}
569
- >
570
- {state === 'passed' ? (
571
- <Svg width={14} height={14} viewBox="0 0 24 24">
572
- <Path d="M5 13l4 4L19 7" fill="none" stroke="#FFFFFF" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
573
- </Svg>
574
- ) : (
575
- <MyazaText variant="bodySmall" color={state === 'active' ? colors.success : colors.textMuted} style={{ fontWeight: '700' }}>
576
- {String(i + 1)}
577
- </MyazaText>
578
- )}
579
- </View>
580
- {i < total - 1 ? (
581
- <View style={{ width: 28, height: 2, backgroundColor: i < completed ? colors.success : colors.gray300 }} />
582
- ) : null}
583
- </React.Fragment>
584
- );
585
- })}
586
- </View>
587
- );
588
- }
589
-
590
- function SelfiePreview({ uri, uploading }: { uri: string; uploading?: boolean }): React.ReactElement {
591
- const { colors } = useTheme();
592
- const S = 200;
593
- return (
594
- <View style={{ alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch' }}>
595
- <View style={{ width: S, height: S, borderRadius: S / 2, borderWidth: 4, backgroundColor: '#111111', borderColor: `${colors.primary}33`, overflow: 'hidden' }}>
596
- {/* borderRadius repeated on the Image — iOS doesn't reliably clip an Image
597
- child to a rounded parent (same fix as the header brand bar). */}
598
- <Image source={{ uri }} style={{ width: '100%', height: '100%', borderRadius: S / 2 }} resizeMode="cover" />
599
- {/* Standard upload loader inside the preview circle (mirrors web/Flutter). */}
600
- {uploading ? (
601
- <View style={[StyleAbsFill, { backgroundColor: 'rgba(0,0,0,0.45)', alignItems: 'center', justifyContent: 'center' }]}>
602
- <MyazaPulseLoader size={64} />
603
- </View>
604
- ) : null}
605
- </View>
606
- <View style={{ height: spacing.md }} />
607
- <MyazaText variant="heading3" style={{ textAlign: 'center' }}>
608
- Looking good!
609
- </MyazaText>
610
- <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
611
- Tap Continue to submit, or Retake to try again.
612
- </MyazaText>
613
- </View>
614
- );
615
- }