@myazahq/kyc-sdk-react-native 2.0.2 → 2.2.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 +152 -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 +70 -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 +162 -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 +322 -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 +207 -0
  198. package/src/screens/CompanyInfoFields.tsx +83 -0
  199. package/src/screens/ConsentStep.tsx +68 -68
  200. package/src/screens/ContactChannelChoice.tsx +133 -0
  201. package/src/screens/ContactCodeStep.tsx +130 -0
  202. package/src/screens/ContactDestinationField.tsx +70 -0
  203. package/src/screens/ContactFooterNote.tsx +34 -0
  204. package/src/screens/ContactVerificationStep.tsx +199 -0
  205. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  206. package/src/screens/CountrySelectStep.tsx +210 -0
  207. package/src/screens/DocumentCaptureStep.tsx +156 -132
  208. package/src/screens/IdInputStep.tsx +13 -5
  209. package/src/screens/IdTypeStep.tsx +74 -33
  210. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  211. package/src/screens/KeyPersonCard.tsx +119 -0
  212. package/src/screens/KeyPersonForm.tsx +148 -0
  213. package/src/screens/KeyPersonSheet.tsx +184 -0
  214. package/src/screens/LivenessStep.tsx +155 -247
  215. package/src/screens/MrzScanView.tsx +173 -0
  216. package/src/screens/NfcStep.tsx +290 -0
  217. package/src/screens/ProofOfAddressStep.tsx +238 -0
  218. package/src/screens/QuestionnaireField.tsx +219 -0
  219. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  220. package/src/screens/QuestionnaireStep.tsx +76 -0
  221. package/src/screens/SubmittedStep.tsx +5 -0
  222. package/src/screens/consent/model.ts +109 -0
  223. package/src/screens/document/CameraPhase.tsx +150 -0
  224. package/src/screens/document/RequiredPill.tsx +60 -0
  225. package/src/screens/liveness/LightingBanner.tsx +50 -0
  226. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  227. package/src/screens/liveness/ProgressDots.tsx +67 -0
  228. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  229. package/src/screens/liveness/constants.ts +31 -0
  230. package/src/screens/liveness/index.ts +8 -0
  231. package/src/screens/liveness/resolveGuidance.ts +27 -0
  232. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  233. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  234. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  235. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  236. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  237. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  238. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  239. package/src/screens/nfc/availability.ts +73 -0
  240. package/src/screens/nfc/readErrorMessage.ts +93 -0
  241. package/src/screens/nfc/useChipPortrait.ts +58 -0
  242. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  243. package/src/screens/useFlashHole.ts +105 -0
  244. package/src/screens/useFlashSequence.ts +96 -0
  245. package/src/screens/usePoaAttach.ts +121 -0
  246. package/src/services/api-types.ts +164 -0
  247. package/src/services/api-verify-types.ts +99 -0
  248. package/src/services/api.ts +72 -102
  249. package/src/services/cardCrop.ts +25 -10
  250. package/src/services/contactErrors.ts +43 -0
  251. package/src/services/deviceMetadata.ts +1 -1
  252. package/src/services/documentPicker.ts +20 -0
  253. package/src/services/fingerprint-sources.ts +129 -0
  254. package/src/services/fingerprint.ts +101 -0
  255. package/src/services/haptics.ts +38 -0
  256. package/src/services/mediaCompress.ts +7 -2
  257. package/src/services/resolveUrl.ts +2 -2
  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 -124
  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
@@ -0,0 +1,109 @@
1
+ import type { MyazaKYCConfig } from '../../types/config';
2
+ import type { IconName } from '../../components/Icon';
3
+ import { fillTokens } from '../../utils/tokens';
4
+ import { hasEmailVerificationStep, hasPhoneVerificationStep } from '../../config/contact';
5
+ import {
6
+ hasApplicantVerification,
7
+ hasBusinessDocumentsStep,
8
+ hasKeyPeopleCollection,
9
+ } from '../../config/businessSteps';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // What the consent screen SAYS, derived from what the flow actually DOES.
13
+ //
14
+ // Mirrors the web SDK's ConsentStep derivations exactly — a KYB flow gets the
15
+ // business title, description, step list and data clause, never the identity
16
+ // copy ("verify your government-issued ID" on a registry-lookup flow is simply
17
+ // false). The biometric sentence is derived, not assumed: claiming facial
18
+ // recognition on a flow with no face capture would be a false statement in a
19
+ // legal notice, and recording video without saying so is the failure that
20
+ // actually carries risk.
21
+ //
22
+ // Pure (no RN imports beyond types) so it is unit-testable next to the web
23
+ // SDK's consent-disclosure tests. The screen just renders this.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface ConsentProcessStep {
27
+ icon: IconName;
28
+ label: string;
29
+ }
30
+
31
+ export interface ConsentModel {
32
+ isBusiness: boolean;
33
+ title: string;
34
+ description: string;
35
+ steps: ConsentProcessStep[];
36
+ /** Drives the "facial recognition" sentence in the legal notice. */
37
+ capturesFace: boolean;
38
+ /** Drives the "recording this session" sentence. */
39
+ recordsVideo: boolean;
40
+ }
41
+
42
+ const DEFAULT_DESCRIPTION =
43
+ 'We need to verify your identity to comply with regulatory requirements. This process is quick and secure.';
44
+
45
+ const DEFAULT_BUSINESS_DESCRIPTION =
46
+ 'We need to verify your business to comply with regulatory requirements. This process is quick and secure.';
47
+
48
+ export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
49
+ const isBusiness = config.subjectType === 'business';
50
+ const firstName = config.userData?.firstName ?? '';
51
+
52
+ const defaultTitle = firstName
53
+ ? `Welcome, ${firstName}`
54
+ : isBusiness
55
+ ? 'Business Verification'
56
+ : 'Identity Verification';
57
+ const title = config.consent?.title
58
+ ? fillTokens(config.consent.title, config.userData)
59
+ : defaultTitle;
60
+ const description = config.consent?.description
61
+ ? fillTokens(config.consent.description, config.userData)
62
+ : isBusiness
63
+ ? DEFAULT_BUSINESS_DESCRIPTION
64
+ : DEFAULT_DESCRIPTION;
65
+
66
+ // A business flow captures a face only when the applicant verifies their own
67
+ // identity in-flow; an individual flow whenever the selfie step is on.
68
+ const capturesFace = isBusiness
69
+ ? hasApplicantVerification(config.business)
70
+ : config.enableSelfie !== false;
71
+ const recordsVideo = capturesFace || (!isBusiness && config.enableDocumentCapture !== false);
72
+
73
+ const steps: ConsentProcessStep[] = isBusiness
74
+ ? [
75
+ { icon: 'building-2', label: 'Collect your business registration details' },
76
+ { icon: 'badge-check', label: 'Verify your business against the official registry' },
77
+ ]
78
+ : [
79
+ { icon: 'badge-check', label: 'Verify your government-issued ID' },
80
+ { icon: 'user', label: 'Collect basic personal information' },
81
+ ];
82
+ if (hasEmailVerificationStep(config.emailVerification) || hasPhoneVerificationStep(config.phoneVerification)) {
83
+ steps.push({ icon: 'lock', label: 'Confirm your contact details with a one-time code' });
84
+ }
85
+ if (!isBusiness && config.enableDocumentCapture !== false) {
86
+ steps.push({ icon: 'scan-line', label: 'Capture a photo of your ID document' });
87
+ }
88
+ // Chip-capable IDs additionally read the document's NFC chip — listed when
89
+ // the flow enables NFC so the user knows to have the physical document to
90
+ // hand (a non-chip ID simply skips the step). Individual flows only; a KYB
91
+ // config can't carry `nfc`.
92
+ if (!isBusiness && config.nfc?.enabled) {
93
+ steps.push({ icon: 'nfc', label: 'Scan your document’s security chip (NFC)' });
94
+ }
95
+ if (!isBusiness && config.enableSelfie !== false) {
96
+ steps.push({ icon: 'scan-face', label: 'Take a selfie for facial verification' });
97
+ }
98
+ if (isBusiness && hasKeyPeopleCollection(config.business)) {
99
+ steps.push({ icon: 'users', label: "List the company's directors and owners" });
100
+ }
101
+ if (isBusiness && hasBusinessDocumentsStep(config.business)) {
102
+ steps.push({ icon: 'file-text', label: 'Upload supporting business documents' });
103
+ }
104
+ if (isBusiness && hasApplicantVerification(config.business)) {
105
+ steps.push({ icon: 'scan-face', label: 'Verify your own identity' });
106
+ }
107
+
108
+ return { isBusiness, title, description, steps, capturesFace, recordsVideo };
109
+ }
@@ -0,0 +1,150 @@
1
+ import React from 'react';
2
+ import { Modal, Pressable, 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 { CameraViewfinder } from '../../components/CameraViewfinder';
9
+ import { RequiredPill } from './RequiredPill';
10
+ import type { MrzScan } from '../../mrz/parse';
11
+
12
+ export interface CameraPhaseProps {
13
+ isBack: boolean;
14
+ /** True once the live preview is actually on screen — drives immersive mode. */
15
+ cameraLive: boolean;
16
+ active: boolean;
17
+ documentLabel: string;
18
+ guideAspect: number;
19
+ isTwoSided: boolean;
20
+ busy: boolean;
21
+ allowUpload: boolean;
22
+ country: string;
23
+ idType?: string;
24
+ mrzAlreadyCaptured: boolean;
25
+ onMrz: (scan: MrzScan) => void;
26
+ onCapture: (uri: string, videoPath: string | null, viewAr: number) => void;
27
+ onUpload: () => void;
28
+ onBack: () => void;
29
+ /** Safe-area bottom inset for the full-bleed camera's controls. */
30
+ bottomInset?: number;
31
+ }
32
+
33
+ /**
34
+ * The document-capture camera, in both of its shapes.
35
+ *
36
+ * IMMERSIVE is the normal one: the camera owns the screen, because the sheet's
37
+ * header, padding and scroll view are exactly what force a small viewfinder on
38
+ * a short phone — and a camera you have to scroll to is a broken camera. The
39
+ * pill and helper text go with it; the viewfinder carries that information as
40
+ * live overlays instead.
41
+ *
42
+ * The framed shape below it is what renders in the brief window while the OS
43
+ * permission prompt is open, where there is no live preview to hand the screen
44
+ * to yet.
45
+ */
46
+ export function CameraPhase(p: CameraPhaseProps): React.ReactElement {
47
+ const { colors } = useTheme();
48
+
49
+ const viewfinder = (extra: {
50
+ fill?: boolean;
51
+ onBack?: () => void;
52
+ onUpload?: (() => void) | null;
53
+ bottomInset?: number;
54
+ }) => (
55
+ <CameraViewfinder
56
+ active={p.active}
57
+ side={p.isBack ? 'back' : 'front'}
58
+ documentLabel={p.documentLabel}
59
+ guideAspect={p.guideAspect}
60
+ onCapture={p.onCapture}
61
+ busy={p.busy}
62
+ // Auto-capture needs to know WHICH document it is looking for — without
63
+ // this it would fire on any text-dense frame.
64
+ country={p.country}
65
+ idType={p.idType}
66
+ // Banking the MRZ from a live frame is what spares the user scanning the
67
+ // same passport again at the chip step. Only the front carries one.
68
+ onMrz={p.isBack ? undefined : p.onMrz}
69
+ mrzAlreadyCaptured={p.mrzAlreadyCaptured}
70
+ {...extra}
71
+ />
72
+ );
73
+
74
+ if (p.cameraLive) {
75
+ // A FULL-SCREEN modal of its own, not just full-bleed content.
76
+ //
77
+ // The SDK's sheet is an iOS pageSheet: inset from the top, rounded, with
78
+ // the card visible behind it. Filling that still leaves the camera in a
79
+ // letterboxed card — and a document held at arm's length needs every pixel.
80
+ //
81
+ // Presenting it as a Modal rather than switching the sheet's own
82
+ // presentationStyle matters: a Modal's children stay in the SAME React
83
+ // tree, so this step keeps its state (the acknowledged primer, the captured
84
+ // front) — where changing presentationStyle on a live modal remounts the
85
+ // subtree and loses it. It closes when the capture finishes, and the sheet
86
+ // is exactly as it was.
87
+ return (
88
+ <Modal
89
+ visible
90
+ animationType="fade"
91
+ presentationStyle="fullScreen"
92
+ statusBarTranslucent
93
+ onRequestClose={p.onBack}
94
+ >
95
+ <View style={{ flex: 1, backgroundColor: '#000000' }}>
96
+ {viewfinder({
97
+ fill: true,
98
+ onBack: p.onBack,
99
+ // The gallery escape stays reachable full-screen: it is the
100
+ // fallback when the camera will not read a worn or laminated
101
+ // document.
102
+ onUpload: p.allowUpload ? p.onUpload : null,
103
+ bottomInset: p.bottomInset ?? 0,
104
+ })}
105
+ </View>
106
+ </Modal>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <View>
112
+ <RequiredPill
113
+ documentLabel={p.documentLabel}
114
+ sideBadge={p.isTwoSided ? (p.isBack ? 'Back Side' : 'Front Side') : undefined}
115
+ stepLabel={p.isTwoSided ? (p.isBack ? 'Step 2 of 2' : 'Step 1 of 2') : undefined}
116
+ />
117
+ {p.isBack ? (
118
+ <View style={{ flexDirection: 'row', alignItems: 'center', marginTop: spacing.sm, marginBottom: spacing.xs }}>
119
+ <Icon name="credit-card" size={14} color={colors.primary} />
120
+ <View style={{ width: 4 }} />
121
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '500' }}>
122
+ Flip the card over and scan the other side
123
+ </MyazaText>
124
+ </View>
125
+ ) : null}
126
+ <View style={{ height: spacing.md }} />
127
+ {viewfinder({})}
128
+ {!p.busy ? (
129
+ <>
130
+ <View style={{ height: spacing.md }} />
131
+ <MyazaText variant="bodySmall" style={{ textAlign: 'center' }}>
132
+ Tap the button to capture manually
133
+ </MyazaText>
134
+ {p.allowUpload ? (
135
+ <Pressable onPress={p.onUpload} style={{ marginTop: spacing.sm }}>
136
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
137
+ <MyazaText variant="bodySmall">Having trouble? </MyazaText>
138
+ <Icon name="upload" size={14} color={colors.primary} />
139
+ <View style={{ width: 4 }} />
140
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
141
+ Upload a photo instead
142
+ </MyazaText>
143
+ </View>
144
+ </Pressable>
145
+ ) : null}
146
+ </>
147
+ ) : null}
148
+ </View>
149
+ );
150
+ }
@@ -0,0 +1,60 @@
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
+
9
+ // "Required: {label}" pill with optional side badge + step label — mirrors the
10
+ // Flutter SDK's _RequiredPill on the capture screen.
11
+ export function RequiredPill({
12
+ documentLabel,
13
+ sideBadge,
14
+ stepLabel,
15
+ }: {
16
+ documentLabel: string;
17
+ sideBadge?: string;
18
+ stepLabel?: string;
19
+ }): React.ReactElement {
20
+ const { colors } = useTheme();
21
+ return (
22
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
23
+ <View
24
+ style={{
25
+ flex: 1,
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ flexWrap: 'wrap',
29
+ borderWidth: 1,
30
+ borderColor: `${colors.primary}33`,
31
+ backgroundColor: `${colors.primary}0D`,
32
+ borderRadius: radius.md,
33
+ paddingHorizontal: spacing.sm + 4,
34
+ paddingVertical: spacing.sm,
35
+ }}
36
+ >
37
+ <Icon name="credit-card" size={16} color={colors.primary} />
38
+ <View style={{ width: 8 }} />
39
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600' }}>
40
+ Required:{' '}
41
+ </MyazaText>
42
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ flexShrink: 1 }}>
43
+ {documentLabel}
44
+ </MyazaText>
45
+ {sideBadge ? (
46
+ <View style={{ backgroundColor: `${colors.primary}26`, borderRadius: radius.full, paddingHorizontal: 8, paddingVertical: 2, marginLeft: 8 }}>
47
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '600', fontSize: 11 }}>
48
+ {sideBadge}
49
+ </MyazaText>
50
+ </View>
51
+ ) : null}
52
+ </View>
53
+ {stepLabel ? (
54
+ <MyazaText variant="bodySmall" color={colors.textMuted} style={{ marginLeft: spacing.sm }}>
55
+ {stepLabel}
56
+ </MyazaText>
57
+ ) : null}
58
+ </View>
59
+ );
60
+ }
@@ -0,0 +1,50 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing, View } from 'react-native';
3
+
4
+ import { spacing, radius } from '../../config/theme';
5
+ import { MyazaText } from '../../components/Typography';
6
+ import { Icon } from '../../components/Icon';
7
+ import { AMBER_50, AMBER_200, AMBER_800 } from './constants';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Lighting warning — 1:1 with the Flutter `_LightingWarningBanner`: amber-50
11
+ // background, amber-200 border, amber-800 lightbulb + text, fading and sliding
12
+ // in over ~300ms.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function LightingBanner({ text }: { text: string }): React.ReactElement {
16
+ const anim = useRef(new Animated.Value(0)).current;
17
+ useEffect(() => {
18
+ Animated.timing(anim, { toValue: 1, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
19
+ }, [anim]);
20
+ return (
21
+ <Animated.View
22
+ style={{
23
+ alignSelf: 'stretch',
24
+ opacity: anim,
25
+ transform: [{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }],
26
+ }}
27
+ >
28
+ <View
29
+ style={{
30
+ flexDirection: 'row',
31
+ alignItems: 'flex-start',
32
+ gap: spacing.sm,
33
+ borderRadius: radius.sm,
34
+ borderWidth: 1,
35
+ borderColor: AMBER_200,
36
+ backgroundColor: AMBER_50,
37
+ paddingHorizontal: spacing.sm + 4,
38
+ paddingVertical: 10,
39
+ }}
40
+ >
41
+ <Icon name="lightbulb" size={16} color={AMBER_800} />
42
+ <MyazaText variant="bodySmall" color={AMBER_800} style={{ flexShrink: 1, lineHeight: 17 }}>
43
+ {text}
44
+ </MyazaText>
45
+ </View>
46
+ </Animated.View>
47
+ );
48
+ }
49
+
50
+ /** The single guidance string + tone to show under the camera. */
@@ -0,0 +1,109 @@
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 { MyazaButton } from '../../components/MyazaButton';
8
+ import { SelfiePreview } from './SelfiePreview';
9
+ import type { SelfieUpload } from './useSelfieUpload';
10
+ import type { LivenessFailureReason } from '../../liveness/types';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // The two ways the liveness step ends.
14
+ //
15
+ // COMPLETE is not "done": the selfie is uploading behind the preview, so
16
+ // Continue stays disabled until the media id exists. A failed upload keeps the
17
+ // captured selfie and offers a retry rather than sending the user back through
18
+ // the whole check for a network blip.
19
+ //
20
+ // FAILED mirrors the Flutter failed view — a centred red line and a full-width
21
+ // "Try Again", no icon.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function LivenessComplete({
25
+ selfieUri,
26
+ upload,
27
+ onRetake,
28
+ onContinue,
29
+ }: {
30
+ selfieUri: string;
31
+ upload: SelfieUpload;
32
+ onRetake: () => void;
33
+ onContinue: () => void;
34
+ }): React.ReactElement {
35
+ const { colors } = useTheme();
36
+ const { uploading, uploadError, retryInfo, selfieIdRef, videoPathRef, uploadSelfieAndVideo } =
37
+ upload;
38
+ return (
39
+ <View>
40
+ <SelfiePreview uri={selfieUri} uploading={uploading && !uploadError} />
41
+ {retryInfo && uploading ? (
42
+ <MyazaText variant="bodySmall" color={colors.warning} style={{ textAlign: 'center', marginTop: spacing.sm }}>
43
+ {`Upload failed — retrying (${retryInfo.attempt}/${retryInfo.total})…`}
44
+ </MyazaText>
45
+ ) : null}
46
+ <View style={{ height: spacing.md }} />
47
+ {uploadError ? (
48
+ // The error message itself is shown as a top toast; keep a retry action.
49
+ <MyazaButton
50
+ label="Try Again"
51
+ // Disabled while the retry is in flight, not spinning — the progress
52
+ // indicator belongs in the selfie frame, where the work is.
53
+ disabled={uploading}
54
+ onPress={() => void uploadSelfieAndVideo(selfieUri, videoPathRef.current)}
55
+ />
56
+ ) : (
57
+ <View style={{ flexDirection: 'row', gap: spacing.md }}>
58
+ <View style={{ flex: 1 }}>
59
+ <MyazaButton
60
+ label="Retake"
61
+ variant="outline"
62
+ leadingIcon="refresh"
63
+ disabled={uploading}
64
+ onPress={onRetake}
65
+ />
66
+ </View>
67
+ <View style={{ flex: 1 }}>
68
+ {/* No spinner on Continue, deliberately — 1:1 with Flutter.
69
+
70
+ There is already ONE progress indicator on this screen, inside
71
+ the selfie frame, and it is the truthful place for it: what is
72
+ loading is the photo, not the button. A second spinner on the
73
+ action implies the tap did something, when in fact the upload
74
+ started the moment the selfie appeared and the button has been
75
+ disabled since. Disabled-and-quiet says "not yet"; a spinner
76
+ says "working on your request", which would be a lie. */}
77
+ <MyazaButton
78
+ label="Continue"
79
+ disabled={uploading || !selfieIdRef.current}
80
+ onPress={onContinue}
81
+ />
82
+ </View>
83
+ </View>
84
+ )}
85
+ </View>
86
+ );
87
+ }
88
+
89
+ export function LivenessFailed({
90
+ reason,
91
+ onRetry,
92
+ }: {
93
+ reason: LivenessFailureReason | null;
94
+ onRetry: () => void;
95
+ }): React.ReactElement {
96
+ const { colors } = useTheme();
97
+ return (
98
+ <View style={{ alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.xl, gap: spacing.lg }}>
99
+ <MyazaText variant="bodyMedium" color={colors.error} style={{ textAlign: 'center', fontWeight: '500' }}>
100
+ {reason === 'timeout'
101
+ ? "Time's up. Let's try again."
102
+ : 'Face lost. Please try again.'}
103
+ </MyazaText>
104
+ <View style={{ alignSelf: 'stretch' }}>
105
+ <MyazaButton label="Try Again" onPress={onRetry} />
106
+ </View>
107
+ </View>
108
+ );
109
+ }
@@ -0,0 +1,67 @@
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 Svg, { Path } from 'react-native-svg';
7
+
8
+ import { Icon } from '../../components/Icon';
9
+ import { MyazaText } from '../../components/Typography';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Numbered progress dots with connectors.
13
+ //
14
+ // One dot per step the user actually performs — the gestures AND the flash. The
15
+ // flash was omitted for a long time, so in 'both' mode the indicator read as
16
+ // finished while the flash was still to come, and in flash-only mode it showed
17
+ // no steps at all.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export function ProgressDots({
21
+ total,
22
+ completed,
23
+ active,
24
+ }: {
25
+ total: number;
26
+ completed: number;
27
+ active: boolean;
28
+ }): React.ReactElement {
29
+ const { colors } = useTheme();
30
+ return (
31
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }}>
32
+ {Array.from({ length: total }).map((_, i) => {
33
+ const state: 'passed' | 'active' | 'pending' =
34
+ i < completed ? 'passed' : i === completed && active ? 'active' : 'pending';
35
+ return (
36
+ <React.Fragment key={i}>
37
+ <View
38
+ style={{
39
+ width: 28,
40
+ height: 28,
41
+ borderRadius: 14,
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ backgroundColor: state === 'passed' ? colors.success : state === 'active' ? colors.background : colors.gray300,
45
+ borderWidth: state === 'active' ? 2 : 0,
46
+ borderColor: colors.success,
47
+ }}
48
+ >
49
+ {state === 'passed' ? (
50
+ <Svg width={14} height={14} viewBox="0 0 24 24">
51
+ <Path d="M5 13l4 4L19 7" fill="none" stroke="#FFFFFF" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />
52
+ </Svg>
53
+ ) : (
54
+ <MyazaText variant="bodySmall" color={state === 'active' ? colors.success : colors.textMuted} style={{ fontWeight: '700' }}>
55
+ {String(i + 1)}
56
+ </MyazaText>
57
+ )}
58
+ </View>
59
+ {i < total - 1 ? (
60
+ <View style={{ width: 28, height: 2, backgroundColor: i < completed ? colors.success : colors.gray300 }} />
61
+ ) : null}
62
+ </React.Fragment>
63
+ );
64
+ })}
65
+ </View>
66
+ );
67
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, Image, 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 { MyazaPulseLoader } from '../../components/MyazaPulseLoader';
8
+ import { StyleAbsFill } from './constants';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The captured selfie, shown back while it uploads.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function SelfiePreview({ uri, uploading }: { uri: string; uploading?: boolean }): React.ReactElement {
15
+ const { colors } = useTheme();
16
+ const S = 200;
17
+ return (
18
+ <View style={{ alignItems: 'center', justifyContent: 'center', alignSelf: 'stretch' }}>
19
+ <View style={{ width: S, height: S, borderRadius: S / 2, borderWidth: 4, backgroundColor: '#111111', borderColor: `${colors.primary}33`, overflow: 'hidden' }}>
20
+ {/* borderRadius repeated on the Image — iOS doesn't reliably clip an Image
21
+ child to a rounded parent (same fix as the header brand bar). */}
22
+ <Image source={{ uri }} style={{ width: '100%', height: '100%', borderRadius: S / 2 }} resizeMode="cover" />
23
+ {/* Standard upload loader inside the preview circle (mirrors web/Flutter). */}
24
+ {uploading ? (
25
+ <View style={[StyleAbsFill, { backgroundColor: 'rgba(0,0,0,0.45)', alignItems: 'center', justifyContent: 'center' }]}>
26
+ <MyazaPulseLoader size={64} />
27
+ </View>
28
+ ) : null}
29
+ </View>
30
+ <View style={{ height: spacing.md }} />
31
+ <MyazaText variant="heading3" style={{ textAlign: 'center' }}>
32
+ Looking good!
33
+ </MyazaText>
34
+ <MyazaText variant="bodySmall" color={colors.textSecondary} style={{ textAlign: 'center' }}>
35
+ Tap Continue to submit, or Retake to try again.
36
+ </MyazaText>
37
+ </View>
38
+ );
39
+ }
@@ -0,0 +1,31 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Layout constants and timings shared across the liveness step's pieces.
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export const StyleAbsFill = {
6
+ position: 'absolute' as const,
7
+ top: 0,
8
+ left: 0,
9
+ right: 0,
10
+ bottom: 0,
11
+ };
12
+
13
+ /**
14
+ * How long to wait for the face to reappear before abandoning a capture.
15
+ *
16
+ * Generous: a full-screen colour flash can make a face momentarily
17
+ * undetectable, and giving up on one dropped frame would restart a sequence the
18
+ * user is still correctly inside.
19
+ */
20
+ export const FRESH_FACE_TIMEOUT_MS = 1500;
21
+
22
+ /**
23
+ * Reserved height of the instruction line. Fixed so the circle does not jump as
24
+ * the wording changes.
25
+ */
26
+ export const INSTRUCTION_HEIGHT = 26;
27
+
28
+ // Lighting warning palette — 1:1 with the Flutter `_LightingWarningBanner`.
29
+ export const AMBER_50 = '#FFFBEB';
30
+ export const AMBER_200 = '#FDE68A';
31
+ export const AMBER_800 = '#92400E';
@@ -0,0 +1,8 @@
1
+ // Barrel for the liveness step's pieces, so LivenessStep imports one path.
2
+ export { StyleAbsFill, FRESH_FACE_TIMEOUT_MS, INSTRUCTION_HEIGHT } from './constants';
3
+ export { LightingBanner } from './LightingBanner';
4
+ export { resolveGuidance } from './resolveGuidance';
5
+ export { ProgressDots } from './ProgressDots';
6
+ export { SelfiePreview } from './SelfiePreview';
7
+ export { useSelfieUpload } from './useSelfieUpload';
8
+ export { LivenessComplete, LivenessFailed } from './LivenessOutcome';
@@ -0,0 +1,27 @@
1
+ import {
2
+ lightingGuidanceText,
3
+ positionGuidanceText,
4
+ type useLiveness,
5
+ } from '../../liveness/useLiveness';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Which single line the step shows, and in what tone.
9
+ //
10
+ // Ordered by severity: a warning the user must act on outranks the current
11
+ // instruction, so "move closer" is never buried under "nod your head".
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function resolveGuidance(l: ReturnType<typeof useLiveness>): { text: string; tone: 'normal' | 'error' } {
15
+ if (l.multipleFaces) return { text: l.instruction, tone: 'error' };
16
+ // Lighting is shown in its own banner (below), not as the main instruction.
17
+ if (l.phase === 'positioning' && !l.faceDetected) {
18
+ return { text: 'Position your face in the circle', tone: 'normal' };
19
+ }
20
+ if (l.positionGuidance) return { text: positionGuidanceText(l.positionGuidance), tone: 'error' };
21
+ if (l.wrongGesture) return { text: 'Wrong gesture — follow the prompt', tone: 'error' };
22
+ return { text: l.instruction, tone: 'normal' };
23
+ }
24
+
25
+ // Numbered progress steps with connectors — mirrors the web/Flutter SDKs:
26
+ // passed = solid green + checkmark, active = green-outlined number, pending =
27
+ // grey number. `active` marks the current (completed-th) step as in-progress.