@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,139 @@
1
+ import type {
2
+ LivenessChallenge,
3
+ LivenessConfig,
4
+ LivenessFaceData,
5
+ LivenessFailureReason,
6
+ LivenessPhase,
7
+ } from './types';
8
+ import type { FlashResult } from './flashDetector';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // The liveness machine's vocabulary: its UI state, its options, its guidance
12
+ // strings and the two phase predicates the handlers branch on.
13
+ //
14
+ // Split out of useLiveness (200-line rule). Deliberately free of the hook, so
15
+ // the phase predicates below can be read — and reasoned about — without the
16
+ // state machine around them. They are load-bearing: `isSettled` decides where
17
+ // guidance is suppressed, and getting its scope wrong is what once left the
18
+ // flash and the capture without a multiple-face check.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export const INTEGRITY_FAILED_PATCH = {
22
+ phase: 'failed' as const,
23
+ failureReason: 'face_swap' as const,
24
+ instruction: 'Let\u2019s start over \u2014 please stay in frame.',
25
+ activeChallenge: null,
26
+ positionGuidance: null,
27
+ };
28
+
29
+ import { LivenessSpeaker } from './speech';
30
+ import type { VoiceGuidanceOption } from '../types/config';
31
+
32
+ export const CHALLENGE_PASSED_MS = 700; // brief "Great!" before the next challenge
33
+ export const POSITION_TOO_FAR = 'too_far';
34
+ export const POSITION_TOO_CLOSE = 'too_close';
35
+ export const MULTIPLE_FACES_GUIDANCE = 'Make sure only your face is visible';
36
+
37
+ export type PositionGuidance = typeof POSITION_TOO_FAR | typeof POSITION_TOO_CLOSE;
38
+ export type LightingGuidance = 'dark' | 'bright';
39
+
40
+ export interface LivenessUiState {
41
+ phase: LivenessPhase;
42
+ instruction: string;
43
+ activeChallenge: LivenessChallenge | null;
44
+ timeoutRemaining: number;
45
+ completedCount: number;
46
+ totalCount: number;
47
+ positionGuidance: PositionGuidance | null;
48
+ lightingGuidance: LightingGuidance | null;
49
+ multipleFaces: boolean;
50
+ wrongGesture: boolean;
51
+ faceDetected: boolean;
52
+ failureReason: LivenessFailureReason | null;
53
+ }
54
+
55
+ export interface UseLivenessOptions {
56
+ config?: Partial<LivenessConfig>;
57
+ voiceGuidance?: VoiceGuidanceOption;
58
+ /** Fires once when all challenges pass and the phase enters `capturing`. */
59
+ onReadyToCapture?: () => void;
60
+ /**
61
+ * Whether the step is actually on screen and the user has started.
62
+ *
63
+ * The machine is constructed while the readiness primer is still up — hooks
64
+ * cannot be called conditionally — so without this it announces "Position
65
+ * your face in the circle" to someone still reading the primer, before they
66
+ * have tapped anything. Guidance for a screen the user cannot see is worse
67
+ * than none: it is a voice from nowhere.
68
+ *
69
+ * Defaults true so a consumer that never sets it behaves as before.
70
+ */
71
+ announce?: boolean;
72
+ }
73
+
74
+ export interface UseLivenessReturn extends LivenessUiState {
75
+ /** Push a face frame into the machine (call from the frame processor via runOnJS). */
76
+ onFace: (data: LivenessFaceData) => void;
77
+ /** Push a "no face this frame" signal. */
78
+ onNoFace: () => void;
79
+ /**
80
+ * Resolves true once a face is detected AFTER the call, or false on timeout.
81
+ * The capture gate uses this so an empty frame is never photographed.
82
+ */
83
+ awaitFreshFace: (timeoutMs: number) => Promise<boolean>;
84
+ /** Feed live lighting quality (null = ok). */
85
+ setLighting: (guidance: LightingGuidance | null) => void;
86
+ /** Mark the selfie captured → phase `complete`. */
87
+ markComplete: () => void;
88
+ /** Restart the session with a fresh random challenge set. */
89
+ reset: () => void;
90
+ /** True while the machine wants a still captured (phase === 'capturing'). */
91
+ shouldCapture: boolean;
92
+ /** True while the screen should be running the flash sequence. */
93
+ shouldFlash: boolean;
94
+ /** Called by the screen when the flash sequence finishes. */
95
+ completeFlash: (result: FlashResult) => void;
96
+ /** The flash verdict, once run — carried into the submission as context. */
97
+ flashResult: FlashResult | null;
98
+ /** How many face discontinuities the continuity guard saw this session. */
99
+ faceGlitches: number;
100
+ /**
101
+ * True once the session can no longer vouch for who is in frame. The capture
102
+ * path reads this AFTER the still is taken: a substitution detected during
103
+ * `capturing` is recorded silently (so nothing re-lays out mid-capture) and
104
+ * acted on here.
105
+ */
106
+ integrityBroken: boolean;
107
+ /** Live read of the integrity flag — see `isCompromised` in the hook. */
108
+ isCompromised: () => boolean;
109
+ /** Surfaces a failure that was detected mid-capture and deliberately held. */
110
+ reportIntegrityFailure: () => void;
111
+ }
112
+
113
+ /** Position guidance text (spoken + shown), matching the Flutter SDK exactly. */
114
+ export function positionGuidanceText(g: PositionGuidance): string {
115
+ return g === POSITION_TOO_FAR ? 'Kindly move closer' : 'Kindly move further away';
116
+ }
117
+ /** Lighting warning banner text — the full Flutter strings (shown, not spoken). */
118
+ export function lightingGuidanceText(g: LightingGuidance): string {
119
+ return g === 'dark'
120
+ ? 'It looks dark here. Move to a brighter area or near a light source for better detection.'
121
+ : 'Too bright — reduce glare or move away from direct light for better detection.';
122
+ }
123
+
124
+ /**
125
+ * Phases during which the screen must NOT change layout.
126
+ *
127
+ * While capturing, a re-layout moves the subject mid-shutter. While flashing,
128
+ * the overlay's cutout is measured once when the flash starts, so shifting the
129
+ * preview underneath it leaves the hole in the wrong place — the exact bug the
130
+ * Flutter SDK hit. Guidance banners are also meaningless during a flash, since
131
+ * the screen is deliberately painting the "wrong" light.
132
+ */
133
+ export function isSettled(phase: LivenessPhase): boolean {
134
+ return phase === 'capturing' || phase === 'flash';
135
+ }
136
+
137
+ export function isTerminal(phase: LivenessPhase): boolean {
138
+ return phase === 'complete' || phase === 'failed';
139
+ }
@@ -30,11 +30,23 @@ export class LivenessSpeaker {
30
30
  private readonly enabled: boolean;
31
31
  private readonly language: string;
32
32
  private lastSpoken: string | null = null;
33
+ private spokeBefore = false;
33
34
 
34
35
  constructor(option: VoiceGuidanceOption | undefined) {
35
36
  const resolved = resolveVoiceGuidance(option);
36
37
  this.enabled = resolved.enabled;
37
38
  this.language = resolved.language;
39
+ if (this.enabled) {
40
+ // Warm the engine NOW, not at the first phrase. Android's TextToSpeech
41
+ // initialises asynchronously, and when the user is well-positioned from
42
+ // the start the first thing this session ever says is the first GESTURE
43
+ // COMMAND — which then races engine init and loses: exactly the phrase
44
+ // a user holding the phone at arm's length cannot afford to miss. Any
45
+ // expo-speech call triggers init; voices is the harmless one. By the
46
+ // time positioning settles the engine is ready and the command plays
47
+ // immediately. iOS initialises synchronously and ignores the warm-up.
48
+ void Speech.getAvailableVoicesAsync().catch(() => undefined);
49
+ }
38
50
  }
39
51
 
40
52
  /** Speaks `text` unless guidance is off or it equals the last phrase spoken. */
@@ -42,7 +54,13 @@ export class LivenessSpeaker {
42
54
  if (!this.enabled || !text || text === this.lastSpoken) return;
43
55
  this.lastSpoken = text;
44
56
  try {
45
- Speech.stop();
57
+ // stop() exists to cut off a STALE phrase so guidance stays current.
58
+ // Before anything has been spoken there is nothing to cut off — and on
59
+ // Android calling stop() against a still-initialising engine is exactly
60
+ // the poke that can eat the first queued utterance. Skip it until a
61
+ // phrase has actually been issued.
62
+ if (this.spokeBefore) Speech.stop();
63
+ this.spokeBefore = true;
46
64
  Speech.speak(text, { language: this.language });
47
65
  } catch {
48
66
  /* TTS is best-effort — a failure must never block the flow */
@@ -32,11 +32,25 @@ export type LivenessPhase =
32
32
  | 'positioning' // "Position your face in the circle"
33
33
  | 'challenge' // Active gesture challenge
34
34
  | 'challenge_passed' // Brief green flash
35
+ // Screen-reflection liveness: the display emits a randomised colour sequence
36
+ // and the camera watches the face reflect it. Runs as the FINAL challenge, so
37
+ // by the time it starts the face is already positioned and steady.
38
+ | 'flash'
35
39
  | 'capturing' // Auto-capturing selfie
36
40
  | 'complete' // All done — selfie review
37
41
  | 'failed'; // Timeout or face lost
38
42
 
39
- export type LivenessFailureReason = 'timeout' | 'face_lost' | 'no_camera' | 'load_error';
43
+ export type LivenessFailureReason =
44
+ | 'timeout'
45
+ | 'face_lost'
46
+ | 'no_camera'
47
+ | 'load_error'
48
+ /**
49
+ * A different face appeared mid-session. Liveness proves a live human
50
+ * performed the challenges; only continuity ties that human to the one being
51
+ * captured. Matches the web SDK's `face_swap`.
52
+ */
53
+ | 'face_swap';
40
54
 
41
55
  // ---------------------------------------------------------------------------
42
56
  // Configuration
@@ -47,12 +61,24 @@ export interface LivenessConfig {
47
61
  challengePool?: LivenessChallenge[];
48
62
  timeoutPerChallenge: number;
49
63
  positioningTimeout: number;
64
+ /**
65
+ * Which liveness method runs. 'gestures' (default) is the randomised head
66
+ * movements; 'flash' is screen reflection; 'both' runs gestures then flash,
67
+ * because each catches what the other misses — gestures defeat a static
68
+ * photo, flash defeats a replayed video.
69
+ */
70
+ mode?: LivenessMode;
71
+ /** Colours in the flash sequence (2–5, default 4). Flash modes only. */
72
+ flashSequenceLength?: number;
50
73
  }
51
74
 
75
+ export type LivenessMode = 'gestures' | 'flash' | 'both';
76
+
52
77
  export const DEFAULT_LIVENESS_CONFIG: LivenessConfig = {
53
78
  challengeCount: 2,
54
79
  timeoutPerChallenge: 8,
55
80
  positioningTimeout: 15,
81
+ mode: 'gestures',
56
82
  };
57
83
 
58
84
  // ---------------------------------------------------------------------------
@@ -84,6 +110,27 @@ export interface LivenessFaceData {
84
110
  faceCount: number;
85
111
  /** Mean frame luma (0–255). Feeds the low-light gate (<62 dark, >200 bright). */
86
112
  brightness: number;
113
+ /**
114
+ * Face centre in normalised frame coordinates (0–1), when the detector
115
+ * reports it. Together with {@link faceSizeRatio} this is what distinguishes
116
+ * "the same face moved" from "a different face appeared" — size alone cannot,
117
+ * since two people at the same distance measure alike.
118
+ */
119
+ faceCenterX?: number;
120
+ faceCenterY?: number;
121
+ /**
122
+ * ML Kit's per-face tracking id (Android only; absent on iOS, whose Vision
123
+ * framework has no cross-frame equivalent). When present a CHANGE is proof of
124
+ * a different face — which is why it supplements the geometric continuity
125
+ * guard rather than being relied on alone.
126
+ */
127
+ trackingId?: number;
128
+ /**
129
+ * Mean RGB of the face region (0–255 each), for flash liveness. Absent when
130
+ * the platform could not sample it — flash then degrades to inconclusive,
131
+ * which fails soft, rather than to a wrong verdict.
132
+ */
133
+ faceRgb?: readonly [number, number, number];
87
134
  }
88
135
 
89
136
  /** Convenience: average of both eye-open probabilities. */
@@ -0,0 +1,78 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import type { FlashResult } from './flashDetector';
4
+ import type { LivenessMode } from './types';
5
+ import type { LivenessUiState } from './useLiveness';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // The flash phase's own state.
9
+ //
10
+ // Split out of useLiveness (200-line rule). Everything here is a REF rather
11
+ // than state on purpose: the pending check is read from inside the challenge
12
+ // transition, and a stale closure there would either skip the flash entirely or
13
+ // run it twice.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface FlashPhase {
17
+ /** Whether the flash sequence still has to run. */
18
+ pending: () => boolean;
19
+ /** Called by the screen once the sequence finishes. */
20
+ complete: (result: FlashResult) => void;
21
+ /** The verdict, once run. */
22
+ result: () => FlashResult | null;
23
+ /** Face discontinuities seen this session. */
24
+ glitches: () => number;
25
+ recordGlitch: () => void;
26
+ /** Clears everything for a retry — including the colour sequence. */
27
+ reset: () => void;
28
+ }
29
+
30
+ export function useFlashPhase(
31
+ mode: LivenessMode | undefined,
32
+ setState: (updater: (s: LivenessUiState) => LivenessUiState) => void,
33
+ ): FlashPhase {
34
+ const doneRef = useRef(false);
35
+ const resultRef = useRef<FlashResult | null>(null);
36
+ const glitchesRef = useRef(0);
37
+
38
+ const pending = useCallback(
39
+ () => (mode === 'flash' || mode === 'both') && !doneRef.current,
40
+ [mode],
41
+ );
42
+
43
+ /**
44
+ * The result is carried into the submission as context, NOT used to fail the
45
+ * step: an inconclusive flash (sunlight, or a platform that cannot sample
46
+ * RGB) must not block a user, and the server re-analyses the recorded video
47
+ * regardless. A definite mismatch is what the server acts on.
48
+ */
49
+ const complete = useCallback(
50
+ (result: FlashResult) => {
51
+ doneRef.current = true;
52
+ resultRef.current = result;
53
+ setState((s) =>
54
+ s.phase === 'flash' ? { ...s, phase: 'capturing', instruction: 'Hold still…' } : s,
55
+ );
56
+ },
57
+ [setState],
58
+ );
59
+
60
+ const reset = useCallback(() => {
61
+ // A retry runs the whole check again, INCLUDING a fresh colour sequence —
62
+ // reusing the previous one would hand an attacker the answer.
63
+ doneRef.current = false;
64
+ resultRef.current = null;
65
+ glitchesRef.current = 0;
66
+ }, []);
67
+
68
+ return {
69
+ pending,
70
+ complete,
71
+ result: () => resultRef.current,
72
+ glitches: () => glitchesRef.current,
73
+ recordGlitch: () => {
74
+ glitchesRef.current += 1;
75
+ },
76
+ reset,
77
+ };
78
+ }