@myazahq/kyc-sdk-react-native 2.2.0 → 2.4.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 (51) hide show
  1. package/package.json +1 -2
  2. package/src/components/BrandBar.tsx +137 -0
  3. package/src/components/DialCodePicker.tsx +11 -17
  4. package/src/components/DocumentReviewSide.tsx +34 -14
  5. package/src/components/Icon.tsx +5 -0
  6. package/src/components/KycSheet.tsx +161 -180
  7. package/src/components/MediaSourceSheet.tsx +7 -37
  8. package/src/components/MyazaDateField.tsx +6 -20
  9. package/src/components/MyazaInput.tsx +6 -1
  10. package/src/components/MyazaSelect.tsx +20 -39
  11. package/src/components/PoweredBy.tsx +20 -15
  12. package/src/components/ProgressBar.tsx +91 -0
  13. package/src/components/SandboxBanner.tsx +92 -0
  14. package/src/components/StepHeader.tsx +15 -2
  15. package/src/components/StepIndicator.tsx +145 -31
  16. package/src/components/fonts.ts +23 -0
  17. package/src/components/glass/ChromeGlass.tsx +65 -0
  18. package/src/components/glass/FloatingSheet.tsx +190 -0
  19. package/src/components/glass/GlassSheet.tsx +38 -0
  20. package/src/components/glass/GlassSurface.tsx +31 -3
  21. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  22. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  23. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  24. package/src/config/questionnaire.ts +45 -4
  25. package/src/config/workflowMerge.ts +1 -0
  26. package/src/emrtd/crypto.ts +1 -1
  27. package/src/emrtd/dg1.ts +55 -0
  28. package/src/emrtd/ec-curves.ts +142 -0
  29. package/src/emrtd/ec.ts +196 -0
  30. package/src/emrtd/index.ts +1 -0
  31. package/src/emrtd/mrzKey.ts +21 -1
  32. package/src/emrtd/open.ts +169 -0
  33. package/src/emrtd/pace-params.ts +169 -0
  34. package/src/emrtd/pace.ts +295 -0
  35. package/src/emrtd/secureMessaging.ts +32 -14
  36. package/src/emrtd/session.ts +124 -20
  37. package/src/emrtd/suites.ts +99 -0
  38. package/src/index.ts +1 -0
  39. package/src/lib/step-log.ts +43 -0
  40. package/src/lib/step-window.ts +96 -0
  41. package/src/liveness/useLiveness.ts +1 -1
  42. package/src/screens/IdTypeStep.tsx +15 -2
  43. package/src/screens/NfcStep.tsx +12 -0
  44. package/src/screens/QuestionnaireField.tsx +30 -0
  45. package/src/screens/QuestionnaireStep.tsx +3 -1
  46. package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
  47. package/src/services/deviceMetadata.ts +9 -1
  48. package/src/store/derive.ts +7 -0
  49. package/src/store/kycStore.ts +25 -1
  50. package/src/types/config.ts +22 -0
  51. package/src/types/workflow.ts +10 -0
@@ -1,22 +1,23 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { Image, Platform, ScrollView, StatusBar, View } from 'react-native';
3
- import { SvgXml } from 'react-native-svg';
4
- import { StatusBarController } from './StatusBarController';
5
- import { initialWindowMetrics } from 'react-native-safe-area-context';
1
+ import React from "react";
2
+ import { Platform, ScrollView, StatusBar, View } from "react-native";
3
+ import { StatusBarController } from "./StatusBarController";
4
+ import { initialWindowMetrics } from "react-native-safe-area-context";
6
5
 
7
- import { headerSurface, radius, spacing } from '../config/theme';
8
- import { useKeyboardInset } from '../lib/use-keyboard-inset';
9
- import type { SupportedCountry } from '../types/config';
10
- import { useKycConfig, useTheme } from './runtime';
11
- import { useBranding } from './useBranding';
12
- import { MyazaText } from './Typography';
13
- import { PoweredBy } from './PoweredBy';
14
- import { StepHeader } from './StepHeader';
15
- import { StepIndicator } from './StepIndicator';
16
- import { GlassIconButton } from './GlassIconButton';
17
- import { GlassSurface } from './glass/GlassSurface';
18
- import { GlassGroup } from './glass/GlassGroup';
19
- import { FlashOverlay } from './FlashOverlay';
6
+ import { headerSurface, spacing } from "../config/theme";
7
+ import { useKeyboardInset } from "../lib/use-keyboard-inset";
8
+ import type { SupportedCountry } from "../types/config";
9
+ import { useKycConfig, useTheme } from "./runtime";
10
+ import { useBranding } from "./useBranding";
11
+ import { BrandBar } from "./BrandBar";
12
+ import { PoweredBy } from "./PoweredBy";
13
+ import { StepHeader } from "./StepHeader";
14
+ import { StepIndicator } from "./StepIndicator";
15
+ import { ProgressBar } from "./ProgressBar";
16
+ import { GlassIconButton } from "./GlassIconButton";
17
+ import { GlassSurface } from "./glass/GlassSurface";
18
+ import { SandboxBanner, useSandboxBannerVisible } from "./SandboxBanner";
19
+ import { GlassGroup } from "./glass/GlassGroup";
20
+ import { FlashOverlay } from "./FlashOverlay";
20
21
 
21
22
  // Full-screen sheet shell — 1:1 with the Flutter SDK's KycBottomSheet:
22
23
  // • a tinted header block (Liquid Glass on iOS 26) with a bottom border:
@@ -83,16 +84,38 @@ export function KycSheet({
83
84
  const keyboardInset = useKeyboardInset();
84
85
 
85
86
  const tint = headerSurface(colors, mode);
86
- const showIndicator = progress != null && stepCount != null;
87
- const showBrand = !hideBrand && !!logoUri;
88
-
87
+ // Decides who absorbs the status-bar inset: the banner sits above the header,
88
+ // so when it renders it is the topmost element and takes it instead.
89
+ const bannerVisible = useSandboxBannerVisible();
90
+ const hasProgress = progress != null && stepCount != null;
91
+ // The three styles are mutually exclusive: the bar REPLACES the step row
92
+ // rather than joining it (two progress indicators on one header would be
93
+ // noise, and the point of the bar is that it costs no height), and 'none'
94
+ // drops both. Switching on the resolved style rather than negating flags
95
+ // keeps a future fourth style from silently falling into the 'steps' branch.
96
+ const progressStyle = config.progressStyle ?? "steps";
97
+ const showIndicator = hasProgress && progressStyle === "steps";
98
+ const showBar = hasProgress && progressStyle === "bar";
89
99
 
100
+ // The header's bottom padding is the GAP to whatever sits under the brand
101
+ // row — the title block or the step row. With neither (a step that owns its
102
+ // own title, and progress off or drawn as the edge bar), that padding is
103
+ // space to a row that never renders, and the brand + controls read as
104
+ // sitting high in the band rather than centred on one line. Fall back to the
105
+ // row's own top padding so the two edges match.
106
+ //
107
+ // The bar is exempt: it is absolutely positioned ON the bottom edge, so the
108
+ // wider padding is what keeps it clear of the controls.
109
+ const hasRowBelowBrand = !!(title || onBack) || showIndicator;
110
+ const headerPaddingBottom =
111
+ hasRowBelowBrand || showBar ? spacing.md : spacing.sm;
112
+ const showBrand = !hideBrand && !!logoUri;
90
113
 
91
114
  // On iOS the modal is a pageSheet by default (sits below the status bar), but
92
115
  // `disableClose` presents it full-screen (to drop the swipe-to-dismiss), so it
93
116
  // then extends under the notch + over the home indicator and needs its own
94
117
  // safe-area insets — the pageSheet provides them for free.
95
- const iosFullScreen = Platform.OS === 'ios' && config.disableClose === true;
118
+ const iosFullScreen = Platform.OS === "ios" && config.disableClose === true;
96
119
 
97
120
  // Static safe-area snapshot (captured at app launch — no SafeAreaProvider
98
121
  // needed). On Android 15+/API 35+ the modal is drawn EDGE-TO-EDGE, and
@@ -106,7 +129,7 @@ export function KycSheet({
106
129
  // • iOS pageSheet: already below the status bar → no inset.
107
130
  // • iOS full-screen (disableClose): 56 covers the status bar + notch.
108
131
  const topInset =
109
- Platform.OS === 'android'
132
+ Platform.OS === "android"
110
133
  ? sa?.top || StatusBar.currentHeight || 28
111
134
  : iosFullScreen
112
135
  ? 56
@@ -114,95 +137,139 @@ export function KycSheet({
114
137
 
115
138
  // Clear the home indicator / nav bar at the bottom of a full-screen modal.
116
139
  const bottomInset =
117
- Platform.OS === 'android' ? sa?.bottom ?? 0 : iosFullScreen ? 34 : 0;
140
+ Platform.OS === "android" ? (sa?.bottom ?? 0) : iosFullScreen ? 34 : 0;
118
141
 
119
142
  return (
120
- <View style={{ flex: 1, backgroundColor: immersive ? '#000000' : colors.background }}>
143
+ <View
144
+ style={{
145
+ flex: 1,
146
+ backgroundColor: immersive ? "#000000" : colors.background,
147
+ }}
148
+ >
121
149
  {/* The SDK theme drives the status-bar glyph colour so it stays readable on
122
150
  the modal background — light glyphs in dark mode, dark in light mode.
123
151
  Android: JS StatusBar/SystemBars can't reach the <Modal>'s Dialog window,
124
152
  so a native view (StatusBarController) themes it from inside the modal.
125
153
  iOS: only the full-screen modal (disableClose) slides under the bar — the
126
154
  default pageSheet keeps the host bar above the card. */}
127
- {Platform.OS === 'android' ? (
128
- <StatusBarController barStyle={mode === 'dark' ? 'light' : 'dark'} />
155
+ {Platform.OS === "android" ? (
156
+ <StatusBarController barStyle={mode === "dark" ? "light" : "dark"} />
129
157
  ) : iosFullScreen ? (
130
- <StatusBar barStyle={mode === 'dark' ? 'light-content' : 'dark-content'} animated />
158
+ <StatusBar
159
+ barStyle={mode === "dark" ? "light-content" : "dark-content"}
160
+ animated
161
+ />
131
162
  ) : null}
132
163
  <View style={{ flex: 1 }}>
164
+ {/* Above the header block (and outside the immersive branch) so a
165
+ capture step that hides the chrome still says it is not live. It is
166
+ then the topmost element, so it — not the header — absorbs the
167
+ status-bar inset. */}
168
+ <SandboxBanner topInset={topInset} />
133
169
  {/* Header block (glass on iOS 26, tinted surface otherwise) — dropped
134
170
  entirely when the step owns the display. */}
135
- {immersive ? null : <GlassSurface
136
- glassStyle="regular"
137
- fallbackColor={tint}
138
- style={{ borderBottomWidth: 1, borderBottomColor: colors.border, paddingBottom: spacing.md }}
139
- >
140
- {/* Row 1 — brand + controls */}
141
- <View
171
+ {immersive ? null : (
172
+ <GlassSurface
173
+ glassStyle="regular"
174
+ fallbackColor={tint}
142
175
  style={{
143
- flexDirection: 'row',
144
- alignItems: 'center',
145
- paddingHorizontal: spacing.md,
146
- paddingTop: topInset + spacing.sm,
176
+ // The bar sits ON this edge and paints its own track, so the
177
+ // border would double it.
178
+ borderBottomWidth: showBar ? 0 : 1,
179
+ borderBottomColor: colors.border,
180
+ paddingBottom: headerPaddingBottom,
147
181
  }}
148
182
  >
149
- <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
150
- {showBrand ? <BrandBar logoUri={logoUri!} companyName={companyName} /> : null}
151
- </View>
183
+ {/* Row 1 — brand + controls */}
184
+ <View
185
+ style={{
186
+ flexDirection: "row",
187
+ alignItems: "center",
188
+ paddingHorizontal: spacing.md,
189
+ // The banner, when shown, already cleared the status bar above
190
+ // us — padding for it again would leave a gap the width of the
191
+ // notch between the strip and the brand row.
192
+ paddingTop: (bannerVisible ? 0 : topInset) + spacing.sm,
193
+ }}
194
+ >
195
+ <View
196
+ style={{ flex: 1, flexDirection: "row", alignItems: "center" }}
197
+ >
198
+ {showBrand ? (
199
+ <BrandBar logoUri={logoUri!} companyName={companyName} />
200
+ ) : null}
201
+ </View>
152
202
 
153
- {/* Header controls. When BOTH the theme toggle and close show, they
203
+ {/* Header controls. When BOTH the theme toggle and close show, they
154
204
  share a single Liquid Glass capsule (each button `plain`). When
155
205
  only one shows — the consumer disabled the other (`disableClose`
156
206
  or `showThemeToggle: false`) — that button stands on its own
157
207
  glass instead of a one-icon capsule. Neither → nothing. */}
158
- {(() => {
159
- const showToggle = config.showThemeToggle !== false;
160
- const showClose = !config.disableClose;
161
- const toggleBtn = showToggle ? (
162
- <GlassIconButton
163
- icon={mode === 'dark' ? 'sun' : 'moon'}
164
- onPress={toggle}
165
- solid
166
- plain={showClose}
167
- accessibilityLabel="Toggle theme"
168
- />
169
- ) : null;
170
- const closeBtn = showClose ? (
171
- <GlassIconButton
172
- icon="close"
173
- onPress={onClose}
174
- plain={showToggle}
175
- accessibilityLabel="Close"
176
- />
177
- ) : null;
208
+ {(() => {
209
+ const showToggle = config.showThemeToggle !== false;
210
+ const showClose = !config.disableClose;
211
+ const toggleBtn = showToggle ? (
212
+ <GlassIconButton
213
+ icon={mode === "dark" ? "sun" : "moon"}
214
+ onPress={toggle}
215
+ solid
216
+ plain={showClose}
217
+ accessibilityLabel="Toggle theme"
218
+ />
219
+ ) : null;
220
+ const closeBtn = showClose ? (
221
+ <GlassIconButton
222
+ icon="close"
223
+ onPress={onClose}
224
+ plain={showToggle}
225
+ accessibilityLabel="Close"
226
+ />
227
+ ) : null;
178
228
 
179
- if (showToggle && showClose) {
180
- return (
181
- <GlassGroup style={{ gap: 2 }}>
182
- {toggleBtn}
183
- {closeBtn}
184
- </GlassGroup>
185
- );
186
- }
187
- // Exactly one (or zero) — render it standalone with its own glass.
188
- return toggleBtn ?? closeBtn;
189
- })()}
190
- </View>
191
-
192
- {/* Row 2 — title + back + flag */}
193
- {title || onBack ? (
194
- <View style={{ paddingHorizontal: spacing.md, paddingTop: spacing.sm }}>
195
- <StepHeader title={title} description={description} onBack={onBack} country={country} />
229
+ if (showToggle && showClose) {
230
+ return (
231
+ <GlassGroup style={{ gap: 2 }}>
232
+ {toggleBtn}
233
+ {closeBtn}
234
+ </GlassGroup>
235
+ );
236
+ }
237
+ // Exactly one (or zero) — render it standalone with its own glass.
238
+ return toggleBtn ?? closeBtn;
239
+ })()}
196
240
  </View>
197
- ) : null}
198
241
 
199
- {/* Row 3 — step indicator */}
200
- {showIndicator ? (
201
- <View style={{ marginTop: spacing.md }}>
202
- <StepIndicator progress={progress!} stepCount={stepCount!} />
203
- </View>
204
- ) : null}
205
- </GlassSurface>}
242
+ {/* Row 2 — title + back + flag */}
243
+ {title || onBack ? (
244
+ <View
245
+ style={{
246
+ paddingHorizontal: spacing.md,
247
+ paddingTop: spacing.sm,
248
+ }}
249
+ >
250
+ <StepHeader
251
+ title={title}
252
+ description={description}
253
+ onBack={onBack}
254
+ country={country}
255
+ />
256
+ </View>
257
+ ) : null}
258
+
259
+ {/* Row 3 — step indicator */}
260
+ {showIndicator ? (
261
+ <View style={{ marginTop: spacing.md }}>
262
+ <StepIndicator progress={progress!} stepCount={stepCount!} />
263
+ </View>
264
+ ) : null}
265
+
266
+ {/* Absolutely positioned, so it overlays the header's bottom edge
267
+ instead of adding a row — the whole reason to prefer it. */}
268
+ {showBar ? (
269
+ <ProgressBar progress={progress!} stepCount={stepCount!} />
270
+ ) : null}
271
+ </GlassSurface>
272
+ )}
206
273
 
207
274
  {/* Step body */}
208
275
  {fillsViewport ? (
@@ -210,7 +277,9 @@ export function KycSheet({
210
277
  // padding exists so scrolled content clears the home indicator, but
211
278
  // in a fixed-height body it just lifts the list off the footer and
212
279
  // reads as a dead band. PoweredBy owns the safe-area inset already.
213
- <View style={{ flex: 1, padding: spacing.md, paddingBottom: spacing.md }}>
280
+ <View
281
+ style={{ flex: 1, padding: spacing.md, paddingBottom: spacing.md }}
282
+ >
214
283
  {children}
215
284
  </View>
216
285
  ) : (
@@ -236,7 +305,9 @@ export function KycSheet({
236
305
  // input into view — the platform's own handling, which a
237
306
  // KeyboardAvoidingView only approximates. Without this the
238
307
  // keyboard simply covered the bottom of every input step.
239
- automaticallyAdjustKeyboardInsets={Platform.OS === 'ios' && !immersive}
308
+ automaticallyAdjustKeyboardInsets={
309
+ Platform.OS === "ios" && !immersive
310
+ }
240
311
  >
241
312
  {children}
242
313
  </ScrollView>
@@ -262,93 +333,3 @@ export function KycSheet({
262
333
  </View>
263
334
  );
264
335
  }
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
-
272
- // Persistent header brand — circular logo avatar + company name. A broken/missing
273
- // logo image collapses the whole brand bar (mirrors the web SDK's `onError` and
274
- // Flutter's `errorBuilder`), so the header never shows a broken-image box.
275
- function BrandBar({ logoUri, companyName }: { logoUri: string; companyName: string }): React.ReactElement | null {
276
- const { colors } = useTheme();
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]);
299
- if (broken) return null;
300
- return (
301
- <>
302
- <View
303
- style={{
304
- width: 28,
305
- height: 28,
306
- borderRadius: radius.full,
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.
311
- borderWidth: 1,
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,
318
- overflow: 'hidden',
319
- marginRight: spacing.sm,
320
- alignItems: 'center',
321
- justifyContent: 'center',
322
- }}
323
- >
324
- {/* Fill + cover-crop the circle. The borderRadius is repeated on the
325
- Image itself because iOS doesn't reliably clip an Image child to a
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
- )}
348
- </View>
349
- <MyazaText variant="heading3" numberOfLines={1} color={colors.textDark} style={{ flexShrink: 1, fontSize: 14 }}>
350
- {companyName}
351
- </MyazaText>
352
- </>
353
- );
354
- }
@@ -1,10 +1,11 @@
1
1
  import React, { useRef } from 'react';
2
- import { Modal, Platform, Pressable, View } from 'react-native';
2
+ import { Platform, Pressable, View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
5
  import { Icon, type IconName } from './Icon';
6
6
  import { useTheme } from './runtime';
7
7
  import { MyazaText } from './Typography';
8
+ import { FloatingSheet } from './glass/FloatingSheet';
8
9
 
9
10
  /**
10
11
  * The SDK's own "where is the document?" sheet.
@@ -61,46 +62,16 @@ export function MediaSourceSheet({
61
62
  };
62
63
 
63
64
  return (
64
- <Modal
65
- transparent
65
+ <FloatingSheet
66
66
  visible={open}
67
- animationType="fade"
68
- onRequestClose={onClose}
67
+ onClose={onClose}
69
68
  onDismiss={() => {
70
69
  const run = pendingRef.current;
71
70
  pendingRef.current = null;
72
71
  run?.();
73
72
  }}
74
73
  >
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
-
74
+ <View style={{ padding: spacing.md, paddingTop: spacing.sm }}>
104
75
  <MyazaText variant="heading3" style={{ textAlign: 'center', marginBottom: spacing.md }}>
105
76
  {title}
106
77
  </MyazaText>
@@ -156,8 +127,7 @@ export function MediaSourceSheet({
156
127
  Cancel
157
128
  </MyazaText>
158
129
  </Pressable>
159
- </Pressable>
160
- </Pressable>
161
- </Modal>
130
+ </View>
131
+ </FloatingSheet>
162
132
  );
163
133
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { Modal, Pressable, ScrollView, View } from 'react-native';
2
+ import { Pressable, ScrollView, View } from 'react-native';
3
3
 
4
4
  import { radius, spacing } from '../config/theme';
5
5
  import {
@@ -13,6 +13,7 @@ import {
13
13
  import { Icon } from './Icon';
14
14
  import { useTheme } from './runtime';
15
15
  import { MyazaText } from './Typography';
16
+ import { FloatingSheet } from './glass/FloatingSheet';
16
17
 
17
18
  /**
18
19
  * A date field that opens a REAL calendar picker, replacing the "type
@@ -117,22 +118,8 @@ function DatePickerSheet({
117
118
  !!initial && initial.year === cursor.year && initial.month0 === cursor.month0 && initial.day === d;
118
119
 
119
120
  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
- >
121
+ <FloatingSheet visible onClose={onClose} closeLabel="Close date picker">
122
+ <View style={{ padding: spacing.md, paddingTop: spacing.sm }}>
136
123
  {/* Month header: step, or tap the title to jump years. */}
137
124
  <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }}>
138
125
  <Pressable
@@ -233,8 +220,7 @@ function DatePickerSheet({
233
220
  ))}
234
221
  </>
235
222
  )}
236
- </Pressable>
237
- </Pressable>
238
- </Modal>
223
+ </View>
224
+ </FloatingSheet>
239
225
  );
240
226
  }
@@ -11,6 +11,7 @@ type BlurHandler = NonNullable<TextInputProps['onBlur']>;
11
11
 
12
12
  import { radius, sizing, spacing } from '../config/theme';
13
13
  import { useTheme } from './runtime';
14
+ import { useInputFontFamily } from './fonts';
14
15
  import { MyazaText } from './Typography';
15
16
 
16
17
  // Branded text input with label, helper, and error states. Mirrors the Flutter
@@ -60,6 +61,9 @@ export function MyazaInput({
60
61
  onBlur,
61
62
  }: MyazaInputProps): React.ReactElement {
62
63
  const { colors } = useTheme();
64
+ // TextInput does not inherit fontFamily in RN — set it or the field (and its
65
+ // placeholder) renders in the system face beside brand-font text.
66
+ const fontFamily = useInputFontFamily();
63
67
  const [focused, setFocused] = useState(false);
64
68
 
65
69
  // Mirror the Flutter SDK's MyazaInput border states:
@@ -113,7 +117,7 @@ export function MyazaInput({
113
117
  autoFocus={autoFocus}
114
118
  onFocus={handleFocus}
115
119
  onBlur={handleBlur}
116
- style={{ flex: 1, height: '100%', color: colors.textDark, fontSize: 16 }}
120
+ style={{ flex: 1, height: '100%', color: colors.textDark, fontSize: 16, fontFamily }}
117
121
  />
118
122
  {suffix ? <View style={{ width: spacing.sm }} /> : null}
119
123
  {suffix}
@@ -142,6 +146,7 @@ export function MyazaInput({
142
146
  color: colors.textDark,
143
147
  backgroundColor: colors.background,
144
148
  fontSize: 16,
149
+ fontFamily,
145
150
  }}
146
151
  />
147
152
  )}