@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.
- package/package.json +1 -2
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DialCodePicker.tsx +11 -17
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +5 -0
- package/src/components/KycSheet.tsx +161 -180
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +6 -1
- package/src/components/MyazaSelect.tsx +20 -39
- package/src/components/PoweredBy.tsx +20 -15
- package/src/components/ProgressBar.tsx +91 -0
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepIndicator.tsx +145 -31
- package/src/components/fonts.ts +23 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +190 -0
- package/src/components/glass/GlassSheet.tsx +38 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
- package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
- package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
- package/src/config/questionnaire.ts +45 -4
- package/src/config/workflowMerge.ts +1 -0
- package/src/emrtd/crypto.ts +1 -1
- package/src/emrtd/dg1.ts +55 -0
- package/src/emrtd/ec-curves.ts +142 -0
- package/src/emrtd/ec.ts +196 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/mrzKey.ts +21 -1
- package/src/emrtd/open.ts +169 -0
- package/src/emrtd/pace-params.ts +169 -0
- package/src/emrtd/pace.ts +295 -0
- package/src/emrtd/secureMessaging.ts +32 -14
- package/src/emrtd/session.ts +124 -20
- package/src/emrtd/suites.ts +99 -0
- package/src/index.ts +1 -0
- package/src/lib/step-log.ts +43 -0
- package/src/lib/step-window.ts +96 -0
- package/src/liveness/useLiveness.ts +1 -1
- package/src/screens/IdTypeStep.tsx +15 -2
- package/src/screens/NfcStep.tsx +12 -0
- package/src/screens/QuestionnaireField.tsx +30 -0
- package/src/screens/QuestionnaireStep.tsx +3 -1
- package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
- package/src/services/deviceMetadata.ts +9 -1
- package/src/store/derive.ts +7 -0
- package/src/store/kycStore.ts +25 -1
- package/src/types/config.ts +22 -0
- package/src/types/workflow.ts +10 -0
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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,
|
|
8
|
-
import { useKeyboardInset } from
|
|
9
|
-
import type { SupportedCountry } from
|
|
10
|
-
import { useKycConfig, useTheme } from
|
|
11
|
-
import { useBranding } from
|
|
12
|
-
import {
|
|
13
|
-
import { PoweredBy } from
|
|
14
|
-
import { StepHeader } from
|
|
15
|
-
import { StepIndicator } from
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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
|
-
|
|
87
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
140
|
+
Platform.OS === "android" ? (sa?.bottom ?? 0) : iosFullScreen ? 34 : 0;
|
|
118
141
|
|
|
119
142
|
return (
|
|
120
|
-
<View
|
|
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 ===
|
|
128
|
-
<StatusBarController barStyle={mode ===
|
|
155
|
+
{Platform.OS === "android" ? (
|
|
156
|
+
<StatusBarController barStyle={mode === "dark" ? "light" : "dark"} />
|
|
129
157
|
) : iosFullScreen ? (
|
|
130
|
-
<StatusBar
|
|
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 :
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
>
|
|
140
|
-
{/* Row 1 — brand + controls */}
|
|
141
|
-
<View
|
|
171
|
+
{immersive ? null : (
|
|
172
|
+
<GlassSurface
|
|
173
|
+
glassStyle="regular"
|
|
174
|
+
fallbackColor={tint}
|
|
142
175
|
style={{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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={
|
|
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 {
|
|
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
|
-
<
|
|
65
|
-
transparent
|
|
65
|
+
<FloatingSheet
|
|
66
66
|
visible={open}
|
|
67
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
</Modal>
|
|
130
|
+
</View>
|
|
131
|
+
</FloatingSheet>
|
|
162
132
|
);
|
|
163
133
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
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
|
-
<
|
|
121
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
)}
|