@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
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, Pressable, View } from 'react-native';
3
+ import Svg, { Circle } from 'react-native-svg';
4
+
5
+ import { radius, spacing } from '../../config/theme';
6
+ import { useTheme } from '../runtime';
7
+ import { MyazaText } from '../Typography';
8
+ import { Icon } from '../Icon';
9
+ import { CountryFlag } from '../CountryFlag';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Immersive capture chrome — 1:1 with the Flutter SDK's full-screen camera.
13
+ //
14
+ // Positions are deliberate and differ from the framed viewfinder:
15
+ // • the HINT sits at the BOTTOM, near the shutter the user is about to press,
16
+ // not at the top where it competes with the document itself;
17
+ // • the TORCH is bottom-right, in thumb reach on a phone held two-handed;
18
+ // • the side badge and document pill sit top, out of the frame's way.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const SCRIM = 'rgba(0,0,0,0.55)';
22
+
23
+ /** Round translucent control — back, torch. */
24
+ export function RoundControl({
25
+ icon,
26
+ label,
27
+ onPress,
28
+ active,
29
+ size = 48,
30
+ style,
31
+ }: {
32
+ icon: 'back' | 'lightbulb';
33
+ label: string;
34
+ onPress: () => void;
35
+ active?: boolean;
36
+ size?: number;
37
+ style: object;
38
+ }): React.ReactElement {
39
+ const { colors } = useTheme();
40
+ return (
41
+ <Pressable
42
+ onPress={onPress}
43
+ accessibilityRole="button"
44
+ accessibilityLabel={label}
45
+ style={{
46
+ position: 'absolute',
47
+ width: size,
48
+ height: size,
49
+ borderRadius: radius.full,
50
+ backgroundColor: active ? colors.primary : SCRIM,
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ ...style,
54
+ }}
55
+ >
56
+ <Icon name={icon} size={20} color="#FFFFFF" />
57
+ </Pressable>
58
+ );
59
+ }
60
+
61
+ /** "FRONT" / "BACK" — which face of the document is expected right now. */
62
+ export function SideBadge({ side, top }: { side: 'front' | 'back'; top: number }): React.ReactElement {
63
+ const { colors } = useTheme();
64
+ return (
65
+ <View
66
+ style={{
67
+ position: 'absolute',
68
+ top,
69
+ right: spacing.md,
70
+ backgroundColor: colors.primary,
71
+ borderRadius: radius.full,
72
+ paddingHorizontal: 16,
73
+ paddingVertical: 8,
74
+ }}
75
+ >
76
+ <MyazaText variant="bodySmall" color="#FFFFFF" style={{ fontWeight: '800', letterSpacing: 0.8 }}>
77
+ {side === 'back' ? 'BACK' : 'FRONT'}
78
+ </MyazaText>
79
+ </View>
80
+ );
81
+ }
82
+
83
+ /** Flag + document name, so the user can see WHICH document is expected. */
84
+ export function DocumentPill({
85
+ country,
86
+ label,
87
+ top,
88
+ }: {
89
+ country: string;
90
+ label: string;
91
+ top: number;
92
+ }): React.ReactElement {
93
+ return (
94
+ <View style={{ position: 'absolute', top, left: 0, right: 0, alignItems: 'center' }}>
95
+ <View
96
+ style={{
97
+ flexDirection: 'row',
98
+ alignItems: 'center',
99
+ backgroundColor: SCRIM,
100
+ borderRadius: radius.full,
101
+ borderWidth: 1,
102
+ borderColor: 'rgba(255,255,255,0.18)',
103
+ paddingHorizontal: 12,
104
+ paddingVertical: 7,
105
+ maxWidth: '80%',
106
+ }}
107
+ >
108
+ {/* The flag carries the country on its own — a name beside it is the
109
+ same fact twice, and it reads faster at a glance than text. */}
110
+ {country ? (
111
+ <>
112
+ <CountryFlag country={country} size={26} />
113
+ <View style={{ width: 9 }} />
114
+ </>
115
+ ) : null}
116
+ <MyazaText
117
+ variant="bodyMedium"
118
+ color="#FFFFFF"
119
+ numberOfLines={1}
120
+ style={{ fontWeight: '700', flexShrink: 1 }}
121
+ >
122
+ {label}
123
+ </MyazaText>
124
+ </View>
125
+ </View>
126
+ );
127
+ }
@@ -0,0 +1,174 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing } from 'react-native';
3
+ import Svg, { Defs, G, Line, Mask, Path, Rect } from 'react-native-svg';
4
+
5
+ import { useTheme } from '../runtime';
6
+ import { DocumentGhost } from './DocumentGhost';
7
+ import type { DocumentFraming } from '../../capture';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // The full-screen document guide.
11
+ //
12
+ // A 1:1 port of the Flutter SDK's immersive viewfinder frame: a dimmed
13
+ // surround, a rounded window at the document's own aspect, heavy corner
14
+ // brackets, a faint grid inside, and a scan line sweeping down it.
15
+ //
16
+ // The grid and the sweep are not decoration — they are what makes a dim,
17
+ // featureless preview read as "actively looking at something". Without them a
18
+ // user holding a passport still gets no feedback until the shutter fires.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Share of the screen width the guide occupies (Flutter uses ~88%). */
22
+ const GUIDE_WIDTH_FRACTION = 0.88;
23
+ /** Grid divisions inside the window. */
24
+ const GRID_COLS = 8;
25
+ const GRID_ROWS = 6;
26
+
27
+ export function ImmersiveGuide({
28
+ guideAspect,
29
+ primary,
30
+ width,
31
+ height,
32
+ framing,
33
+ showMrzBand,
34
+ }: {
35
+ guideAspect: number;
36
+ primary: string;
37
+ width: number;
38
+ height: number;
39
+ /** Auto-capture's verdict for the current frame. */
40
+ framing: DocumentFraming;
41
+ /** Draw the ghost's machine-readable band (passport data pages carry one). */
42
+ showMrzBand: boolean;
43
+ }): React.ReactElement {
44
+ const { colors } = useTheme();
45
+
46
+ // Each state answers a question the user is actually asking:
47
+ // searching → "is the camera even working?" → neutral, sweeping
48
+ // wrongShape/adjust → "what do I do?" → amber, motion stops
49
+ // holding/ready → "did it see it?" → green, locked
50
+ //
51
+ // Amber for "I can see something but it isn't right" is distinct from the
52
+ // neutral searching state on purpose: it tells the user to ACT rather than
53
+ // wait, which is the difference between a fixable framing error and a camera
54
+ // that looks frozen.
55
+ const locked = framing === 'holding' || framing === 'ready';
56
+ const needsAction = framing === 'wrongShape' || framing === 'adjust';
57
+ const accent = locked ? colors.success : needsAction ? colors.warning : primary;
58
+
59
+ // Only dim once confident: dimming while still searching makes a dark room
60
+ // impossible to aim in.
61
+ const scrim = locked ? 0.45 : needsAction ? 0.25 : 0.18;
62
+ const gw = width * GUIDE_WIDTH_FRACTION;
63
+ const gh = gw / guideAspect;
64
+ const gx = (width - gw) / 2;
65
+ const gy = (height - gh) / 2;
66
+ const corner = Math.min(gw, gh) * 0.16;
67
+ const r = 14;
68
+
69
+ // The sweep runs the height of the window and repeats. Native driver: it is a
70
+ // pure transform, and a JS-driven animation over a live camera preview
71
+ // stutters exactly when the user is trying to hold still.
72
+ const sweep = useRef(new Animated.Value(0)).current;
73
+ useEffect(() => {
74
+ const loop = Animated.loop(
75
+ Animated.timing(sweep, {
76
+ toValue: 1,
77
+ duration: 2600,
78
+ easing: Easing.inOut(Easing.ease),
79
+ useNativeDriver: true,
80
+ }),
81
+ );
82
+ if (locked) {
83
+ // Freeze once locked: continuing to sweep would imply we are still
84
+ // looking after we have decided.
85
+ sweep.stopAnimation();
86
+ return undefined;
87
+ }
88
+ loop.start();
89
+ return () => loop.stop();
90
+ }, [sweep, locked]);
91
+
92
+ return (
93
+ <>
94
+ <Svg style={{ position: 'absolute', inset: 0 }} width={width} height={height}>
95
+ <Defs>
96
+ <Mask id="guideWindow">
97
+ <Rect width={width} height={height} fill="white" />
98
+ <Rect x={gx} y={gy} width={gw} height={gh} rx={r} fill="black" />
99
+ </Mask>
100
+ </Defs>
101
+
102
+ {/* Everything outside the window is dimmed, so the document reads as
103
+ the lit subject even on a dark desk. */}
104
+ <Rect width={width} height={height} fill={`rgba(0,0,0,${scrim})`} mask="url(#guideWindow)" />
105
+
106
+ {/* Faint grid — alignment help that survives a low-contrast page. */}
107
+ <G opacity={0.28}>
108
+ {Array.from({ length: GRID_COLS - 1 }, (_, i) => (
109
+ <Line
110
+ key={`c${i}`}
111
+ x1={gx + (gw / GRID_COLS) * (i + 1)}
112
+ y1={gy}
113
+ x2={gx + (gw / GRID_COLS) * (i + 1)}
114
+ y2={gy + gh}
115
+ stroke={accent}
116
+ strokeWidth={0.75}
117
+ />
118
+ ))}
119
+ {Array.from({ length: GRID_ROWS - 1 }, (_, i) => (
120
+ <Line
121
+ key={`r${i}`}
122
+ x1={gx}
123
+ y1={gy + (gh / GRID_ROWS) * (i + 1)}
124
+ x2={gx + gw}
125
+ y2={gy + (gh / GRID_ROWS) * (i + 1)}
126
+ stroke={accent}
127
+ strokeWidth={0.75}
128
+ />
129
+ ))}
130
+ </G>
131
+
132
+ <Rect x={gx} y={gy} width={gw} height={gh} rx={r} fill="none" stroke={`${accent}66`} strokeWidth={1.5} />
133
+
134
+ {/* Corner brackets — the frame's actual edges, heavy enough to aim by. */}
135
+ {[
136
+ `M${gx},${gy + corner} L${gx},${gy + r} Q${gx},${gy} ${gx + r},${gy} L${gx + corner},${gy}`,
137
+ `M${gx + gw - corner},${gy} L${gx + gw - r},${gy} Q${gx + gw},${gy} ${gx + gw},${gy + r} L${gx + gw},${gy + corner}`,
138
+ `M${gx + gw},${gy + gh - corner} L${gx + gw},${gy + gh - r} Q${gx + gw},${gy + gh} ${gx + gw - r},${gy + gh} L${gx + gw - corner},${gy + gh}`,
139
+ `M${gx + corner},${gy + gh} L${gx + r},${gy + gh} Q${gx},${gy + gh} ${gx},${gy + gh - r} L${gx},${gy + gh - corner}`,
140
+ ].map((d, i) => (
141
+ <Path key={i} d={d} stroke={accent} strokeWidth={4} fill="none" strokeLinecap="round" />
142
+ ))}
143
+ </Svg>
144
+
145
+ {/* Scan line, above the SVG so it can be transform-animated natively. */}
146
+ <Animated.View
147
+ pointerEvents="none"
148
+ style={{
149
+ position: 'absolute',
150
+ left: gx,
151
+ top: gy,
152
+ width: gw,
153
+ height: 2,
154
+ backgroundColor: accent,
155
+ shadowColor: accent,
156
+ shadowOpacity: 0.9,
157
+ shadowRadius: 8,
158
+ shadowOffset: { width: 0, height: 0 },
159
+ transform: [
160
+ { translateY: sweep.interpolate({ inputRange: [0, 1], outputRange: [0, gh - 2] }) },
161
+ ],
162
+ }}
163
+ />
164
+ <DocumentGhost
165
+ x={gx}
166
+ y={gy}
167
+ width={gw}
168
+ height={gh}
169
+ showMrzBand={showMrzBand}
170
+ documentFound={framing !== 'none' && framing !== 'wrongShape'}
171
+ />
172
+ </>
173
+ );
174
+ }
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { spacing } from '../../config/theme';
5
+ import { useTheme } from '../runtime';
6
+ import { ImmersiveGuide } from './ImmersiveGuide';
7
+ import { DocumentPill, RoundControl, SideBadge } from './ImmersiveControls';
8
+ import { BottomBar } from './ImmersiveBottomBar';
9
+ import type { DocumentFraming } from '../../capture';
10
+
11
+ /**
12
+ * Everything drawn over the full-screen camera.
13
+ *
14
+ * Composed here rather than in CameraViewfinder so that file stays about camera
15
+ * wiring — and so the immersive layout can be read in one place against the
16
+ * Flutter screen it mirrors.
17
+ */
18
+ export function ImmersiveOverlay(p: {
19
+ side: 'front' | 'back';
20
+ country: string;
21
+ documentLabel: string;
22
+ guideAspect: number;
23
+ hint: string;
24
+ hintIsAction: boolean;
25
+ progress: number;
26
+ busy: boolean;
27
+ torch: boolean;
28
+ hasTorch: boolean;
29
+ onToggleTorch: () => void;
30
+ onCapture: () => void;
31
+ onBack: (() => void) | null;
32
+ onUpload: (() => void) | null;
33
+ bottomInset: number;
34
+ width: number;
35
+ height: number;
36
+ topInset: number;
37
+ framing: DocumentFraming;
38
+ /** Passports carry an MRZ band; the ghost draws it so it is not cropped. */
39
+ showMrzBand: boolean;
40
+ }): React.ReactElement {
41
+ const { colors } = useTheme();
42
+ return (
43
+ <>
44
+ <ImmersiveGuide
45
+ guideAspect={p.guideAspect}
46
+ primary={colors.primary}
47
+ width={p.width}
48
+ height={p.height}
49
+ framing={p.framing}
50
+ showMrzBand={p.showMrzBand}
51
+ />
52
+ {p.onBack ? (
53
+ <RoundControl
54
+ icon="back"
55
+ label="Go back"
56
+ onPress={p.onBack}
57
+ style={{ top: p.topInset, left: spacing.md }}
58
+ />
59
+ ) : null}
60
+ <SideBadge side={p.side} top={p.topInset + 4} />
61
+ <DocumentPill country={p.country} label={p.documentLabel} top={p.topInset + 62} />
62
+ <BottomBar
63
+ hint={p.hint}
64
+ hintIsAction={p.hintIsAction}
65
+ allowUpload={!!p.onUpload}
66
+ onUpload={() => p.onUpload?.()}
67
+ onCapture={p.onCapture}
68
+ busy={p.busy}
69
+ ready
70
+ progress={p.progress}
71
+ hasTorch={p.hasTorch}
72
+ torch={p.torch}
73
+ onToggleTorch={p.onToggleTorch}
74
+ bottomInset={p.bottomInset}
75
+ />
76
+ <View pointerEvents="none" />
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,154 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, Pressable, View } from 'react-native';
3
+ import Svg, { Circle } from 'react-native-svg';
4
+
5
+ import { radius, spacing } from '../../config/theme';
6
+ import { useTheme } from '../runtime';
7
+ import { MyazaText } from '../Typography';
8
+ import { Icon } from '../Icon';
9
+
10
+ /** Top offset that clears the status bar when the camera runs full-bleed. */
11
+ export function topInset(fill: boolean): number {
12
+ return fill ? spacing.xl + spacing.md : spacing.md;
13
+ }
14
+
15
+ /**
16
+ * The live instruction from the auto-capture gate.
17
+ *
18
+ * It replaced a static "align your ID" precisely because that told the user
19
+ * nothing about WHICH way to move — and the commonest failure, holding the
20
+ * document too close, is the one nobody guesses. Action hints take the primary
21
+ * colour so they read as something to do.
22
+ */
23
+ export function HintBanner({
24
+ text,
25
+ isAction,
26
+ fill,
27
+ }: {
28
+ text: string;
29
+ isAction: boolean;
30
+ fill: boolean;
31
+ }): React.ReactElement {
32
+ const { colors } = useTheme();
33
+ return (
34
+ <View style={{ position: 'absolute', top: topInset(fill), left: 0, right: 0, alignItems: 'center' }}>
35
+ <View
36
+ style={{
37
+ backgroundColor: isAction ? colors.primary : 'rgba(0,0,0,0.4)',
38
+ paddingHorizontal: spacing.sm + 4,
39
+ paddingVertical: 6,
40
+ borderRadius: radius.full,
41
+ maxWidth: '90%',
42
+ }}
43
+ >
44
+ <MyazaText variant="bodySmall" color="#FFFFFF" style={{ textAlign: 'center' }}>
45
+ {text}
46
+ </MyazaText>
47
+ </View>
48
+ </View>
49
+ );
50
+ }
51
+
52
+ /** A round overlay button — the torch and the immersive back control. */
53
+ export function OverlayButton({
54
+ icon,
55
+ onPress,
56
+ label,
57
+ active,
58
+ side,
59
+ fill,
60
+ }: {
61
+ icon: 'lightbulb' | 'back';
62
+ onPress: () => void;
63
+ label: string;
64
+ active?: boolean;
65
+ side: 'left' | 'right';
66
+ fill: boolean;
67
+ }): React.ReactElement {
68
+ const { colors } = useTheme();
69
+ return (
70
+ <Pressable
71
+ onPress={onPress}
72
+ accessibilityRole="button"
73
+ accessibilityLabel={label}
74
+ style={{
75
+ position: 'absolute',
76
+ top: topInset(fill),
77
+ [side]: spacing.md,
78
+ width: 36,
79
+ height: 36,
80
+ borderRadius: radius.full,
81
+ backgroundColor: active ? colors.primary : 'rgba(0,0,0,0.4)',
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ }}
85
+ >
86
+ <Icon name={icon} size={18} color="#FFFFFF" />
87
+ </Pressable>
88
+ );
89
+ }
90
+
91
+ /**
92
+ * The shutter, with the stability dwell drawn around it.
93
+ *
94
+ * The ring is what tells a user who is holding steady that something is
95
+ * happening — without it, auto-capture looks like a camera doing nothing right
96
+ * up until it fires.
97
+ */
98
+ export function Shutter({
99
+ onPress,
100
+ ready,
101
+ busy,
102
+ progress,
103
+ }: {
104
+ onPress: () => void;
105
+ ready: boolean;
106
+ busy: boolean;
107
+ progress: number;
108
+ }): React.ReactElement {
109
+ const { colors } = useTheme();
110
+ const R = 36;
111
+ const CIRC = 2 * Math.PI * R;
112
+
113
+ return (
114
+ <View style={{ position: 'absolute', bottom: spacing.lg, left: 0, right: 0, alignItems: 'center' }}>
115
+ {progress > 0 && progress < 1 ? (
116
+ <Svg width={80} height={80} style={{ position: 'absolute', bottom: -8 }}>
117
+ <Circle cx={40} cy={40} r={R} stroke="rgba(255,255,255,0.25)" strokeWidth={3} fill="none" />
118
+ <Circle
119
+ cx={40}
120
+ cy={40}
121
+ r={R}
122
+ stroke={colors.primary}
123
+ strokeWidth={3}
124
+ fill="none"
125
+ strokeLinecap="round"
126
+ strokeDasharray={CIRC}
127
+ strokeDashoffset={CIRC * (1 - progress)}
128
+ transform="rotate(-90 40 40)"
129
+ />
130
+ </Svg>
131
+ ) : null}
132
+ <Pressable
133
+ onPress={onPress}
134
+ disabled={busy || !ready}
135
+ accessibilityRole="button"
136
+ accessibilityLabel="Capture photo"
137
+ style={({ pressed }) => ({
138
+ width: 64,
139
+ height: 64,
140
+ borderRadius: radius.full,
141
+ backgroundColor: ready ? '#FFFFFF' : colors.gray400,
142
+ borderWidth: 4,
143
+ borderColor: ready ? colors.primary : colors.gray300,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ opacity: busy ? 0.6 : 1,
147
+ transform: [{ scale: pressed ? 0.94 : 1 }],
148
+ })}
149
+ >
150
+ {busy ? <ActivityIndicator color={colors.primary} /> : <Icon name="camera" size={24} color={ready ? colors.primary : '#FFFFFF'} />}
151
+ </Pressable>
152
+ </View>
153
+ );
154
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Vendor brand constants for the SDK's own attribution — NOT the integrating
3
+ * org's branding (that is `useBranding()`, driven by `/api/kyc/config`).
4
+ *
5
+ * The platform is **Myaza Trust**, a product of **Myaza** (the parent company).
6
+ * Always the full product name: bare "Myaza" is the company, and a pipe
7
+ * ("Myaza | Trust") reads as a separator between two items rather than one
8
+ * name. See the naming rule in kyc-dashboard/CLAUDE.md.
9
+ */
10
+ export const PRODUCT_NAME = 'Myaza Trust';
11
+
12
+ /** The product site — where "who are these people?" gets answered. */
13
+ export const PRODUCT_URL = 'https://trust.myaza.co';
14
+
15
+ /**
16
+ * Wordmark ink. The icon keeps its brand colours in both themes; only the
17
+ * "myaza" lettering flips. These are the exact values the dashboard's own Logo
18
+ * renders (logo.svg / logo-white.svg), so the mark is brand-accurate rather
19
+ * than tinted to whatever the sheet's text colour happens to be.
20
+ */
21
+ export const WORDMARK_LIGHT = '#19156F';
22
+ export const WORDMARK_DARK = '#F6F5FE';
23
+
24
+ /**
25
+ * End-user legal documents linked from the consent screen. These are MYAZA's
26
+ * terms — the person consents to us processing their data as the verification
27
+ * provider — so they are never org-overridable.
28
+ */
29
+ export const TERMS_URL = 'https://trust.myaza.co/legal/terms';
30
+ export const PRIVACY_URL = 'https://trust.myaza.co/legal/privacy';
31
+
32
+ /**
33
+ * Version of the consent wording. Consent is given by ACTING (tapping
34
+ * Continue) rather than ticking a box, so bump this whenever the copy changes
35
+ * materially and store it with the verification — otherwise nothing proves
36
+ * which disclosure was on screen. Keep in step with the web SDK's value.
37
+ */
38
+ export const CONSENT_VERSION = '2026-08-06.1';
39
+
40
+ /**
41
+ * Tones the footer mark may use — the design system's INK and LIGHT text
42
+ * colours, not the brand purple.
43
+ *
44
+ * Attribution, not advertising. A saturated purple mark is the loudest thing in
45
+ * the footer on an org whose palette is yellow or green: it draws the eye to the
46
+ * least important element on screen and reads as a clash rather than a
47
+ * signature. Every comparable vendor mark (Stripe, Plaid, Persona) is monochrome
48
+ * for the same reason.
49
+ *
50
+ * Nothing is lost by it — the logo ICON keeps its fixed brand fills, so the
51
+ * Myaza identity is still carried by the mark itself while the text recedes.
52
+ * These two are the design system's own text tones, so this stays on-brand
53
+ * rather than arbitrary black-on-white.
54
+ *
55
+ * Mirrors the web SDK's `lib/brand.ts` — keep the list and the rule in step.
56
+ */
57
+ const MARK_TONES = ['#070330', '#F6F5FE'] as const;
58
+
59
+ /** WCAG AA for the small text this renders at. */
60
+ const MIN_MARK_CONTRAST = 4.5;
61
+
62
+ function relativeLuminance(hex: string): number {
63
+ const h = hex.replace('#', '');
64
+ const channel = (i: number): number => {
65
+ const s = parseInt(h.slice(i, i + 2), 16) / 255;
66
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
67
+ };
68
+ return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
69
+ }
70
+
71
+ function contrastRatio(a: string, b: string): number {
72
+ const la = relativeLuminance(a);
73
+ const lb = relativeLuminance(b);
74
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
75
+ }
76
+
77
+ /**
78
+ * The mark tone that stays legible on `background`.
79
+ *
80
+ * The footer mark must NOT follow the org's palette: it is our attribution, and
81
+ * an org whose brand is close to ours would otherwise get a mark that either
82
+ * stops reading as Myaza or disappears into the background entirely. Chosen by
83
+ * CONTRAST rather than by light/dark, because an org can set any background —
84
+ * including mid-tones where neither variant is obviously right.
85
+ *
86
+ * Falls back to the most visible tone when none clears AA, so the answer is
87
+ * always the best available rather than a fixed guess.
88
+ */
89
+ export function brandMarkColor(background: string): string {
90
+ const bg = /^#[0-9a-f]{6}$/i.test(background.trim()) ? background.trim() : '#FFFFFF';
91
+ for (const tone of MARK_TONES) {
92
+ if (contrastRatio(tone, bg) >= MIN_MARK_CONTRAST) return tone;
93
+ }
94
+ return MARK_TONES.reduce((best, tone) =>
95
+ contrastRatio(tone, bg) > contrastRatio(best, bg) ? tone : best,
96
+ );
97
+ }