@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,171 @@
1
+ import {
2
+ verifyDocumentIdentity,
3
+ type DocumentGuidance,
4
+ type DocumentHint,
5
+ } from './documentIdentity';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Text-based auto-capture gate.
9
+ //
10
+ // Auto-capture rides on-device TEXT recognition — a KYC document is text-dense,
11
+ // and a check-digit-valid MRZ is an even stronger "framed & readable" signal.
12
+ // The recogniser is already in the build for the MRZ step, so this needs no new
13
+ // native code and works identically on both platforms.
14
+ //
15
+ // Three questions before firing:
16
+ //
17
+ // • IDENTITY — is this the document the user picked? (documentIdentity.ts)
18
+ // • FRAMING — is it presented like a document? Measured from the region the
19
+ // recognised text occupies, which is the only geometry text recognition
20
+ // gives us. It is a proxy for the document's extent, not its edges, so the
21
+ // thresholds are deliberately loose: they exist to reject text sprawling
22
+ // across the whole frame (a screen, a page of prose) or a document too far
23
+ // away to read — not to fine-tune composition.
24
+ // • STABILITY — held steady for a dwell, so the shot isn't taken mid-move.
25
+ // This is what stops the blurry captures that make auto-capture feel worse
26
+ // than a manual shutter.
27
+ //
28
+ // Pure logic (no camera or ML dependency) so the thresholds are unit-tested
29
+ // rather than tuned by trial on a device. Ported from the Flutter SDK's
30
+ // document_text_gate.dart — keep the two in step.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Normalised (0..1) bounding box of all recognised text. */
34
+ export interface TextBounds {
35
+ x: number;
36
+ y: number;
37
+ width: number;
38
+ height: number;
39
+ }
40
+
41
+ export interface TextGateOptions {
42
+ /** Minimum recognised text lines to treat the frame as showing a document. */
43
+ minLines?: number;
44
+ /** Smallest share of the frame the text region may cover. */
45
+ minTextArea?: number;
46
+ /** Largest share. Text over nearly the whole frame is a screen, not a card. */
47
+ maxTextArea?: number;
48
+ /** How far the text region's centre may sit from the frame's centre. */
49
+ maxOffCentre?: number;
50
+ /** How long the framing must hold before firing. */
51
+ dwellMs?: number;
52
+ }
53
+
54
+ export interface UpdateOptions {
55
+ country: string;
56
+ idType: string;
57
+ /** Which face is up. The back skips keyword identity — see documentIdentity. */
58
+ side?: 'front' | 'back';
59
+ bounds?: TextBounds | null;
60
+ requireMrz?: boolean;
61
+ hasValidMrz?: boolean;
62
+ mrzAlreadyCaptured?: boolean;
63
+ now?: number;
64
+ }
65
+
66
+ export class DocumentTextGate {
67
+ private readonly minLines: number;
68
+ private readonly minTextArea: number;
69
+ private readonly maxTextArea: number;
70
+ private readonly maxOffCentre: number;
71
+ private readonly dwellMs: number;
72
+
73
+ private heldSince: number | null = null;
74
+ private fired = false;
75
+
76
+ constructor(opts: TextGateOptions = {}) {
77
+ this.minLines = opts.minLines ?? 5;
78
+ this.minTextArea = opts.minTextArea ?? 0.06;
79
+ this.maxTextArea = opts.maxTextArea ?? 0.82;
80
+ this.maxOffCentre = opts.maxOffCentre ?? 0.3;
81
+ this.dwellMs = opts.dwellMs ?? 700;
82
+ }
83
+
84
+ get hasFired(): boolean {
85
+ return this.fired;
86
+ }
87
+
88
+ /** Progress through the stability dwell, 0..1 — drives the UI's scan ring. */
89
+ progress(now: number): number {
90
+ if (this.heldSince == null) return 0;
91
+ if (this.dwellMs <= 0) return 1;
92
+ return Math.min(1, Math.max(0, (now - this.heldSince) / this.dwellMs));
93
+ }
94
+
95
+ /** Feeds one frame's recognised `lines`. */
96
+ update(lines: string[], o: UpdateOptions): DocumentGuidance {
97
+ const now = o.now ?? Date.now();
98
+ if (this.fired) return { framing: 'ready', hint: 'captured' };
99
+
100
+ const identity = verifyDocumentIdentity(lines, {
101
+ country: o.country,
102
+ idType: o.idType,
103
+ side: o.side ?? 'front',
104
+ requireValidMrz: o.requireMrz ?? false,
105
+ hasValidMrz: o.hasValidMrz ?? false,
106
+ mrzAlreadyCaptured: o.mrzAlreadyCaptured ?? false,
107
+ });
108
+ if (!identity.identified) {
109
+ this.heldSince = null;
110
+ return {
111
+ framing: identity.hint === 'wrongDocument' ? 'wrongShape' : 'none',
112
+ hint: identity.hint,
113
+ };
114
+ }
115
+
116
+ // A check-digit-valid MRZ read from THIS frame is conclusive: only a travel
117
+ // document carries one, and it only validates when legible — so it proves
118
+ // identity and readability at once, and the shot need not wait out a dwell.
119
+ // (A merely STORED MRZ does not qualify — see `mrzAlreadyCaptured` — or
120
+ // every retake would fire on its first frame.)
121
+ if (o.hasValidMrz) {
122
+ this.fired = true;
123
+ return { framing: 'ready', hint: 'holdStill' };
124
+ }
125
+
126
+ const framingHint = this.framingProblem(o.bounds ?? null, lines.length, o.side ?? 'front');
127
+ if (framingHint) {
128
+ this.heldSince = null;
129
+ return { framing: 'adjust', hint: framingHint };
130
+ }
131
+
132
+ if (this.heldSince == null) this.heldSince = now;
133
+ if (now - this.heldSince >= this.dwellMs) {
134
+ this.fired = true;
135
+ return { framing: 'ready', hint: 'holdStill' };
136
+ }
137
+ return { framing: 'holding', hint: 'holdStill' };
138
+ }
139
+
140
+ /** Why the text region isn't acceptably framed, or null when it is. */
141
+ private framingProblem(
142
+ bounds: TextBounds | null,
143
+ lineCount: number,
144
+ side: 'front' | 'back',
145
+ ): DocumentHint | null {
146
+ // Card backs are text-SPARSE by design — a PVC's is mostly barcode with a
147
+ // few small print lines — so the front's density thresholds would hold the
148
+ // gate at "move closer" forever on a perfectly framed back. The rect
149
+ // detector (where present) still guards the geometry.
150
+ const minLines = side === 'back' ? Math.min(3, this.minLines) : this.minLines;
151
+ const minTextArea = side === 'back' ? Math.min(0.03, this.minTextArea) : this.minTextArea;
152
+ if (!bounds) {
153
+ // No geometry from this platform — fall back to text density alone.
154
+ return lineCount < minLines ? 'moveCloser' : null;
155
+ }
156
+ const area = Math.min(1, Math.max(0, bounds.width * bounds.height));
157
+ if (area < minTextArea) return 'moveCloser';
158
+ if (area > this.maxTextArea) return 'moveBack';
159
+
160
+ const offCentreX = Math.abs(bounds.x + bounds.width / 2 - 0.5);
161
+ const offCentreY = Math.abs(bounds.y + bounds.height / 2 - 0.5);
162
+ if (offCentreX > this.maxOffCentre || offCentreY > this.maxOffCentre) return 'centre';
163
+ return null;
164
+ }
165
+
166
+ /** Clears the latch so the next side (or a retake) starts a fresh dwell. */
167
+ reset(): void {
168
+ this.heldSince = null;
169
+ this.fired = false;
170
+ }
171
+ }
@@ -0,0 +1,57 @@
1
+ import type { DocumentHint } from './documentIdentity';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Capture hint copy.
5
+ //
6
+ // One instruction per rejection reason. Deliberately imperative and specific:
7
+ // a single message ("Align your ID within the frame") told the user nothing
8
+ // about WHICH way to move, and the commonest failure — holding the document
9
+ // too close — is the one people cannot guess, because the instinct on "it isn't
10
+ // reading" is to move nearer still.
11
+ //
12
+ // Copy is document-agnostic. `label` is the ID's own name so the same strings
13
+ // serve a passport, a card or a licence without special-casing any of them.
14
+ //
15
+ // Verbatim from the Flutter SDK's capture_hints.dart — keep the two identical.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export function documentHintText(hint: DocumentHint, label: string): string {
19
+ switch (hint) {
20
+ case 'searching':
21
+ return `Point the camera at your ${label}`;
22
+ case 'moreLight':
23
+ return 'Too dark — move somewhere brighter';
24
+ case 'wrongDocument':
25
+ return `That doesn't look like a ${label}`;
26
+ case 'showMrz':
27
+ return 'Include the code strip along the bottom of the page';
28
+ case 'moveCloser':
29
+ return 'Move closer';
30
+ case 'moveBack':
31
+ // The one users don't reach for on their own.
32
+ return `Move back a little — fit the whole ${label} in view`;
33
+ case 'centre':
34
+ return `Centre your ${label} in the frame`;
35
+ case 'holdStill':
36
+ return 'Hold still…';
37
+ case 'captured':
38
+ return 'Got it';
39
+ default:
40
+ return `Point the camera at your ${label}`;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * True when the hint is asking the user to change something, so the UI can
46
+ * give it more weight than the neutral prompts.
47
+ */
48
+ export function documentHintIsAction(hint: DocumentHint): boolean {
49
+ return (
50
+ hint === 'moreLight' ||
51
+ hint === 'wrongDocument' ||
52
+ hint === 'showMrz' ||
53
+ hint === 'moveCloser' ||
54
+ hint === 'moveBack' ||
55
+ hint === 'centre'
56
+ );
57
+ }
@@ -0,0 +1,24 @@
1
+ // Auto-capture: the pure decision logic behind "shoot now".
2
+ //
3
+ // Barrel-only surface — screens import from here, never the internals, so the
4
+ // thresholds stay in one testable place.
5
+ export {
6
+ DOCUMENT_SIGNALS,
7
+ countSignalHits,
8
+ detectDocumentType,
9
+ hasDocumentSignals,
10
+ hasMrzLines,
11
+ type DocumentTypeMatch,
12
+ } from './documentSignals';
13
+ export {
14
+ documentCarriesMrz,
15
+ verifyDocumentIdentity,
16
+ type DocumentFraming,
17
+ type DocumentGuidance,
18
+ type DocumentHint,
19
+ type DocumentIdentityResult,
20
+ } from './documentIdentity';
21
+ export { DocumentTextGate, type TextBounds, type TextGateOptions } from './documentTextGate';
22
+ export { documentHintText, documentHintIsAction } from './hints';
23
+ export { useAutoCapture, type AutoCaptureState } from './useAutoCapture';
24
+ export { hasRectDetector, rectFramingProblem, type RectGateOptions } from './rectDetector';
@@ -0,0 +1,84 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+ import type { BoxedHybridObject } from 'react-native-nitro-modules';
3
+ import type { Frame } from 'react-native-vision-camera';
4
+
5
+ import type { DetectedRect, MyazaRectDetector } from '../specs/MyazaRectDetector.nitro';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Document EDGE detection — the geometry half of auto-capture.
9
+ //
10
+ // iOS only in practice: Apple Vision has a rectangle detector, ML Kit does not,
11
+ // and the Android implementation reports nothing found. Everything here is
12
+ // therefore written to degrade to "no opinion" rather than to block, so a
13
+ // platform without geometry captures exactly as it did before.
14
+ //
15
+ // Geometry NEVER decides alone. An aspect ratio cannot tell a passport from a
16
+ // driver's licence, so a capture that fired on shape would shoot the wrong
17
+ // document — the text gate is what establishes identity, and this only says
18
+ // whether what's there is presented like a document.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Resolved LAZILY and re-tried until it appears.
23
+ *
24
+ * Creating it at module load fails silently and permanently when this module
25
+ * evaluates before Nitro's registry is populated (Fast Refresh does this): the
26
+ * reference stays null for the session and the geometry gate silently becomes a
27
+ * no-op, which is indistinguishable from a platform that has no detector.
28
+ *
29
+ * The BOXED handle is what the worklet unboxes, so it is created alongside on
30
+ * the JS thread — a worklet cannot construct one itself.
31
+ */
32
+ function create(): MyazaRectDetector | null {
33
+ try {
34
+ return NitroModules.hasHybridObject('MyazaRectDetector')
35
+ ? NitroModules.createHybridObject<MyazaRectDetector>('MyazaRectDetector')
36
+ : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // EAGER, and it has to be — the worklet captures `boxed` at creation, so a null
43
+ // there is permanent on the camera thread and every frame reports no rectangle.
44
+ let detector: MyazaRectDetector | null = create();
45
+ // `let`, not `const`: if the eager create above ran before Nitro's registry was
46
+ // populated, `resolve()` re-creates AND re-boxes. Leaving this const meant a
47
+ // late resolution produced a working JS-side module with a null worklet handle
48
+ // — the camera thread stayed blind while everything looked fine from JS.
49
+ let boxed: BoxedHybridObject<MyazaRectDetector> | null = detector ? NitroModules.box(detector) : null;
50
+
51
+ function resolve(): MyazaRectDetector | null {
52
+ if (!detector) {
53
+ detector = create();
54
+ if (detector) boxed = NitroModules.box(detector);
55
+ }
56
+ return detector;
57
+ }
58
+
59
+ /**
60
+ * Whether this build can measure document edges at all.
61
+ *
62
+ * Also the WARM-UP: calling it resolves the module on the JS thread so the
63
+ * boxed handle exists before the first frame reaches the worklet.
64
+ */
65
+ export function hasRectDetector(): boolean {
66
+ return resolve() !== null;
67
+ }
68
+
69
+ const NOT_FOUND: DetectedRect = {
70
+ found: false, x: 0, y: 0, width: 0, height: 0, aspect: 0, confidence: 0,
71
+ };
72
+
73
+ /** Measure the document-like rectangle in a frame. A WORKLET. */
74
+ export function detectRectOnFrame(frame: Frame): DetectedRect {
75
+ 'worklet';
76
+ if (boxed == null) return NOT_FOUND;
77
+ try {
78
+ return boxed.unbox().detectRect(frame);
79
+ } catch {
80
+ return NOT_FOUND;
81
+ }
82
+ }
83
+
84
+ export { rectFramingProblem, type RectGateOptions, type MeasuredRect } from './rectGate';
@@ -0,0 +1,91 @@
1
+ import type { DocumentHint } from './documentIdentity';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Judging a detected document rectangle — the PURE half of edge detection.
5
+ //
6
+ // Split from rectDetector.ts on purpose: that module binds the native detector
7
+ // and so drags react-native-nitro-modules into whatever imports it. The
8
+ // thresholds below are the part worth testing, and they must be reachable in
9
+ // CI with no device and no native build.
10
+ //
11
+ // Geometry NEVER decides alone. An aspect ratio cannot tell a passport from a
12
+ // driver's licence, so a capture that fired on shape would shoot the wrong
13
+ // document — the text gate establishes identity; this only says whether what is
14
+ // there is presented like a document.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** The measured box, as the native detector reports it (normalised 0..1). */
18
+ export interface MeasuredRect {
19
+ found: boolean;
20
+ x: number;
21
+ y: number;
22
+ width: number;
23
+ height: number;
24
+ aspect: number;
25
+ confidence: number;
26
+ }
27
+
28
+ export interface RectGateOptions {
29
+ /** Expected width ÷ height: 1.586 for ID cards, 1.42 for a passport page. */
30
+ expectedAspect: number;
31
+ /** How far the observed aspect may stray, as a fraction of the expected. */
32
+ aspectTolerance?: number;
33
+ /** Smallest share of the frame the document may cover. */
34
+ minArea?: number;
35
+ /** Largest share — beyond this the corners are being cut off. */
36
+ maxArea?: number;
37
+ /** Clearance every edge must keep from the frame border. */
38
+ minMargin?: number;
39
+ }
40
+
41
+ /**
42
+ * Judge a detected rectangle. Returns the framing complaint, or null when the
43
+ * shape is acceptable — and null ALSO when nothing was detected, because
44
+ * "no geometry" must not read as "badly framed" on a platform that cannot
45
+ * measure. The text gate still has to agree either way.
46
+ */
47
+ export function rectFramingProblem(
48
+ rect: MeasuredRect | null,
49
+ {
50
+ expectedAspect,
51
+ aspectTolerance = 0.35,
52
+ minArea = 0.12,
53
+ maxArea = 0.92,
54
+ minMargin = 0.01,
55
+ }: RectGateOptions,
56
+ ): DocumentHint | null {
57
+ if (!rect || !rect.found) return null;
58
+
59
+ // SHAPE. The detector will happily segment a book, a receipt or a laptop;
60
+ // aspect is what tells an ID document apart from them.
61
+ //
62
+ // Compared against BOTH orientations: a document held in landscape and one
63
+ // held in portrait are the same document, and the detector reports whatever
64
+ // it saw. Rejecting the rotated case would refuse a perfectly good frame.
65
+ const observed = rect.aspect;
66
+ const alt = observed > 0 ? 1 / observed : 0;
67
+ const matches =
68
+ Math.abs(observed - expectedAspect) <= expectedAspect * aspectTolerance ||
69
+ Math.abs(alt - expectedAspect) <= expectedAspect * aspectTolerance;
70
+ if (!matches) return 'wrongDocument';
71
+
72
+ // SIZE. Big enough that the crop keeps the detail OCR and the MRZ need, but
73
+ // not so big that it is overflowing the frame.
74
+ const area = rect.width * rect.height;
75
+ if (area < minArea) return 'moveCloser';
76
+ if (area > maxArea) return 'moveBack';
77
+
78
+ // MARGIN. Every edge clear of the border, so all four corners are in shot. A
79
+ // document cropped flush to an edge loses characters, and OCR reads edges
80
+ // worst.
81
+ if (
82
+ rect.x < minMargin ||
83
+ rect.y < minMargin ||
84
+ rect.x + rect.width > 1 - minMargin ||
85
+ rect.y + rect.height > 1 - minMargin
86
+ ) {
87
+ return 'moveBack';
88
+ }
89
+
90
+ return null;
91
+ }
@@ -0,0 +1,198 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useFrameOutput } from 'react-native-vision-camera';
3
+ import { runOnJS } from 'react-native-worklets';
4
+
5
+ import { hasTextRecognizer, recognizeAllLines } from '../mrz/textRecognizer';
6
+ import { extractMrz } from '../mrz/extract';
7
+ import type { MrzScan } from '../mrz/parse';
8
+ import { DocumentTextGate } from './documentTextGate';
9
+ import { documentCarriesMrz, type DocumentGuidance } from './documentIdentity';
10
+ import { detectRectOnFrame, hasRectDetector } from './rectDetector';
11
+ import { rectFramingProblem } from './rectGate';
12
+ import type { DetectedRect } from '../specs/MyazaRectDetector.nitro';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Auto-capture: shoot when the document is identified, framed and still.
16
+ //
17
+ // Detection is an ACCELERATOR, never the only route — the manual shutter stays
18
+ // live throughout, and a build without the native recogniser simply never fires
19
+ // this, leaving the manual flow exactly as it was.
20
+ //
21
+ // Reading text per frame also banks the MRZ ahead of the shutter, so the chip
22
+ // step needs no second scan of the same passport. That is the main reason the
23
+ // gate reads text rather than just measuring edges.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** How often a frame is actually recognised. OCR is far slower than 30fps. */
27
+ const DETECT_INTERVAL_MS = 250;
28
+
29
+ export interface AutoCaptureOptions {
30
+ /** Off while capturing/uploading, on a preview, or when there is no camera. */
31
+ enabled: boolean;
32
+ country: string;
33
+ idType: string;
34
+ /**
35
+ * Which face is up. The back of a card carries none of the front's identity
36
+ * keywords (a PVC back is barcode and small print), so the gate must not
37
+ * demand them there — the front capture moments earlier established what
38
+ * document this is.
39
+ */
40
+ side?: 'front' | 'back';
41
+ /** The chip step wants the MRZ, so hold out for a check-digit-valid read. */
42
+ requireMrz?: boolean;
43
+ /** An MRZ is already stored from an earlier frame or a previous side. */
44
+ mrzAlreadyCaptured?: boolean;
45
+ /** Called once per fresh MRZ read — bank it for the chip step. */
46
+ onMrz?: (scan: MrzScan) => void;
47
+ /** Called once when the gate decides to shoot. */
48
+ onFire: () => void;
49
+ /** Expected width ÷ height, for the geometry check where it is available. */
50
+ guideAspect?: number;
51
+ /**
52
+ * Changing this starts a fresh dwell.
53
+ *
54
+ * Load-bearing for two-sided documents: the front and the back are the same
55
+ * `idType`, and the viewfinder does not remount between them — so without a
56
+ * key that actually changes, the gate stays LATCHED from the front and the
57
+ * back never auto-captures. Pass the side (or anything else that identifies
58
+ * "this is a new thing to look at").
59
+ */
60
+ resetKey?: string;
61
+ }
62
+
63
+ export interface AutoCaptureState {
64
+ /** Attach to `<Camera outputs={[...]}>`. */
65
+ frameOutput: ReturnType<typeof useFrameOutput>;
66
+ guidance: DocumentGuidance;
67
+ /** 0..1 through the stability dwell — drives the scan ring. */
68
+ progress: number;
69
+ /** Whether a check-digit-valid MRZ has been read this session. */
70
+ hasMrz: boolean;
71
+ /** Start a fresh dwell (new side, or a retake). */
72
+ reset: () => void;
73
+ }
74
+
75
+ const INITIAL: DocumentGuidance = { framing: 'none', hint: 'searching' };
76
+
77
+ export function useAutoCapture({
78
+ enabled,
79
+ country,
80
+ idType,
81
+ side = 'front',
82
+ requireMrz = false,
83
+ mrzAlreadyCaptured = false,
84
+ onMrz,
85
+ onFire,
86
+ guideAspect = 1.586,
87
+ resetKey,
88
+ }: AutoCaptureOptions): AutoCaptureState {
89
+ // RESOLVE FIRST, synchronously, before `useFrameOutput` below builds the
90
+ // worklet. Both modules hand the worklet a BOXED handle that only the JS
91
+ // thread can create, and the worklet captures that handle when it is CREATED
92
+ // — so a handle still unresolved at that moment is null on the camera thread
93
+ // forever, and every frame reports "no text, no rectangle".
94
+ //
95
+ // Module-import time is too early (Nitro's registry may not be populated) and
96
+ // a useEffect is too late (it runs after the worklet exists). The component
97
+ // body is the one place that is both.
98
+ hasTextRecognizer();
99
+ hasRectDetector();
100
+
101
+ const gate = useMemo(() => new DocumentTextGate(), []);
102
+ const [guidance, setGuidance] = useState<DocumentGuidance>(INITIAL);
103
+ const [progress, setProgress] = useState(0);
104
+ const [hasMrz, setHasMrz] = useState(false);
105
+
106
+ const lastDetectRef = useRef(0);
107
+ const firedRef = useRef(false);
108
+ // Refs, not deps: the worklet callback is created once, and re-creating it on
109
+ // every guidance change would tear down the frame output mid-stream.
110
+ const optsRef = useRef({ enabled, country, idType, side, requireMrz, mrzAlreadyCaptured, onMrz, onFire, guideAspect });
111
+ optsRef.current = { enabled, country, idType, side, requireMrz, mrzAlreadyCaptured, onMrz, onFire, guideAspect };
112
+
113
+ // Deliberately does NOT clear `hasMrz`: the MRZ belongs to the document, not
114
+ // to the side being photographed, and clearing it would make the chip step
115
+ // ask for a strip that has already been read.
116
+ const reset = useCallback(() => {
117
+ gate.reset();
118
+ firedRef.current = false;
119
+ lastDetectRef.current = 0;
120
+ setGuidance(INITIAL);
121
+ setProgress(0);
122
+ }, [gate]);
123
+
124
+ // A new side is a new document face: the previous side's dwell and verdict
125
+ // must not carry over, or the back would fire on the front's decision — and
126
+ // worse, the gate's `fired` latch would mean it never fires at all.
127
+ // `idType` alone is NOT enough: both sides of a card share it.
128
+ useEffect(() => {
129
+ reset();
130
+ }, [idType, resetKey, reset]);
131
+
132
+ const handleFrame = useCallback((lines: string[], rect: DetectedRect) => {
133
+ const o = optsRef.current;
134
+ if (!o.enabled || firedRef.current) return;
135
+
136
+ const now = Date.now();
137
+ if (now - lastDetectRef.current < DETECT_INTERVAL_MS) return;
138
+ lastDetectRef.current = now;
139
+
140
+ // Bank the MRZ whenever one validates — this is what spares the user
141
+ // scanning the same passport again at the chip step.
142
+ let freshMrz = false;
143
+ const scan = extractMrz(lines);
144
+ if (scan) {
145
+ freshMrz = true;
146
+ setHasMrz(true);
147
+ o.onMrz?.(scan);
148
+ }
149
+
150
+ // GEOMETRY first, where the platform has it. A shape complaint is
151
+ // actionable on its own ("move back"), and letting it through would mean
152
+ // running out a stability dwell on a document whose corners are cut off.
153
+ // Absent a detector this is null and the text gate decides alone.
154
+ const shapeProblem = rectFramingProblem(rect, { expectedAspect: o.guideAspect });
155
+ if (shapeProblem) {
156
+ setGuidance({ framing: 'adjust', hint: shapeProblem });
157
+ setProgress(0);
158
+ return;
159
+ }
160
+
161
+ const verdict = gate.update(lines, {
162
+ country: o.country,
163
+ idType: o.idType,
164
+ side: o.side,
165
+ // The MRZ is a FRONT-side key; the back of a card has no strip to wait for.
166
+ requireMrz: o.side !== 'back' && (o.requireMrz || documentCarriesMrz(o.idType)),
167
+ hasValidMrz: freshMrz,
168
+ mrzAlreadyCaptured: o.mrzAlreadyCaptured,
169
+ now,
170
+ });
171
+ setGuidance(verdict);
172
+ setProgress(gate.progress(now));
173
+
174
+ if (verdict.framing === 'ready' && !firedRef.current) {
175
+ firedRef.current = true;
176
+ o.onFire();
177
+ }
178
+ }, [gate]);
179
+
180
+ const frameOutput = useFrameOutput({
181
+ pixelFormat: 'yuv',
182
+ // Recognition runs far longer than a frame interval, so skip whatever
183
+ // arrives mid-pass rather than queueing it late.
184
+ dropFramesWhileBusy: true,
185
+ onFrame: (frame) => {
186
+ 'worklet';
187
+ const lines = recognizeAllLines(frame);
188
+ const rect = detectRectOnFrame(frame);
189
+ frame.dispose();
190
+ runOnJS(handleFrame)(lines, rect);
191
+ },
192
+ onFrameDropped: () => {
193
+ 'worklet';
194
+ },
195
+ });
196
+
197
+ return { frameOutput, guidance, progress, hasMrz, reset };
198
+ }