@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,18 +1,22 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { Image, Platform, ScrollView, StatusBar, View } from 'react-native';
3
+ import { SvgXml } from 'react-native-svg';
3
4
  import { StatusBarController } from './StatusBarController';
4
5
  import { initialWindowMetrics } from 'react-native-safe-area-context';
5
6
 
6
7
  import { headerSurface, radius, spacing } from '../config/theme';
8
+ import { useKeyboardInset } from '../lib/use-keyboard-inset';
7
9
  import type { SupportedCountry } from '../types/config';
8
10
  import { useKycConfig, useTheme } from './runtime';
9
11
  import { useBranding } from './useBranding';
10
12
  import { MyazaText } from './Typography';
13
+ import { PoweredBy } from './PoweredBy';
11
14
  import { StepHeader } from './StepHeader';
12
15
  import { StepIndicator } from './StepIndicator';
13
16
  import { GlassIconButton } from './GlassIconButton';
14
17
  import { GlassSurface } from './glass/GlassSurface';
15
18
  import { GlassGroup } from './glass/GlassGroup';
19
+ import { FlashOverlay } from './FlashOverlay';
16
20
 
17
21
  // Full-screen sheet shell — 1:1 with the Flutter SDK's KycBottomSheet:
18
22
  // • a tinted header block (Liquid Glass on iOS 26) with a bottom border:
@@ -33,6 +37,29 @@ export interface KycSheetProps {
33
37
  onClose: () => void;
34
38
  /** Hide the persistent brand bar (e.g. on a fatal config-error screen). */
35
39
  hideBrand?: boolean;
40
+ /**
41
+ * Render the step body as a fixed-height flex container instead of a scroll
42
+ * view, so the step owns its own scrolling.
43
+ *
44
+ * Needed by any step with a PINNED element above a long list — the country
45
+ * picker's search box, for one. RN gives a nested same-axis ScrollView an
46
+ * unbounded height, so the inner list grows to fit every row, the outer
47
+ * content outgrows the viewport, and the outer view scrolls the "pinned"
48
+ * element off the top. `flexGrow: 1` cannot prevent that: it sets a minimum
49
+ * height, not a maximum. Flutter hits the same wall and solves it the same
50
+ * way (`KycBottomSheet.fillsViewport`).
51
+ */
52
+ fillsViewport?: boolean;
53
+ /**
54
+ * Hand the whole surface to the step — no header, no footer, no padding.
55
+ *
56
+ * Used by the live camera, which wants the entire display. It is a PROP
57
+ * rather than the caller rendering its own tree because the toggle happens
58
+ * MID-STEP: swapping the step's parent would unmount and remount it, losing
59
+ * its state (the acknowledged primer, the captured sides) and restarting the
60
+ * camera. Keeping one stable tree is the whole point.
61
+ */
62
+ immersive?: boolean;
36
63
  }
37
64
 
38
65
  export function KycSheet({
@@ -45,10 +72,15 @@ export function KycSheet({
45
72
  onBack,
46
73
  onClose,
47
74
  hideBrand,
75
+ fillsViewport,
76
+ immersive = false,
48
77
  }: KycSheetProps): React.ReactElement {
49
78
  const { colors, mode, toggle } = useTheme();
50
79
  const config = useKycConfig();
51
80
  const { logoUri, companyName } = useBranding();
81
+ // Android-only (0 on iOS): the translucent Modal blocks adjustResize there,
82
+ // so the keyboard height is added to the scroll padding by hand.
83
+ const keyboardInset = useKeyboardInset();
52
84
 
53
85
  const tint = headerSurface(colors, mode);
54
86
  const showIndicator = progress != null && stepCount != null;
@@ -85,7 +117,7 @@ export function KycSheet({
85
117
  Platform.OS === 'android' ? sa?.bottom ?? 0 : iosFullScreen ? 34 : 0;
86
118
 
87
119
  return (
88
- <View style={{ flex: 1, backgroundColor: colors.background }}>
120
+ <View style={{ flex: 1, backgroundColor: immersive ? '#000000' : colors.background }}>
89
121
  {/* The SDK theme drives the status-bar glyph colour so it stays readable on
90
122
  the modal background — light glyphs in dark mode, dark in light mode.
91
123
  Android: JS StatusBar/SystemBars can't reach the <Modal>'s Dialog window,
@@ -98,8 +130,9 @@ export function KycSheet({
98
130
  <StatusBar barStyle={mode === 'dark' ? 'light-content' : 'dark-content'} animated />
99
131
  ) : null}
100
132
  <View style={{ flex: 1 }}>
101
- {/* Header block (glass on iOS 26, tinted surface otherwise) */}
102
- <GlassSurface
133
+ {/* Header block (glass on iOS 26, tinted surface otherwise) — dropped
134
+ entirely when the step owns the display. */}
135
+ {immersive ? null : <GlassSurface
103
136
  glassStyle="regular"
104
137
  fallbackColor={tint}
105
138
  style={{ borderBottomWidth: 1, borderBottomColor: colors.border, paddingBottom: spacing.md }}
@@ -169,26 +202,100 @@ export function KycSheet({
169
202
  <StepIndicator progress={progress!} stepCount={stepCount!} />
170
203
  </View>
171
204
  ) : null}
172
- </GlassSurface>
205
+ </GlassSurface>}
173
206
 
174
207
  {/* Step body */}
175
- <ScrollView
176
- contentContainerStyle={{ padding: spacing.md, paddingTop: spacing.md, paddingBottom: spacing.xl + bottomInset, flexGrow: 1 }}
177
- keyboardShouldPersistTaps="handled"
178
- >
179
- {children}
180
- </ScrollView>
208
+ {fillsViewport ? (
209
+ // Only the visual gap here, NOT the scroll variant's `xl`: that
210
+ // padding exists so scrolled content clears the home indicator, but
211
+ // in a fixed-height body it just lifts the list off the footer and
212
+ // reads as a dead band. PoweredBy owns the safe-area inset already.
213
+ <View style={{ flex: 1, padding: spacing.md, paddingBottom: spacing.md }}>
214
+ {children}
215
+ </View>
216
+ ) : (
217
+ // IMMERSIVE keeps this a ScrollView rather than swapping in a View.
218
+ // The toggle happens MID-STEP, and changing the children's parent
219
+ // element remounts the step — which wiped the acknowledged primer and
220
+ // bounced the user straight back to it. Same element, different
221
+ // props: scrolling off and the padding dropped.
222
+ <ScrollView
223
+ scrollEnabled={!immersive}
224
+ contentContainerStyle={
225
+ immersive
226
+ ? { flexGrow: 1 }
227
+ : {
228
+ padding: spacing.md,
229
+ paddingTop: spacing.md,
230
+ paddingBottom: spacing.xl + keyboardInset,
231
+ flexGrow: 1,
232
+ }
233
+ }
234
+ keyboardShouldPersistTaps="handled"
235
+ // iOS: inset the content by the keyboard AND scroll the focused
236
+ // input into view — the platform's own handling, which a
237
+ // KeyboardAvoidingView only approximates. Without this the
238
+ // keyboard simply covered the bottom of every input step.
239
+ automaticallyAdjustKeyboardInsets={Platform.OS === 'ios' && !immersive}
240
+ >
241
+ {children}
242
+ </ScrollView>
243
+ )}
244
+
245
+ {/* Vendor attribution — a sibling of the ScrollView, so it stays pinned
246
+ while a long step scrolls under it. It now owns the bottom safe-area
247
+ inset that the scroll padding used to carry. */}
248
+ {immersive ? null : <PoweredBy bottomInset={bottomInset} />}
181
249
  </View>
250
+
251
+ {/* The flash paints over the WHOLE sheet — header, body and footer — but
252
+ from inside the sheet's own tree, so its cutout and the camera preview
253
+ scale together. It subscribes to the store itself, so a colour change
254
+ re-renders this leaf and not the step (and not the camera).
255
+
256
+ LAST sibling, on purpose. As the first child it sat below everything
257
+ later in the tree, and on Android the header's elevated surface
258
+ painted over it — the flash lit the body and stopped dead at the
259
+ header. Last-with-elevation out-stacks the header on both platforms
260
+ by construction. */}
261
+ <FlashOverlay />
182
262
  </View>
183
263
  );
184
264
  }
185
265
 
266
+ /** Whether a logo URI points at an SVG (extension check, query-safe). */
267
+ function isSvgUri(uri: string): boolean {
268
+ const path = uri.split('?')[0] ?? uri;
269
+ return path.toLowerCase().endsWith('.svg');
270
+ }
271
+
186
272
  // Persistent header brand — circular logo avatar + company name. A broken/missing
187
273
  // logo image collapses the whole brand bar (mirrors the web SDK's `onError` and
188
274
  // Flutter's `errorBuilder`), so the header never shows a broken-image box.
189
275
  function BrandBar({ logoUri, companyName }: { logoUri: string; companyName: string }): React.ReactElement | null {
190
276
  const { colors } = useTheme();
191
277
  const [broken, setBroken] = useState(false);
278
+ const svg = isSvgUri(logoUri);
279
+ // SVG logos render through SvgXml with SELF-FETCHED markup — the exact
280
+ // pipeline the country flags use, which is proven on-device. SvgUri's own
281
+ // fetch/sizing was flaky here (the favicon rendered at intrinsic size
282
+ // instead of filling the chip).
283
+ const [xml, setXml] = useState<string | null>(null);
284
+ useEffect(() => {
285
+ if (!svg) return;
286
+ let cancelled = false;
287
+ fetch(logoUri)
288
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))))
289
+ .then((text) => {
290
+ if (!cancelled) setXml(text);
291
+ })
292
+ .catch(() => {
293
+ if (!cancelled) setBroken(true);
294
+ });
295
+ return () => {
296
+ cancelled = true;
297
+ };
298
+ }, [logoUri, svg]);
192
299
  if (broken) return null;
193
300
  return (
194
301
  <>
@@ -198,8 +305,16 @@ function BrandBar({ logoUri, companyName }: { logoUri: string; companyName: stri
198
305
  height: 28,
199
306
  borderRadius: radius.full,
200
307
  backgroundColor: '#FFFFFF',
308
+ // Light border + soft shadow — the same treatment as the Flutter
309
+ // SDK's brand chip (black 5% ring, black 6% blur-4 y-1 shadow) and
310
+ // the web SDK's ring-1 ring-black/5.
201
311
  borderWidth: 1,
202
312
  borderColor: 'rgba(0,0,0,0.05)',
313
+ shadowColor: '#000000',
314
+ shadowOpacity: 0.06,
315
+ shadowRadius: 4,
316
+ shadowOffset: { width: 0, height: 1 },
317
+ elevation: 1,
203
318
  overflow: 'hidden',
204
319
  marginRight: spacing.sm,
205
320
  alignItems: 'center',
@@ -208,13 +323,28 @@ function BrandBar({ logoUri, companyName }: { logoUri: string; companyName: stri
208
323
  >
209
324
  {/* Fill + cover-crop the circle. The borderRadius is repeated on the
210
325
  Image itself because iOS doesn't reliably clip an Image child to a
211
- parent's rounded corners (mirrors Flutter's ClipOval + cover). */}
212
- <Image
213
- source={{ uri: logoUri }}
214
- style={{ width: '100%', height: '100%', borderRadius: radius.full }}
215
- resizeMode="cover"
216
- onError={() => setBroken(true)}
217
- />
326
+ parent's rounded corners (mirrors Flutter's ClipOval + cover).
327
+ SVG logos (common — the brand import picks up site favicons) can't
328
+ be decoded by <Image>; the fetched markup renders via SvgXml at
329
+ the chip's inner size (26 = 28 minus the 1px border each side),
330
+ slice = cover-crop. */}
331
+ {svg ? (
332
+ xml ? (
333
+ <SvgXml
334
+ xml={xml}
335
+ width={26}
336
+ height={26}
337
+ preserveAspectRatio="xMidYMid slice"
338
+ />
339
+ ) : null
340
+ ) : (
341
+ <Image
342
+ source={{ uri: logoUri }}
343
+ style={{ width: '100%', height: '100%', borderRadius: radius.full }}
344
+ resizeMode="cover"
345
+ onError={() => setBroken(true)}
346
+ />
347
+ )}
218
348
  </View>
219
349
  <MyazaText variant="heading3" numberOfLines={1} color={colors.textDark} style={{ flexShrink: 1, fontSize: 14 }}>
220
350
  {companyName}
@@ -0,0 +1,163 @@
1
+ import React, { useRef } from 'react';
2
+ import { Modal, Platform, Pressable, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { Icon, type IconName } from './Icon';
6
+ import { useTheme } from './runtime';
7
+ import { MyazaText } from './Typography';
8
+
9
+ /**
10
+ * The SDK's own "where is the document?" sheet.
11
+ *
12
+ * Replaces `ActionSheetIOS` / `Alert.alert`, which were the two NATIVE
13
+ * choosers — grey system UI with bare uppercase labels, dropped into the
14
+ * middle of an otherwise fully branded flow. A verification flow trades on
15
+ * looking deliberate; one stock OS dialog in the middle of it reads as a seam,
16
+ * and on Android the three-button alert didn't even look like a chooser.
17
+ *
18
+ * Same presentation grammar as the SDK's other sheets (select, date picker):
19
+ * bottom card, backdrop dismiss, org palette, one icon per option so the three
20
+ * sources are scannable without reading.
21
+ */
22
+ export interface MediaSourceOption {
23
+ icon: IconName;
24
+ label: string;
25
+ /** One line of what picking this means — the scannable part. */
26
+ caption: string;
27
+ onPress: () => void;
28
+ }
29
+
30
+ export function MediaSourceSheet({
31
+ open,
32
+ title,
33
+ options,
34
+ onClose,
35
+ }: {
36
+ open: boolean;
37
+ title: string;
38
+ options: MediaSourceOption[];
39
+ onClose: () => void;
40
+ }): React.ReactElement | null {
41
+ const { colors } = useTheme();
42
+
43
+ // iOS: a picker (photo library / files / camera) is a view controller, and
44
+ // UIKit REFUSES to present one while this sheet's own modal is still
45
+ // animating its dismissal — which is precisely when a "close then run the
46
+ // picker" handler runs. So the chosen action is parked and executed from
47
+ // the Modal's `onDismiss`, the callback that fires only after the
48
+ // dismissal has fully completed. That is also why the Modal stays MOUNTED
49
+ // (`visible` instead of an early null return): unmounting skips the
50
+ // dismiss lifecycle and onDismiss never fires. Android pickers are
51
+ // separate activities with no such conflict, so they run immediately.
52
+ const pendingRef = useRef<(() => void) | null>(null);
53
+ const choose = (action: () => void): void => {
54
+ if (Platform.OS === 'ios') {
55
+ pendingRef.current = action;
56
+ onClose();
57
+ return;
58
+ }
59
+ onClose();
60
+ action();
61
+ };
62
+
63
+ return (
64
+ <Modal
65
+ transparent
66
+ visible={open}
67
+ animationType="fade"
68
+ onRequestClose={onClose}
69
+ onDismiss={() => {
70
+ const run = pendingRef.current;
71
+ pendingRef.current = null;
72
+ run?.();
73
+ }}
74
+ >
75
+ <Pressable
76
+ onPress={onClose}
77
+ style={{ flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.5)' }}
78
+ >
79
+ {/* Swallow taps on the card so only the backdrop dismisses. */}
80
+ <Pressable
81
+ onPress={() => undefined}
82
+ style={{
83
+ backgroundColor: colors.background,
84
+ borderTopLeftRadius: radius.xl,
85
+ borderTopRightRadius: radius.xl,
86
+ padding: spacing.md,
87
+ paddingBottom: spacing.xl,
88
+ }}
89
+ >
90
+ {/* Grab handle — the affordance that says "sheet", and therefore
91
+ "swipe-away-able", without a close button competing with the
92
+ options. */}
93
+ <View
94
+ style={{
95
+ alignSelf: 'center',
96
+ width: 36,
97
+ height: 4,
98
+ borderRadius: 2,
99
+ backgroundColor: colors.border,
100
+ marginBottom: spacing.md,
101
+ }}
102
+ />
103
+
104
+ <MyazaText variant="heading3" style={{ textAlign: 'center', marginBottom: spacing.md }}>
105
+ {title}
106
+ </MyazaText>
107
+
108
+ {options.map((o, i) => (
109
+ <Pressable
110
+ key={o.label}
111
+ onPress={() => choose(o.onPress)}
112
+ accessibilityRole="button"
113
+ accessibilityLabel={o.label}
114
+ style={({ pressed }) => ({
115
+ flexDirection: 'row',
116
+ alignItems: 'center',
117
+ gap: spacing.md,
118
+ paddingVertical: spacing.md,
119
+ paddingHorizontal: spacing.sm,
120
+ borderRadius: radius.md,
121
+ backgroundColor: pressed ? colors.backgroundSecondary : 'transparent',
122
+ marginTop: i === 0 ? 0 : spacing.xs,
123
+ })}
124
+ >
125
+ {/* Tinted icon disc — the same treatment the flow's info rows
126
+ use, so the sheet reads as part of the SDK, not the OS. */}
127
+ <View
128
+ style={{
129
+ width: 42,
130
+ height: 42,
131
+ borderRadius: radius.full,
132
+ backgroundColor: colors.primary50,
133
+ alignItems: 'center',
134
+ justifyContent: 'center',
135
+ }}
136
+ >
137
+ <Icon name={o.icon} size={20} color={colors.primary} />
138
+ </View>
139
+ <View style={{ flex: 1 }}>
140
+ <MyazaText variant="label">{o.label}</MyazaText>
141
+ <MyazaText variant="bodySmall" color={colors.textMuted}>
142
+ {o.caption}
143
+ </MyazaText>
144
+ </View>
145
+ <Icon name="chevron-right" size={18} color={colors.textMuted} />
146
+ </Pressable>
147
+ ))}
148
+
149
+ <Pressable
150
+ onPress={onClose}
151
+ accessibilityRole="button"
152
+ accessibilityLabel="Cancel"
153
+ style={{ paddingVertical: spacing.md, alignItems: 'center', marginTop: spacing.xs }}
154
+ >
155
+ <MyazaText variant="label" color={colors.textSecondary}>
156
+ Cancel
157
+ </MyazaText>
158
+ </Pressable>
159
+ </Pressable>
160
+ </Pressable>
161
+ </Modal>
162
+ );
163
+ }
@@ -85,13 +85,25 @@ export function MyazaButton({
85
85
  {loading ? (
86
86
  <ActivityIndicator color={effectiveFg} size="small" />
87
87
  ) : (
88
- <View style={{ flexDirection: 'row', alignItems: 'center' }}>
88
+ // `flexShrink: 1` on the ROW and the label: a Text in a row that cannot
89
+ // shrink does not wrap, it OVERFLOWS and gets clipped. That was
90
+ // invisible with the bundled font and cut "Continue" to "Continu" the
91
+ // moment an org picked a wider one. A button label must never be
92
+ // truncated by a branding choice, hence the shrink-to-fit as well.
93
+ <View style={{ flexDirection: 'row', alignItems: 'center', flexShrink: 1 }}>
89
94
  {leadingIcon ? (
90
95
  <View style={{ marginRight: 8 }}>
91
96
  <Icon name={leadingIcon} size={18} color={effectiveFg} />
92
97
  </View>
93
98
  ) : null}
94
- <MyazaText variant="button" color={effectiveFg}>
99
+ <MyazaText
100
+ variant="button"
101
+ color={effectiveFg}
102
+ numberOfLines={1}
103
+ adjustsFontSizeToFit
104
+ minimumFontScale={0.8}
105
+ style={{ flexShrink: 1 }}
106
+ >
95
107
  {label}
96
108
  </MyazaText>
97
109
  </View>
@@ -0,0 +1,240 @@
1
+ import React, { useState } from 'react';
2
+ import { Modal, Pressable, ScrollView, View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import {
6
+ MONTH_NAMES,
7
+ WEEKDAY_INITIALS,
8
+ addMonth,
9
+ monthMatrix,
10
+ parseIsoDate,
11
+ toIsoDate,
12
+ } from '../lib/calendar';
13
+ import { Icon } from './Icon';
14
+ import { useTheme } from './runtime';
15
+ import { MyazaText } from './Typography';
16
+
17
+ /**
18
+ * A date field that opens a REAL calendar picker, replacing the "type
19
+ * YYYY-MM-DD" text input — nobody should hand-format an ISO date on a phone
20
+ * keyboard. JS-drawn rather than `@react-native-community/datetimepicker`
21
+ * because a native module would be a new install step for every SDK consumer,
22
+ * and a JS calendar themes correctly (org palette, dark mode) for free — the
23
+ * same reason the Flutter SDK wraps `showDatePicker` in its own theme.
24
+ *
25
+ * The value contract is unchanged: emits strict `YYYY-MM-DD`, exactly what the
26
+ * text input produced and what the server validates.
27
+ *
28
+ * Years run NEWEST-FIRST in the year grid: the common date questions are
29
+ * birthdays and document dates, both reached faster from the recent end.
30
+ */
31
+ export function MyazaDateField({
32
+ value,
33
+ onChange,
34
+ placeholder,
35
+ error,
36
+ }: {
37
+ value?: string;
38
+ onChange: (iso: string) => void;
39
+ placeholder?: string;
40
+ error?: string | null;
41
+ }): React.ReactElement {
42
+ const { colors } = useTheme();
43
+ const [open, setOpen] = useState(false);
44
+ const selected = parseIsoDate(value);
45
+
46
+ return (
47
+ <>
48
+ <Pressable
49
+ onPress={() => setOpen(true)}
50
+ accessibilityRole="button"
51
+ accessibilityLabel={selected ? `Date, ${value}` : (placeholder ?? 'Select a date')}
52
+ style={{
53
+ flexDirection: 'row',
54
+ alignItems: 'center',
55
+ paddingHorizontal: spacing.md,
56
+ paddingVertical: spacing.md,
57
+ borderRadius: radius.sm,
58
+ // Mirrors MyazaInput's border grammar so the two field kinds sit
59
+ // side by side without looking like different systems.
60
+ borderWidth: error ? 1.5 : 1,
61
+ borderColor: error ? colors.error : colors.border,
62
+ backgroundColor: colors.background,
63
+ }}
64
+ >
65
+ <Icon name="calendar" size={18} color={colors.textSecondary} />
66
+ <View style={{ width: spacing.sm }} />
67
+ <MyazaText variant="body" color={selected ? undefined : colors.textMuted} style={{ flex: 1 }}>
68
+ {selected ? value! : (placeholder ?? 'Select a date')}
69
+ </MyazaText>
70
+ <Icon name="chevron-down" size={18} color={colors.textSecondary} />
71
+ </Pressable>
72
+ {error ? (
73
+ <MyazaText variant="bodySmall" color={colors.error} style={{ marginTop: spacing.xs }}>
74
+ {error}
75
+ </MyazaText>
76
+ ) : null}
77
+
78
+ {open ? (
79
+ <DatePickerSheet
80
+ initial={selected}
81
+ onClose={() => setOpen(false)}
82
+ onPick={(iso) => {
83
+ setOpen(false);
84
+ onChange(iso);
85
+ }}
86
+ />
87
+ ) : null}
88
+ </>
89
+ );
90
+ }
91
+
92
+ /** Same range as the Flutter SDK's picker: 1900 through five years out. */
93
+ const MIN_YEAR = 1900;
94
+ const MAX_YEAR = new Date().getFullYear() + 5;
95
+
96
+ function DatePickerSheet({
97
+ initial,
98
+ onClose,
99
+ onPick,
100
+ }: {
101
+ initial: ReturnType<typeof parseIsoDate>;
102
+ onClose: () => void;
103
+ onPick: (iso: string) => void;
104
+ }): React.ReactElement {
105
+ const { colors } = useTheme();
106
+ const today = new Date();
107
+ const [cursor, setCursor] = useState({
108
+ year: initial?.year ?? today.getFullYear(),
109
+ month0: initial?.month0 ?? today.getMonth(),
110
+ });
111
+ const [pickingYear, setPickingYear] = useState(false);
112
+
113
+ const weeks = monthMatrix(cursor.year, cursor.month0);
114
+ const isToday = (d: number) =>
115
+ cursor.year === today.getFullYear() && cursor.month0 === today.getMonth() && d === today.getDate();
116
+ const isSelected = (d: number) =>
117
+ !!initial && initial.year === cursor.year && initial.month0 === cursor.month0 && initial.day === d;
118
+
119
+ return (
120
+ <Modal transparent animationType="fade" onRequestClose={onClose}>
121
+ <Pressable
122
+ onPress={onClose}
123
+ style={{ flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.5)' }}
124
+ >
125
+ {/* Stop the backdrop press from closing when the card itself is tapped. */}
126
+ <Pressable
127
+ onPress={() => undefined}
128
+ style={{
129
+ backgroundColor: colors.background,
130
+ borderTopLeftRadius: radius.xl,
131
+ borderTopRightRadius: radius.xl,
132
+ padding: spacing.md,
133
+ paddingBottom: spacing.xl,
134
+ }}
135
+ >
136
+ {/* Month header: step, or tap the title to jump years. */}
137
+ <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }}>
138
+ <Pressable
139
+ onPress={() => setCursor((c) => addMonth(c.year, c.month0, -1))}
140
+ hitSlop={8}
141
+ accessibilityLabel="Previous month"
142
+ style={{ padding: spacing.sm }}
143
+ >
144
+ <Icon name="chevron-left" size={20} color={colors.textSecondary} />
145
+ </Pressable>
146
+ <Pressable
147
+ onPress={() => setPickingYear((v) => !v)}
148
+ accessibilityLabel="Choose year"
149
+ style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 4 }}
150
+ >
151
+ <MyazaText variant="label">{`${MONTH_NAMES[cursor.month0]} ${cursor.year}`}</MyazaText>
152
+ <Icon name="chevron-down" size={16} color={colors.textSecondary} />
153
+ </Pressable>
154
+ <Pressable
155
+ onPress={() => setCursor((c) => addMonth(c.year, c.month0, 1))}
156
+ hitSlop={8}
157
+ accessibilityLabel="Next month"
158
+ style={{ padding: spacing.sm }}
159
+ >
160
+ <Icon name="chevron-right" size={20} color={colors.textSecondary} />
161
+ </Pressable>
162
+ </View>
163
+
164
+ {pickingYear ? (
165
+ <ScrollView style={{ height: 280 }}>
166
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
167
+ {Array.from({ length: MAX_YEAR - MIN_YEAR + 1 }, (_, i) => MAX_YEAR - i).map((y) => (
168
+ <Pressable
169
+ key={y}
170
+ onPress={() => {
171
+ setCursor((c) => ({ ...c, year: y }));
172
+ setPickingYear(false);
173
+ }}
174
+ style={{ width: '25%', paddingVertical: spacing.sm, alignItems: 'center' }}
175
+ >
176
+ <MyazaText
177
+ variant="body"
178
+ color={y === cursor.year ? colors.primary : undefined}
179
+ style={y === cursor.year ? { fontWeight: '600' } : undefined}
180
+ >
181
+ {String(y)}
182
+ </MyazaText>
183
+ </Pressable>
184
+ ))}
185
+ </View>
186
+ </ScrollView>
187
+ ) : (
188
+ <>
189
+ <View style={{ flexDirection: 'row' }}>
190
+ {WEEKDAY_INITIALS.map((w, i) => (
191
+ <MyazaText
192
+ key={`${w}${i}`}
193
+ variant="bodySmall"
194
+ color={colors.textMuted}
195
+ style={{ flex: 1, textAlign: 'center' }}
196
+ >
197
+ {w}
198
+ </MyazaText>
199
+ ))}
200
+ </View>
201
+ {weeks.map((week, wi) => (
202
+ <View key={wi} style={{ flexDirection: 'row' }}>
203
+ {week.map((day, di) =>
204
+ day == null ? (
205
+ <View key={di} style={{ flex: 1, aspectRatio: 1 }} />
206
+ ) : (
207
+ <Pressable
208
+ key={di}
209
+ onPress={() => onPick(toIsoDate({ year: cursor.year, month0: cursor.month0, day }))}
210
+ accessibilityRole="button"
211
+ accessibilityLabel={`${day} ${MONTH_NAMES[cursor.month0]} ${cursor.year}`}
212
+ style={{
213
+ flex: 1,
214
+ aspectRatio: 1,
215
+ alignItems: 'center',
216
+ justifyContent: 'center',
217
+ borderRadius: radius.full,
218
+ backgroundColor: isSelected(day) ? colors.primary : undefined,
219
+ borderWidth: isToday(day) && !isSelected(day) ? 1 : 0,
220
+ borderColor: colors.primary,
221
+ }}
222
+ >
223
+ <MyazaText
224
+ variant="body"
225
+ color={isSelected(day) ? colors.onPrimary : undefined}
226
+ >
227
+ {String(day)}
228
+ </MyazaText>
229
+ </Pressable>
230
+ ),
231
+ )}
232
+ </View>
233
+ ))}
234
+ </>
235
+ )}
236
+ </Pressable>
237
+ </Pressable>
238
+ </Modal>
239
+ );
240
+ }