@myazahq/kyc-sdk-react-native 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +111 -8
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/AndroidManifest.xml +14 -4
  4. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaEmrtd.kt +358 -0
  5. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +88 -1
  6. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaRectDetector.kt +34 -0
  7. package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +118 -0
  8. package/android/src/main/java/com/margelo/nitro/myazakyc/Jp2Decoder.kt +117 -0
  9. package/app.plugin.js +102 -13
  10. package/ios/HybridMyazaEmrtd.swift +447 -0
  11. package/ios/HybridMyazaFaceDetector.swift +104 -2
  12. package/ios/HybridMyazaRectDetector.swift +103 -0
  13. package/ios/HybridMyazaTextRecognizer.swift +111 -0
  14. package/nitrogen/generated/.gitattributes +1 -0
  15. package/nitrogen/generated/android/KycSdkReactNative+autolinking.cmake +87 -0
  16. package/nitrogen/generated/android/KycSdkReactNative+autolinking.gradle +27 -0
  17. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.cpp +102 -0
  18. package/nitrogen/generated/android/KycSdkReactNativeOnLoad.hpp +34 -0
  19. package/nitrogen/generated/android/c++/JDetectedRect.hpp +81 -0
  20. package/nitrogen/generated/android/c++/JEmrtdApduResponse.hpp +61 -0
  21. package/nitrogen/generated/android/c++/JEmrtdTagInfo.hpp +61 -0
  22. package/nitrogen/generated/android/c++/JFaceResult.hpp +113 -0
  23. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.cpp +176 -0
  24. package/nitrogen/generated/android/c++/JHybridMyazaEmrtdSpec.hpp +75 -0
  25. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +60 -0
  26. package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +63 -0
  27. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.cpp +60 -0
  28. package/nitrogen/generated/android/c++/JHybridMyazaRectDetectorSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +80 -0
  30. package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +64 -0
  31. package/nitrogen/generated/android/c++/JTextResult.hpp +76 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/DetectedRect.kt +81 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdApduResponse.kt +56 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/EmrtdTagInfo.kt +56 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/FaceResult.kt +121 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaEmrtdSpec.kt +103 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +55 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaRectDetectorSpec.kt +55 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +60 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/KycSdkReactNativeOnLoad.kt +35 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/TextResult.kt +51 -0
  42. package/nitrogen/generated/ios/KycSdkReactNative+autolinking.rb +62 -0
  43. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +143 -0
  44. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +423 -0
  45. package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Umbrella.hpp +81 -0
  46. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.mm +57 -0
  47. package/nitrogen/generated/ios/KycSdkReactNativeAutolinking.swift +62 -0
  48. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.cpp +11 -0
  49. package/nitrogen/generated/ios/c++/HybridMyazaEmrtdSpecSwift.hpp +184 -0
  50. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +87 -0
  52. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridMyazaRectDetectorSpecSwift.hpp +87 -0
  54. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +98 -0
  56. package/nitrogen/generated/ios/swift/DetectedRect.swift +59 -0
  57. package/nitrogen/generated/ios/swift/EmrtdApduResponse.swift +34 -0
  58. package/nitrogen/generated/ios/swift/EmrtdTagInfo.swift +34 -0
  59. package/nitrogen/generated/ios/swift/FaceResult.swift +99 -0
  60. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  61. package/nitrogen/generated/ios/swift/Func_void_EmrtdApduResponse.swift +46 -0
  62. package/nitrogen/generated/ios/swift/Func_void_EmrtdTagInfo.swift +46 -0
  63. package/nitrogen/generated/ios/swift/Func_void_TextResult.swift +46 -0
  64. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  65. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  66. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec.swift +67 -0
  67. package/nitrogen/generated/ios/swift/HybridMyazaEmrtdSpec_cxx.swift +317 -0
  68. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +56 -0
  69. package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +143 -0
  70. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec.swift +56 -0
  71. package/nitrogen/generated/ios/swift/HybridMyazaRectDetectorSpec_cxx.swift +143 -0
  72. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +57 -0
  73. package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +162 -0
  74. package/nitrogen/generated/ios/swift/TextResult.swift +35 -0
  75. package/nitrogen/generated/shared/c++/DetectedRect.hpp +107 -0
  76. package/nitrogen/generated/shared/c++/EmrtdApduResponse.hpp +87 -0
  77. package/nitrogen/generated/shared/c++/EmrtdTagInfo.hpp +87 -0
  78. package/nitrogen/generated/shared/c++/FaceResult.hpp +139 -0
  79. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.cpp +33 -0
  80. package/nitrogen/generated/shared/c++/HybridMyazaEmrtdSpec.hpp +80 -0
  81. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +21 -0
  82. package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +67 -0
  83. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.cpp +21 -0
  84. package/nitrogen/generated/shared/c++/HybridMyazaRectDetectorSpec.hpp +67 -0
  85. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +22 -0
  86. package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +70 -0
  87. package/nitrogen/generated/shared/c++/TextResult.hpp +84 -0
  88. package/package.json +16 -4
  89. package/src/MyazaKYC.tsx +115 -22
  90. package/src/capture/documentIdentity.ts +153 -0
  91. package/src/capture/documentSignals.ts +177 -0
  92. package/src/capture/documentTextGate.ts +171 -0
  93. package/src/capture/hints.ts +57 -0
  94. package/src/capture/index.ts +24 -0
  95. package/src/capture/rectDetector.ts +84 -0
  96. package/src/capture/rectGate.ts +91 -0
  97. package/src/capture/useAutoCapture.ts +198 -0
  98. package/src/components/CameraViewfinder.tsx +226 -70
  99. package/src/components/ContactCodeEntry.tsx +139 -0
  100. package/src/components/CountryField.tsx +87 -0
  101. package/src/components/CountryFlag.tsx +15 -15
  102. package/src/components/DialCodePicker.tsx +186 -0
  103. package/src/components/DocumentCropper.tsx +1 -1
  104. package/src/components/DocumentReview.tsx +142 -0
  105. package/src/components/DocumentReviewSide.tsx +135 -0
  106. package/src/components/DocumentReviewZoom.tsx +119 -0
  107. package/src/components/ExpiryCountdown.tsx +52 -0
  108. package/src/components/FlashOverlay.tsx +156 -0
  109. package/src/components/Icon.tsx +67 -1
  110. package/src/components/KycFlow.tsx +66 -94
  111. package/src/components/KycSheet.tsx +148 -18
  112. package/src/components/MediaSourceSheet.tsx +163 -0
  113. package/src/components/MyazaButton.tsx +14 -2
  114. package/src/components/MyazaDateField.tsx +240 -0
  115. package/src/components/MyazaInput.tsx +73 -26
  116. package/src/components/MyazaSelect.tsx +283 -0
  117. package/src/components/MyazaWordmark.tsx +33 -0
  118. package/src/components/OptionRow.tsx +132 -0
  119. package/src/components/PhoneNumberInput.tsx +149 -0
  120. package/src/components/PoweredBy.tsx +89 -0
  121. package/src/components/ReadyPrimer.tsx +149 -0
  122. package/src/components/StepView.tsx +91 -0
  123. package/src/components/Typography.tsx +18 -3
  124. package/src/components/VerifiedNotice.tsx +38 -0
  125. package/src/components/WhatsAppIcon.tsx +29 -0
  126. package/src/components/WorkflowGate.tsx +128 -0
  127. package/src/components/brand-font.ts +113 -0
  128. package/src/components/flashHoleGeometry.ts +35 -0
  129. package/src/components/flow/FatalConfigError.tsx +56 -0
  130. package/src/components/fonts.ts +2 -19
  131. package/src/components/readyPrimerContent.ts +52 -0
  132. package/src/components/runtime.tsx +38 -43
  133. package/src/components/stepHeaderMeta.tsx +87 -0
  134. package/src/components/theme-provider.tsx +89 -0
  135. package/src/components/useWorkflowMount.ts +116 -0
  136. package/src/components/viewfinder/CardGuide.tsx +60 -0
  137. package/src/components/viewfinder/DocumentGhost.tsx +149 -0
  138. package/src/components/viewfinder/ImmersiveBottomBar.tsx +172 -0
  139. package/src/components/viewfinder/ImmersiveControls.tsx +127 -0
  140. package/src/components/viewfinder/ImmersiveGuide.tsx +174 -0
  141. package/src/components/viewfinder/ImmersiveOverlay.tsx +79 -0
  142. package/src/components/viewfinder/ViewfinderControls.tsx +154 -0
  143. package/src/config/brand.ts +97 -0
  144. package/src/config/business.ts +179 -0
  145. package/src/config/businessSteps.ts +105 -0
  146. package/src/config/contact.ts +159 -0
  147. package/src/config/countryNames.g.ts +254 -0
  148. package/src/config/currencyFlags.ts +144 -0
  149. package/src/config/font-resolve.ts +78 -0
  150. package/src/config/idTypes.ts +96 -1
  151. package/src/config/keyPeople.ts +152 -0
  152. package/src/config/phone.ts +39 -0
  153. package/src/config/proofOfAddress.ts +75 -0
  154. package/src/config/questionnaire.ts +153 -0
  155. package/src/config/regions.ts +118 -0
  156. package/src/config/registrationHint.ts +87 -0
  157. package/src/config/stepOrder.ts +118 -0
  158. package/src/config/theme.ts +38 -2
  159. package/src/config/workflowMerge.ts +124 -0
  160. package/src/emrtd/bac.ts +128 -0
  161. package/src/emrtd/bytes.ts +127 -0
  162. package/src/emrtd/crypto.ts +163 -0
  163. package/src/emrtd/der.ts +192 -0
  164. package/src/emrtd/dg2.ts +111 -0
  165. package/src/emrtd/files.ts +215 -0
  166. package/src/emrtd/index.ts +30 -0
  167. package/src/emrtd/mrzKey.ts +54 -0
  168. package/src/emrtd/native.ts +79 -0
  169. package/src/emrtd/optionalRead.ts +42 -0
  170. package/src/emrtd/read.ts +224 -0
  171. package/src/emrtd/retry.ts +98 -0
  172. package/src/emrtd/secureMessaging.ts +178 -0
  173. package/src/emrtd/session.ts +200 -0
  174. package/src/emrtd/stages.ts +122 -0
  175. package/src/index.ts +110 -0
  176. package/src/lib/calendar.ts +71 -0
  177. package/src/lib/use-keyboard-inset.ts +34 -0
  178. package/src/liveness/challengeManager.ts +27 -1
  179. package/src/liveness/faceContinuity.ts +153 -0
  180. package/src/liveness/flashDetector.ts +145 -0
  181. package/src/liveness/flashReadyGate.ts +106 -0
  182. package/src/liveness/flashRunner.ts +118 -0
  183. package/src/liveness/integritySignals.ts +60 -0
  184. package/src/liveness/livenessState.ts +139 -0
  185. package/src/liveness/speech.ts +19 -1
  186. package/src/liveness/types.ts +48 -1
  187. package/src/liveness/useFlashPhase.ts +78 -0
  188. package/src/liveness/useLiveness.ts +260 -75
  189. package/src/liveness/visionCameraFaceDetector.ts +14 -0
  190. package/src/mrz/extract.ts +101 -0
  191. package/src/mrz/parse.ts +178 -0
  192. package/src/mrz/textRecognizer.ts +149 -0
  193. package/src/screens/ApplicantRoleStep.tsx +331 -0
  194. package/src/screens/BusinessDetailsStep.tsx +212 -0
  195. package/src/screens/BusinessDocumentSlot.tsx +167 -0
  196. package/src/screens/BusinessDocumentsStep.tsx +213 -0
  197. package/src/screens/BusinessKeyPeopleStep.tsx +151 -0
  198. package/src/screens/BusinessKeyPersonRow.tsx +170 -0
  199. package/src/screens/CompanyInfoFields.tsx +83 -0
  200. package/src/screens/ConsentStep.tsx +68 -68
  201. package/src/screens/ContactChannelChoice.tsx +133 -0
  202. package/src/screens/ContactCodeStep.tsx +130 -0
  203. package/src/screens/ContactDestinationField.tsx +70 -0
  204. package/src/screens/ContactFooterNote.tsx +34 -0
  205. package/src/screens/ContactVerificationStep.tsx +199 -0
  206. package/src/screens/ContactVerifiedPanel.tsx +36 -0
  207. package/src/screens/CountrySelectStep.tsx +210 -0
  208. package/src/screens/DocumentCaptureStep.tsx +156 -132
  209. package/src/screens/IdInputStep.tsx +13 -5
  210. package/src/screens/IdTypeStep.tsx +74 -33
  211. package/src/screens/KeyPeopleInviteLinks.tsx +239 -0
  212. package/src/screens/LivenessStep.tsx +155 -247
  213. package/src/screens/MrzScanView.tsx +173 -0
  214. package/src/screens/NfcStep.tsx +290 -0
  215. package/src/screens/ProofOfAddressStep.tsx +238 -0
  216. package/src/screens/QuestionnaireField.tsx +219 -0
  217. package/src/screens/QuestionnaireMoneyField.tsx +134 -0
  218. package/src/screens/QuestionnaireStep.tsx +76 -0
  219. package/src/screens/SubmittedStep.tsx +5 -0
  220. package/src/screens/consent/model.ts +109 -0
  221. package/src/screens/document/CameraPhase.tsx +150 -0
  222. package/src/screens/document/RequiredPill.tsx +60 -0
  223. package/src/screens/liveness/LightingBanner.tsx +50 -0
  224. package/src/screens/liveness/LivenessOutcome.tsx +109 -0
  225. package/src/screens/liveness/ProgressDots.tsx +67 -0
  226. package/src/screens/liveness/SelfiePreview.tsx +39 -0
  227. package/src/screens/liveness/constants.ts +31 -0
  228. package/src/screens/liveness/index.ts +8 -0
  229. package/src/screens/liveness/resolveGuidance.ts +27 -0
  230. package/src/screens/liveness/useSelfieUpload.ts +107 -0
  231. package/src/screens/nfc/NfcMrzPrompt.tsx +40 -0
  232. package/src/screens/nfc/NfcReadBody.tsx +63 -0
  233. package/src/screens/nfc/NfcReadProgress.tsx +96 -0
  234. package/src/screens/nfc/NfcScanIllustration.tsx +244 -0
  235. package/src/screens/nfc/NfcScannedSummary.tsx +74 -0
  236. package/src/screens/nfc/NfcSuccessPanel.tsx +162 -0
  237. package/src/screens/nfc/availability.ts +73 -0
  238. package/src/screens/nfc/readErrorMessage.ts +93 -0
  239. package/src/screens/nfc/useChipPortrait.ts +58 -0
  240. package/src/screens/nfc/useNfcAvailability.ts +82 -0
  241. package/src/screens/useFlashHole.ts +105 -0
  242. package/src/screens/useFlashSequence.ts +96 -0
  243. package/src/screens/usePoaAttach.ts +121 -0
  244. package/src/services/api-types.ts +164 -0
  245. package/src/services/api-verify-types.ts +99 -0
  246. package/src/services/api.ts +72 -102
  247. package/src/services/cardCrop.ts +25 -10
  248. package/src/services/contactErrors.ts +43 -0
  249. package/src/services/deviceMetadata.ts +1 -1
  250. package/src/services/documentPicker.ts +20 -0
  251. package/src/services/fingerprint-sources.ts +129 -0
  252. package/src/services/fingerprint.ts +101 -0
  253. package/src/services/haptics.ts +38 -0
  254. package/src/services/mediaCompress.ts +7 -2
  255. package/src/services/resolveUrl.ts +2 -2
  256. package/src/services/workflowGate.ts +138 -0
  257. package/src/specs/MyazaEmrtd.nitro.ts +114 -0
  258. package/src/specs/MyazaFaceDetector.nitro.ts +10 -0
  259. package/src/specs/MyazaRectDetector.nitro.ts +50 -0
  260. package/src/specs/MyazaTextRecognizer.nitro.ts +48 -0
  261. package/src/store/derive.ts +225 -0
  262. package/src/store/kycStore.ts +250 -149
  263. package/src/store/state.ts +253 -0
  264. package/src/store/submit.ts +101 -0
  265. package/src/store/submitApplicant.ts +81 -0
  266. package/src/types/appearance.ts +113 -0
  267. package/src/types/business.ts +131 -0
  268. package/src/types/config.ts +150 -124
  269. package/src/types/id-types.ts +57 -0
  270. package/src/types/verification.ts +5 -0
  271. package/src/types/workflow.ts +141 -0
  272. package/src/utils/amountFormat.ts +73 -0
  273. package/src/utils/tokens.ts +4 -2
  274. package/src/__tests__/cardCrop.test.ts +0 -39
  275. package/src/__tests__/deviceMetadata.test.ts +0 -34
  276. package/src/__tests__/errors.test.ts +0 -34
  277. package/src/__tests__/flow.test.ts +0 -61
  278. package/src/__tests__/gestureDetector.test.ts +0 -37
  279. package/src/__tests__/liveness.test.ts +0 -112
  280. package/src/__tests__/resolveUrl.test.ts +0 -64
  281. package/src/__tests__/validators.test.ts +0 -38
@@ -1,57 +1,34 @@
1
- import type { KYCSubmission, KYCError } from './verification';
2
-
3
1
  // ---------------------------------------------------------------------------
4
- // Supported countries & ID types (identical set to the web + Flutter SDKs)
2
+ // The SDK's public configuration.
3
+ //
4
+ // The country/ID matrix, the presentation types and the workflow-driven blocks
5
+ // live in their own modules (200-line rule) and are re-exported here, so
6
+ // `types/config` remains the single import for consumers.
5
7
  // ---------------------------------------------------------------------------
6
8
 
7
- export type SupportedCountry = 'NG' | 'GH' | 'KE' | 'ZA' | 'CI';
8
-
9
- export type NigeriaIdType = 'bvn' | 'bvn-premium' | 'nin' | 'vnin' | 'tax-id' | 'passport' | 'drivers-license' | 'pvc';
10
- export type GhanaIdType = 'ghana-card' | 'voters' | 'drivers-license' | 'ssnit' | 'passport';
11
- export type KenyaIdType = 'national-id' | 'passport';
12
- export type SouthAfricaIdType = 'national-id';
13
- export type IvoryCoastIdType = 'cni' | 'residence-card';
14
-
15
- export type IdType =
16
- | NigeriaIdType
17
- | GhanaIdType
18
- | KenyaIdType
19
- | SouthAfricaIdType
20
- | IvoryCoastIdType;
21
-
22
- /** Maps a country code to the ID types available in that country. */
23
- export type IdTypeForCountry<C extends SupportedCountry> =
24
- C extends 'NG' ? NigeriaIdType :
25
- C extends 'GH' ? GhanaIdType :
26
- C extends 'KE' ? KenyaIdType :
27
- C extends 'ZA' ? SouthAfricaIdType :
28
- C extends 'CI' ? IvoryCoastIdType :
29
- never;
30
-
31
- export interface IdTypeDefinition {
32
- key: IdType;
33
- label: string;
34
- /**
35
- * What the user actually types when it differs from the ID's name — e.g.
36
- * Tax ID lookups are keyed off the person's NIN, so the input asks for a NIN.
37
- */
38
- inputLabel?: string;
39
- digits?: number;
40
- pattern?: RegExp;
41
- /** Whether this ID type requires photographing/uploading a physical document. */
42
- requiresDocumentCapture: boolean;
43
- /**
44
- * How many sides of the document need to be scanned. Only present when
45
- * `requiresDocumentCapture` is true.
46
- * - `front_only` — single scan (passports, data-page only)
47
- * - `front_and_back` — both sides required
48
- */
49
- scanSides?: 'front_only' | 'front_and_back';
50
- }
51
-
52
- export type IdTypesByCountry = {
53
- [K in SupportedCountry]: readonly IdTypeDefinition[];
54
- };
9
+ import type { KYCSubmission, KYCError } from './verification';
10
+ import type { IdType, IdTypeForCountry, SupportedCountry } from './id-types';
11
+ import type {
12
+ KYCAppearance,
13
+ KYCConsentContent,
14
+ KYCSuccessContent,
15
+ VoiceGuidanceOption,
16
+ } from './appearance';
17
+ import type { SubjectType, WorkflowBusinessConfig } from './business';
18
+ import type {
19
+ EmailVerificationConfig,
20
+ LivenessMode,
21
+ NfcConfig,
22
+ PhoneVerificationConfig,
23
+ ProofOfAddressConfig,
24
+ QuestionnaireConfig,
25
+ WorkflowCountry,
26
+ } from './workflow';
27
+
28
+ export type * from './workflow';
29
+ export type * from './appearance';
30
+ export type * from './id-types';
31
+ export type * from './business';
55
32
 
56
33
  // ---------------------------------------------------------------------------
57
34
  // KYC flow steps
@@ -59,82 +36,27 @@ export type IdTypesByCountry = {
59
36
 
60
37
  export type KYCStep =
61
38
  | 'consent'
39
+ // Contact-verification OTP steps — right after consent (a cheap pre-filter
40
+ // before capture/registry spend). Present when the workflow enables them.
41
+ | 'email-verification'
42
+ | 'phone-verification'
43
+ | 'country-select'
62
44
  | 'id-type'
63
45
  | 'id-input'
64
46
  | 'document-capture'
47
+ // eMRTD chip read — a real step here, unlike the web SDK where it exists only
48
+ // as a builder-preview screen (Web NFC can't do ISO-DEP).
49
+ | 'nfc'
50
+ // KYB application steps — present only when the workflow configures them.
51
+ | 'business-details'
52
+ | 'business-key-people'
53
+ | 'business-documents'
54
+ | 'applicant-role'
65
55
  | 'liveness'
56
+ | 'proof-of-address'
57
+ | 'questionnaire'
66
58
  | 'submitted';
67
59
 
68
- // ---------------------------------------------------------------------------
69
- // Appearance / theming
70
- // ---------------------------------------------------------------------------
71
-
72
- export interface KYCAppearance {
73
- /** Brand color — drives buttons, selected states, focus rings. */
74
- primaryColor?: string;
75
- /** Text/icon color rendered on top of `primaryColor` (e.g. button labels). */
76
- primaryTextColor?: string;
77
- /** Accent color for subtle hover/active surfaces. */
78
- accentColor?: string;
79
- /** Modal/sheet background color. */
80
- backgroundColor?: string;
81
- /** Elevated surface color for cards/panels. */
82
- surfaceColor?: string;
83
- /** Border + input outline color. */
84
- borderColor?: string;
85
- /** Primary text color. */
86
- textColor?: string;
87
- companyName?: string;
88
- /**
89
- * Logo to show in the flow.
90
- * - An image URL renders that logo.
91
- * - The literal `'default'` renders the org's own logo from the server config
92
- * response (falls back to the built-in shield if the org has none set).
93
- * - Omitted renders the built-in shield badge.
94
- */
95
- logo?: string;
96
- /** Initial light/dark mode. Applied on mount; the theme toggle can flip it. */
97
- theme?: 'light' | 'dark';
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Consent / success screen content
102
- // ---------------------------------------------------------------------------
103
-
104
- export interface KYCConsentContent {
105
- /** Consent heading. Supports `{firstName}` / `{lastName}` tokens. */
106
- title?: string;
107
- /** Sub-text under the heading. Same tokens. */
108
- description?: string;
109
- }
110
-
111
- export interface KYCSuccessContent {
112
- /** Success heading. Supports `{firstName}` / `{lastName}` tokens. */
113
- title?: string;
114
- /** Sub-text under the heading. Same tokens. */
115
- description?: string;
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Voice guidance (spoken liveness instructions — TTS output only, no mic)
120
- // ---------------------------------------------------------------------------
121
-
122
- /**
123
- * Configuration for the spoken liveness instructions. TTS **output** for
124
- * accessibility — it never records audio, so no microphone permission is
125
- * involved. An object (not a bare boolean) so a `language` can be added later
126
- * without a breaking change.
127
- */
128
- export interface VoiceGuidanceConfig {
129
- /** Whether spoken guidance plays. Default `true`. */
130
- enabled?: boolean;
131
- /** BCP-47 voice tag (e.g. `'en-US'`, `'fr-FR'`). Default `'en-US'`. */
132
- language?: string;
133
- }
134
-
135
- /** Accepts a bare boolean for ergonomics or the full {@link VoiceGuidanceConfig}. */
136
- export type VoiceGuidanceOption = boolean | VoiceGuidanceConfig;
137
-
138
60
  // ---------------------------------------------------------------------------
139
61
  // Client-side SDK config (MyazaKYC.show() / useMyazaKYC options)
140
62
  // ---------------------------------------------------------------------------
@@ -156,17 +78,57 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
156
78
  */
157
79
  devUrl?: string;
158
80
 
159
- /** Two-letter country code. */
160
- country: C;
81
+ /**
82
+ * A published workflow to run (`wf_…`). The flow is resolved BEFORE the SDK
83
+ * mounts and its config wins over the props below — so an org can change the
84
+ * flow's shape in the dashboard builder without a redeploy. Runtime data
85
+ * (`userId`, `userData`, `metadata`, callbacks) always stays yours.
86
+ *
87
+ * A workflow supplies `country`, so it may be omitted alongside this.
88
+ */
89
+ workflowId?: string;
90
+
91
+ /**
92
+ * KYB only: the mapped applicant workflow's id (business.applicant.workflowId,
93
+ * resolved server-side). Set by the workflow gate after overlaying its
94
+ * capture template; stamped on the applicant's own submission so the server
95
+ * applies that workflow's gates, pricing and decision graph. Internal —
96
+ * never set this yourself.
97
+ */
98
+ applicantWorkflowId?: string;
99
+
100
+ /**
101
+ * Two-letter (ISO-2) country code. Required unless {@link workflowId} is set
102
+ * (the flow carries the country) or {@link subjectType} is `'business'` (the
103
+ * business block carries its own registry country). When both are present the
104
+ * flow's country wins.
105
+ */
106
+ country?: C;
161
107
 
162
108
  /** Subset of ID types to offer. Only types valid for the country are accepted. */
163
109
  idTypes?: IdTypeForCountry<C>[];
164
110
 
111
+ /**
112
+ * The org's own reference for the person being verified (e.g. your internal
113
+ * user id). It is **not** matched during verification — it becomes
114
+ * `Entity.externalUserId` at the KYC seam, so repeat checks of the same user
115
+ * collapse onto one entity and you can correlate results back to your record.
116
+ * Optional; when omitted the server falls back to the provider record id.
117
+ */
118
+ userId?: string;
119
+
165
120
  /** Pre-populated user data. Fields provided here won't be collected again. */
166
121
  userData?: {
167
122
  firstName?: string;
168
123
  lastName?: string;
169
124
  dateOfBirth?: string;
125
+ /**
126
+ * The business's display name, for `{businessName}` tokens in consent
127
+ * copy on KYB flows. Registration details aren't collected until after
128
+ * consent, so this resolves only when the integrator passes it in —
129
+ * mirroring the web SDK.
130
+ */
131
+ businessName?: string;
170
132
  };
171
133
 
172
134
  /** Enable the live-selfie capture step. */
@@ -185,6 +147,52 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
185
147
  /** Enable liveness detection during selfie capture. Default `true`. */
186
148
  enableLiveness?: boolean;
187
149
 
150
+ /**
151
+ * Which liveness method runs: randomized gestures (default), the screen-
152
+ * reflection flash, or both. Workflow-driven.
153
+ */
154
+ livenessMode?: LivenessMode;
155
+
156
+ /** Colours in the flash sequence (2–5, default 4). Flash modes only. */
157
+ flashSequenceLength?: number;
158
+
159
+ // ── Workflow-driven blocks ────────────────────────────────────────────────
160
+ // Authored in the dashboard's workflow builder, not usually in consumer code.
161
+ // A resolved flow always wins over a prop of the same name.
162
+
163
+ /** What this flow verifies. Absent = 'individual' (classic KYC). */
164
+ subjectType?: SubjectType;
165
+
166
+ /** KYB registry configuration. Required when `subjectType` is 'business'. */
167
+ business?: WorkflowBusinessConfig;
168
+
169
+ /**
170
+ * Multi-region: every country this flow serves. More than one inserts a
171
+ * country-select step between consent and ID type.
172
+ */
173
+ countries?: WorkflowCountry[];
174
+
175
+ /** Email OTP possession check, right after consent. */
176
+ emailVerification?: EmailVerificationConfig;
177
+
178
+ /** Phone OTP possession check, right after consent / email. */
179
+ phoneVerification?: PhoneVerificationConfig;
180
+
181
+ /** Proof-of-address document check, after capture. */
182
+ proofOfAddress?: ProofOfAddressConfig;
183
+
184
+ /** Compliance declarations asked just before submission. */
185
+ questionnaire?: QuestionnaireConfig;
186
+
187
+ /** eMRTD chip read for chip-capable documents. */
188
+ nfc?: NfcConfig;
189
+
190
+ /** Collect device + IP fraud signals. Default `true`. */
191
+ deviceIntelligence?: boolean;
192
+
193
+ /** Refuse to run on a desktop/laptop. Default `false`. */
194
+ requireMobileDevice?: boolean;
195
+
188
196
  /**
189
197
  * Spoken liveness instructions (accessibility). `true`/omitted = on,
190
198
  * `false` = off, or a {@link VoiceGuidanceConfig}. TTS output only — no
@@ -213,7 +221,11 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
213
221
  /** Override the success (submitted) screen copy. */
214
222
  success?: KYCSuccessContent;
215
223
 
216
- /** Arbitrary metadata forwarded with every verification request. */
224
+ /**
225
+ * Arbitrary, free-form metadata forwarded verbatim with every verification
226
+ * request. Nothing here is required or interpreted by the SDK/server — use
227
+ * {@link userId} for the user reference, not a `userId` key in here.
228
+ */
217
229
  metadata?: Record<string, string>;
218
230
 
219
231
  // Callbacks
@@ -232,6 +244,20 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
232
244
  onError?: (error: KYCError) => void;
233
245
  }
234
246
 
247
+ /**
248
+ * The config the FLOW runs on, as opposed to the config a consumer writes.
249
+ *
250
+ * `country` is optional on the public surface because a `workflowId` supplies
251
+ * it — but by the time anything mounts, the workflow gate has resolved one, and
252
+ * every screen from ID-type onward needs it. Making that guarantee a type keeps
253
+ * the "is it there yet?" question at the single place that answers it (the
254
+ * gate) instead of in every screen.
255
+ */
256
+ export type ResolvedKYCConfig<C extends SupportedCountry = SupportedCountry> = Omit<
257
+ MyazaKYCConfig<C>,
258
+ 'country'
259
+ > & { country: C };
260
+
235
261
  // ---------------------------------------------------------------------------
236
262
  // useMyazaKYC() hook return type
237
263
  // ---------------------------------------------------------------------------
@@ -0,0 +1,57 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The country and ID-type matrix.
3
+ //
4
+ // Split from config.ts (200-line rule). These exist for compile-time ergonomics
5
+ // — autocomplete and per-country narrowing — not enforcement: the org's actual
6
+ // grants live server-side, and Global Documents means any ISO country can be
7
+ // offered. Adding a country here does not grant it.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export type SupportedCountry = 'NG' | 'GH' | 'KE' | 'ZA' | 'CI';
11
+
12
+ export type NigeriaIdType = 'bvn' | 'bvn-premium' | 'nin' | 'vnin' | 'tax-id' | 'passport' | 'drivers-license' | 'pvc';
13
+ export type GhanaIdType = 'ghana-card' | 'voters' | 'drivers-license' | 'ssnit' | 'passport';
14
+ export type KenyaIdType = 'national-id' | 'passport';
15
+ export type SouthAfricaIdType = 'national-id';
16
+ export type IvoryCoastIdType = 'cni' | 'residence-card';
17
+
18
+ export type IdType =
19
+ | NigeriaIdType
20
+ | GhanaIdType
21
+ | KenyaIdType
22
+ | SouthAfricaIdType
23
+ | IvoryCoastIdType;
24
+
25
+ /** Maps a country code to the ID types available in that country. */
26
+ export type IdTypeForCountry<C extends SupportedCountry> =
27
+ C extends 'NG' ? NigeriaIdType :
28
+ C extends 'GH' ? GhanaIdType :
29
+ C extends 'KE' ? KenyaIdType :
30
+ C extends 'ZA' ? SouthAfricaIdType :
31
+ C extends 'CI' ? IvoryCoastIdType :
32
+ never;
33
+
34
+ export interface IdTypeDefinition {
35
+ key: IdType;
36
+ label: string;
37
+ /**
38
+ * What the user actually types when it differs from the ID's name — e.g.
39
+ * Tax ID lookups are keyed off the person's NIN, so the input asks for a NIN.
40
+ */
41
+ inputLabel?: string;
42
+ digits?: number;
43
+ pattern?: RegExp;
44
+ /** Whether this ID type requires photographing/uploading a physical document. */
45
+ requiresDocumentCapture: boolean;
46
+ /**
47
+ * How many sides of the document need to be scanned. Only present when
48
+ * `requiresDocumentCapture` is true.
49
+ * - `front_only` — single scan (passports, data-page only)
50
+ * - `front_and_back` — both sides required
51
+ */
52
+ scanSides?: 'front_only' | 'front_and_back';
53
+ }
54
+
55
+ export type IdTypesByCountry = {
56
+ [K in SupportedCountry]: readonly IdTypeDefinition[];
57
+ };
@@ -34,6 +34,11 @@ export interface KYCSubmission {
34
34
  export type KYCErrorCode =
35
35
  | 'network_error'
36
36
  | 'invalid_api_key'
37
+ // A `workflowId` that is unknown, unpublished, archived, or belongs to
38
+ // another org/environment. Blocking — the flow never mounts.
39
+ | 'invalid_workflow'
40
+ // The props are unusable as given (e.g. no `country` and no `workflowId`).
41
+ | 'invalid_config'
37
42
  | 'insufficient_credits'
38
43
  | 'upload_failed'
39
44
  | 'camera_permission_denied'
@@ -0,0 +1,141 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Workflow-driven configuration blocks.
3
+ //
4
+ // These are the parts of the flow an org authors in the dashboard's workflow
5
+ // builder rather than in consumer code: extra steps (contact OTP, proof of
6
+ // address, questionnaire, NFC), the KYB subject, and multi-region. They arrive
7
+ // either as props or from `GET /api/kyc/workflows/:id`, and the flow always
8
+ // wins over props (see `mergeWorkflowConfig`).
9
+ //
10
+ // Shapes mirror the web SDK's `types/config.ts` + `types/business.ts` exactly —
11
+ // they are the server's published contract, so the three SDKs must not drift.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ // ── Proof of Address ────────────────────────────────────────────────────────
15
+
16
+ export type PoaDocumentType = 'utility_bill' | 'bank_statement' | 'tenancy_agreement' | 'other';
17
+
18
+ export interface ProofOfAddressConfig {
19
+ /** Adds the Proof of Address step (after capture, before the questionnaire). */
20
+ enabled?: boolean;
21
+ /** Accepted document kinds (absent = all). */
22
+ documentTypes?: PoaDocumentType[];
23
+ /** Custom label for the 'other' kind (absent = "Other document"). */
24
+ otherLabel?: string;
25
+ /** Recency window the server checks the document date against (default 90). */
26
+ maxAgeDays?: number;
27
+ }
28
+
29
+ // ── NFC chip read (eMRTD) ───────────────────────────────────────────────────
30
+
31
+ export interface NfcConfig {
32
+ /** Adds the chip-read step. */
33
+ enabled?: boolean;
34
+ /** Which chip-capable IDs run it, as "CC/idType" keys (absent = all). */
35
+ idTypes?: string[];
36
+ /**
37
+ * Show a manual skip so someone who can't complete the read can proceed.
38
+ * Devices with no NFC radio auto-skip regardless — this is the escape hatch
39
+ * on NFC-capable phones.
40
+ */
41
+ allowSkip?: boolean;
42
+ /** Match the selfie against the authenticated chip portrait (default on). */
43
+ facialMatch?: boolean;
44
+ }
45
+
46
+ // ── Contact verification (email / phone OTP) ────────────────────────────────
47
+
48
+ /** Which code field the SDK renders — the org picks this in the builder. */
49
+ export type OtpInputStyle = 'segmented' | 'text';
50
+
51
+ export interface EmailVerificationConfig {
52
+ /** Adds the email OTP step (right after consent). */
53
+ enabled?: boolean;
54
+ /**
55
+ * Whether a verified email is required to proceed (default true when
56
+ * enabled). `false` shows a "skip for now" affordance and the server accepts
57
+ * a submission without the proof.
58
+ */
59
+ required?: boolean;
60
+ /** Digits in the code (4–8; default 6). Drives the OTP-input slots. */
61
+ codeLength?: number;
62
+ /** Wrong entries allowed per code before it's dead (1–5; default 3). */
63
+ maxAttempts?: number;
64
+ inputStyle?: OtpInputStyle;
65
+ }
66
+
67
+ export interface PhoneVerificationConfig {
68
+ /** Adds the phone OTP step (after consent / email verification). */
69
+ enabled?: boolean;
70
+ required?: boolean;
71
+ codeLength?: number;
72
+ maxAttempts?: number;
73
+ inputStyle?: OtpInputStyle;
74
+ /** Offered delivery channels (default ['sms']). */
75
+ channels?: Array<'sms' | 'whatsapp'>;
76
+ /** Default dial-code country (falls back to the flow's country). */
77
+ defaultCountry?: string;
78
+ }
79
+
80
+ // ── Questionnaire (compliance declarations) ─────────────────────────────────
81
+
82
+ export interface QuestionnaireFieldOption {
83
+ value: string;
84
+ label: string;
85
+ }
86
+
87
+ export interface QuestionnaireField {
88
+ /** Stable snake_case key — also the webhook/decisioning field name. */
89
+ key: string;
90
+ label: string;
91
+ /**
92
+ * 'money' = amount + currency. The answer stores `<key>` (number, 2dp) and a
93
+ * `<key>_currency` companion (ISO code).
94
+ */
95
+ type: 'text' | 'number' | 'money' | 'select' | 'multiselect' | 'boolean' | 'date';
96
+ required?: boolean;
97
+ placeholder?: string;
98
+ helpText?: string;
99
+ options?: QuestionnaireFieldOption[];
100
+ min?: number;
101
+ max?: number;
102
+ /** money only: allowed ISO currency codes; the first is the default. */
103
+ currencies?: string[];
104
+ }
105
+
106
+ export interface QuestionnaireConfig {
107
+ /**
108
+ * Off switch for the step. Omitted/true = shown; false = skipped even though
109
+ * the questions remain configured (the builder's Questionnaire toggle).
110
+ */
111
+ enabled?: boolean;
112
+ title?: string;
113
+ description?: string;
114
+ fields: QuestionnaireField[];
115
+ }
116
+
117
+ export type QuestionnaireAnswerValue = string | number | boolean | string[];
118
+
119
+ // ── Multi-region ────────────────────────────────────────────────────────────
120
+
121
+ export interface WorkflowIdOption {
122
+ govDbCheck?: boolean;
123
+ documentIntelligence?: boolean;
124
+ facialMatch?: boolean;
125
+ }
126
+
127
+ export interface WorkflowCountry {
128
+ /** ISO-3166 alpha-2. */
129
+ country: string;
130
+ /** Offered IDs for this country (absent/empty = every granted ID). */
131
+ idTypes?: string[];
132
+ /** Per-ID validation toggles — restrict-only; they never widen a grant. */
133
+ idOptions?: Record<string, WorkflowIdOption>;
134
+ govDbCheck?: boolean;
135
+ documentIntelligence?: boolean;
136
+ }
137
+
138
+ // ── Liveness ────────────────────────────────────────────────────────────────
139
+
140
+ /** Gestures (default), screen-reflection flash, or both. */
141
+ export type LivenessMode = 'gestures' | 'flash' | 'both';
@@ -0,0 +1,73 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Amount formatting
3
+ //
4
+ // Groups thousands as the user types (250000 → 250,000) and caps the decimals,
5
+ // matching the Flutter SDK's AmountInputFormatter and the web SDK's money
6
+ // input. Long figures are the norm for the expected-volume question in
7
+ // low-denomination currencies, and an ungrouped "250000" is genuinely hard to
8
+ // read back.
9
+ //
10
+ // The DISPLAY is grouped; the ANSWER stays a number. Storing the grouped string
11
+ // would fail validation — `Number("250,000")` is NaN — and would reach the
12
+ // server as text where the decision graph expects something comparable.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Keeps digits plus at most one '.', truncating past `decimalDigits`. */
16
+ function clean(input: string, decimalDigits: number): string {
17
+ let out = '';
18
+ let seenDot = false;
19
+ let decimals = 0;
20
+
21
+ for (const ch of input) {
22
+ if (ch === '.') {
23
+ // Integer-only: everything past the point is fractional, so DROP the
24
+ // rest. Merely skipping the '.' would splice "1234.99" into "123499".
25
+ if (decimalDigits === 0) break;
26
+ if (seenDot) continue;
27
+ seenDot = true;
28
+ out += ch;
29
+ continue;
30
+ }
31
+ if (ch < '0' || ch > '9') continue;
32
+ if (seenDot) {
33
+ if (decimals >= decimalDigits) continue;
34
+ decimals += 1;
35
+ }
36
+ out += ch;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function group(digits: string): string {
42
+ if (digits.length <= 3) return digits;
43
+ const lead = digits.length % 3;
44
+ const parts: string[] = [];
45
+ let i = 0;
46
+ if (lead > 0) {
47
+ parts.push(digits.slice(0, lead));
48
+ i = lead;
49
+ }
50
+ while (i < digits.length) {
51
+ parts.push(digits.slice(i, i + 3));
52
+ i += 3;
53
+ }
54
+ return parts.join(',');
55
+ }
56
+
57
+ /** "250000.5" → "250,000.5". Non-numeric characters are dropped. */
58
+ export function formatGroupedAmount(input: string, decimalDigits = 2): string {
59
+ const cleaned = clean(input, decimalDigits);
60
+ if (cleaned === '') return '';
61
+ const dot = cleaned.indexOf('.');
62
+ const whole = dot === -1 ? cleaned : cleaned.slice(0, dot);
63
+ const fraction = dot === -1 ? '' : cleaned.slice(dot);
64
+ return group(whole) + fraction;
65
+ }
66
+
67
+ /** Numeric value of a grouped amount string ("250,000.50" → 250000.5). */
68
+ export function parseGroupedAmount(text: string): number | null {
69
+ const raw = text.replace(/,/g, '').trim();
70
+ if (raw === '' || raw === '.') return null;
71
+ const n = Number(raw);
72
+ return Number.isFinite(n) ? n : null;
73
+ }
@@ -1,10 +1,12 @@
1
1
  import type { MyazaKYCConfig } from '../types/config';
2
2
 
3
- // Replaces {firstName} / {lastName} tokens with values from userData (or ''),
4
- // mirroring the Flutter SDK's `_fillTokens` and the web SDK's token handling.
3
+ // Replaces {firstName} / {lastName} / {businessName} tokens with values from
4
+ // userData (or ''), mirroring the Flutter SDK's `_fillTokens` and the web
5
+ // SDK's token handling ({businessName} is the KYB consent-copy token).
5
6
  export function fillTokens(template: string, userData: MyazaKYCConfig['userData']): string {
6
7
  return template
7
8
  .replace(/\{firstName\}/g, userData?.firstName ?? '')
8
9
  .replace(/\{lastName\}/g, userData?.lastName ?? '')
10
+ .replace(/\{businessName\}/g, userData?.businessName ?? '')
9
11
  .trim();
10
12
  }
@@ -1,39 +0,0 @@
1
- import { cardCropRect } from '../services/cardCrop';
2
- import { CARD_GUIDE_ASPECT, PASSPORT_GUIDE_ASPECT } from '../config/idTypes';
3
-
4
- describe('cardCropRect', () => {
5
- it('produces a crop whose aspect equals the guide aspect', () => {
6
- const r = cardCropRect(4000, 3000, CARD_GUIDE_ASPECT);
7
- expect(r.width / r.height).toBeCloseTo(CARD_GUIDE_ASPECT, 3);
8
- });
9
-
10
- it('stays within the image bounds', () => {
11
- for (const [w, h] of [
12
- [4000, 3000], // landscape sensor
13
- [3000, 4000], // portrait sensor
14
- [1920, 1080], // 16:9
15
- ] as const) {
16
- const r = cardCropRect(w, h, CARD_GUIDE_ASPECT);
17
- expect(r.originX).toBeGreaterThanOrEqual(0);
18
- expect(r.originY).toBeGreaterThanOrEqual(0);
19
- expect(r.originX + r.width).toBeLessThanOrEqual(w + 0.01);
20
- expect(r.originY + r.height).toBeLessThanOrEqual(h + 0.01);
21
- }
22
- });
23
-
24
- it('centres the crop within the cover-fit 3:4 slice (landscape sensor)', () => {
25
- // 4000×3000 → visible slice is 2250×3000 centred at x=875.
26
- const r = cardCropRect(4000, 3000, CARD_GUIDE_ASPECT);
27
- const sliceCentreX = 875 + 2250 / 2;
28
- expect(r.originX + r.width / 2).toBeCloseTo(sliceCentreX, 3);
29
- expect(r.originY + r.height / 2).toBeCloseTo(3000 / 2, 3); // vertically centred (no shift)
30
- });
31
-
32
- it('gives passports a taller crop (smaller aspect) than ID cards', () => {
33
- const card = cardCropRect(4000, 3000, CARD_GUIDE_ASPECT);
34
- const passport = cardCropRect(4000, 3000, PASSPORT_GUIDE_ASPECT);
35
- expect(passport.height).toBeGreaterThan(card.height);
36
- expect(passport.width).toBeCloseTo(card.width, 3); // same 88% width
37
- expect(passport.width / passport.height).toBeCloseTo(PASSPORT_GUIDE_ASPECT, 3);
38
- });
39
- });