@ramonclaudio/create-vexpo 0.1.0 → 0.1.2
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/README.md +10 -10
- package/dist/index.js +8 -7
- package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
- package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
- package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
- package/dist/templates/default/.eas/workflows/release.yml +3 -7
- package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
- package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
- package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
- package/dist/templates/default/.github/workflows/check.yml +20 -12
- package/dist/templates/default/.maestro/launch.yaml +19 -10
- package/dist/templates/default/AGENTS.md +25 -8
- package/dist/templates/default/DESIGN.md +14 -10
- package/dist/templates/default/README.md +83 -78
- package/dist/templates/default/SETUP.md +159 -152
- package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
- package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
- package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
- package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
- package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
- package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
- package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
- package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
- package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
- package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
- package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
- package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
- package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
- package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
- package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
- package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
- package/dist/templates/default/_env.example +7 -7
- package/dist/templates/default/_gitattributes +1 -1
- package/dist/templates/default/_gitignore +17 -2
- package/dist/templates/default/_npmrc +7 -0
- package/dist/templates/default/_oxlintrc.json +1 -1
- package/dist/templates/default/app-store/accessibility.config.json +20 -0
- package/dist/templates/default/app-store/privacy.config.json +27 -0
- package/dist/templates/default/app.config.ts +105 -33
- package/dist/templates/default/app.json +1 -9
- package/dist/templates/default/convex/_generated/api.d.ts +12 -0
- package/dist/templates/default/convex/admin.ts +0 -13
- package/dist/templates/default/convex/appAttest.ts +467 -0
- package/dist/templates/default/convex/appAttestStore.ts +141 -0
- package/dist/templates/default/convex/apple.ts +53 -0
- package/dist/templates/default/convex/auth.ts +6 -45
- package/dist/templates/default/convex/constants.ts +2 -7
- package/dist/templates/default/convex/crons.ts +12 -5
- package/dist/templates/default/convex/email.ts +4 -24
- package/dist/templates/default/convex/env.ts +0 -4
- package/dist/templates/default/convex/errors.ts +0 -7
- package/dist/templates/default/convex/functions.ts +0 -26
- package/dist/templates/default/convex/http.ts +3 -5
- package/dist/templates/default/convex/log.ts +2 -25
- package/dist/templates/default/convex/pushSender.ts +145 -0
- package/dist/templates/default/convex/pushTokens.ts +110 -13
- package/dist/templates/default/convex/rateLimit.ts +8 -39
- package/dist/templates/default/convex/schema.ts +48 -5
- package/dist/templates/default/convex/tsconfig.json +1 -0
- package/dist/templates/default/convex/users.ts +143 -61
- package/dist/templates/default/convex/validators.ts +1 -38
- package/dist/templates/default/convex/webhook.ts +1 -31
- package/dist/templates/default/convex.json +1 -2
- package/dist/templates/default/metro.config.js +9 -1
- package/dist/templates/default/package.json +67 -70
- package/dist/templates/default/plugins/README.md +5 -1
- package/dist/templates/default/scripts/README.md +9 -9
- package/dist/templates/default/scripts/_run.mjs +3 -20
- package/dist/templates/default/scripts/clean.ts +81 -69
- package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
- package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
- package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
- package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
- package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
- package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
- package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
- package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
- package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
- package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
- package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
- package/dist/templates/default/src/app/+native-intent.tsx +25 -0
- package/dist/templates/default/src/app/+not-found.tsx +43 -0
- package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
- package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
- package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
- package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
- package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
- package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
- package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
- package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
- package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
- package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
- package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
- package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
- package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
- package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
- package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
- package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
- package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
- package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
- package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
- package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
- package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
- package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
- package/dist/templates/default/src/hooks/use-network.ts +34 -0
- package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
- package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
- package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
- package/dist/templates/default/src/lib/appAttest.ts +78 -0
- package/dist/templates/default/src/lib/assets.ts +9 -0
- package/dist/templates/default/src/lib/deep-link.ts +82 -0
- package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
- package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
- package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
- package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
- package/dist/templates/default/src/lib/masks.ts +21 -0
- package/dist/templates/default/src/lib/native-state.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
- package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
- package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
- package/dist/templates/default/src/lib/text-style.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
- package/dist/templates/default/store.config.json +1 -1
- package/dist/templates/default/tsconfig.json +3 -1
- package/dist/templates/default/vitest.config.ts +8 -1
- package/package.json +5 -5
- package/dist/templates/default/app/(app)/_layout.tsx +0 -73
- package/dist/templates/default/app/(app)/debug.tsx +0 -389
- package/dist/templates/default/app/(app)/sessions.tsx +0 -191
- package/dist/templates/default/app/(app)/welcome.tsx +0 -140
- package/dist/templates/default/app/+native-intent.tsx +0 -14
- package/dist/templates/default/app/+not-found.tsx +0 -51
- package/dist/templates/default/bun.lock +0 -1860
- package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
- package/dist/templates/default/components/ui/convex-error.tsx +0 -32
- package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
- package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
- package/dist/templates/default/components/ui/status-text.tsx +0 -49
- package/dist/templates/default/components/ui/update-banner.tsx +0 -82
- package/dist/templates/default/fingerprint.config.js +0 -9
- package/dist/templates/default/hooks/use-debounce.ts +0 -20
- package/dist/templates/default/hooks/use-deep-link.ts +0 -43
- package/dist/templates/default/hooks/use-network.ts +0 -11
- package/dist/templates/default/lib/assets.ts +0 -17
- package/dist/templates/default/lib/deep-link.ts +0 -71
- package/dist/templates/default/patches/PR-368.patch +0 -91
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
- /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Picker, Text } from "@expo/ui/swift-ui";
|
|
2
|
+
import {
|
|
3
|
+
accessibilityLabel,
|
|
4
|
+
controlSize,
|
|
5
|
+
dynamicTypeSize,
|
|
6
|
+
frame,
|
|
7
|
+
pickerStyle,
|
|
8
|
+
tag,
|
|
9
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
10
|
+
|
|
11
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
12
|
+
import { Button as ButtonTokens } from "@/constants/layout";
|
|
13
|
+
import { DynamicType } from "@/constants/ui";
|
|
14
|
+
import { haptics } from "@/lib/haptics";
|
|
15
|
+
|
|
16
|
+
export type SegmentedOption<T extends string> = {
|
|
17
|
+
value: T;
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Props<T extends string> = {
|
|
22
|
+
value: T;
|
|
23
|
+
options: SegmentedOption<T>[];
|
|
24
|
+
onChange: (value: T) => void;
|
|
25
|
+
// Group label spoken by VoiceOver before the segments. Required because a
|
|
26
|
+
// bare segmented Picker reads only the segment labels and would otherwise
|
|
27
|
+
// leave the user without context for what the group controls.
|
|
28
|
+
accessibilityLabel: string;
|
|
29
|
+
testID?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function SegmentedToggle<T extends string>({
|
|
33
|
+
value,
|
|
34
|
+
options,
|
|
35
|
+
onChange,
|
|
36
|
+
accessibilityLabel: a11yLabel,
|
|
37
|
+
testID,
|
|
38
|
+
}: Props<T>) {
|
|
39
|
+
const dfont = useDynamicFont();
|
|
40
|
+
return (
|
|
41
|
+
<Picker
|
|
42
|
+
testID={testID}
|
|
43
|
+
modifiers={[
|
|
44
|
+
pickerStyle("segmented"),
|
|
45
|
+
controlSize("large"),
|
|
46
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
47
|
+
// upstream expo/expo#46540: the two segments sit side by side and can't
|
|
48
|
+
// reflow, so cap Dynamic Type before the labels overflow at AX sizes.
|
|
49
|
+
dynamicTypeSize({ max: DynamicType.control }),
|
|
50
|
+
accessibilityLabel(a11yLabel),
|
|
51
|
+
]}
|
|
52
|
+
selection={value}
|
|
53
|
+
onSelectionChange={(selection) => {
|
|
54
|
+
const next = selection as T;
|
|
55
|
+
if (next === value) return;
|
|
56
|
+
haptics.selection();
|
|
57
|
+
onChange(next);
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{options.map((opt) => (
|
|
61
|
+
<Text
|
|
62
|
+
key={opt.value}
|
|
63
|
+
testID={testID ? `${testID}-${opt.value}` : undefined}
|
|
64
|
+
modifiers={[tag(opt.value), dfont({ size: 14, weight: "medium" })]}
|
|
65
|
+
>
|
|
66
|
+
{opt.label}
|
|
67
|
+
</Text>
|
|
68
|
+
))}
|
|
69
|
+
</Picker>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { ContentUnavailableView, Image, Text, VStack } from "@expo/ui/swift-ui";
|
|
3
|
+
import {
|
|
4
|
+
accessibilityHidden,
|
|
5
|
+
foregroundStyle,
|
|
6
|
+
frame,
|
|
7
|
+
multilineTextAlignment,
|
|
8
|
+
padding,
|
|
9
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
10
|
+
import type { SFSymbol } from "sf-symbols-typescript";
|
|
11
|
+
|
|
12
|
+
import { useColors } from "@/hooks/use-theme";
|
|
13
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
14
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
15
|
+
|
|
16
|
+
// `@expo/ui`'s ContentUnavailableView wraps SwiftUI's iOS 17+ view with no
|
|
17
|
+
// `else`, so it renders blank on the iOS 16.4-16.7 deployment floor. Branch to
|
|
18
|
+
// a hand-built layout there so every empty state still shows; iOS 17+ keeps the
|
|
19
|
+
// native view.
|
|
20
|
+
const NATIVE = Platform.OS === "ios" && Number.parseInt(String(Platform.Version), 10) >= 17;
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
title: string;
|
|
24
|
+
systemImage: SFSymbol;
|
|
25
|
+
description?: string;
|
|
26
|
+
testID?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function ContentUnavailable({ title, systemImage, description, testID }: Props) {
|
|
30
|
+
if (NATIVE) {
|
|
31
|
+
return (
|
|
32
|
+
<ContentUnavailableView
|
|
33
|
+
testID={testID}
|
|
34
|
+
title={title}
|
|
35
|
+
systemImage={systemImage}
|
|
36
|
+
description={description}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<Fallback title={title} systemImage={systemImage} description={description} testID={testID} />
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Fallback({ title, systemImage, description, testID }: Props) {
|
|
46
|
+
const dfont = useDynamicFont();
|
|
47
|
+
const symbolSize = useSymbolSize();
|
|
48
|
+
const colors = useColors();
|
|
49
|
+
return (
|
|
50
|
+
<VStack
|
|
51
|
+
spacing={8}
|
|
52
|
+
alignment="center"
|
|
53
|
+
modifiers={[frame({ maxWidth: Infinity }), padding({ vertical: 40, horizontal: 24 })]}
|
|
54
|
+
>
|
|
55
|
+
<Image
|
|
56
|
+
systemName={systemImage}
|
|
57
|
+
size={symbolSize(40)}
|
|
58
|
+
color={colors.mutedForeground as string}
|
|
59
|
+
modifiers={[accessibilityHidden(true)]}
|
|
60
|
+
/>
|
|
61
|
+
<Text
|
|
62
|
+
testID={description ? undefined : testID}
|
|
63
|
+
modifiers={[dfont({ size: 17, weight: "semibold" }), multilineTextAlignment("center")]}
|
|
64
|
+
>
|
|
65
|
+
{title}
|
|
66
|
+
</Text>
|
|
67
|
+
{description ? (
|
|
68
|
+
<Text
|
|
69
|
+
testID={testID}
|
|
70
|
+
modifiers={[
|
|
71
|
+
dfont({ size: 14 }),
|
|
72
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
73
|
+
multilineTextAlignment("center"),
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
{description}
|
|
77
|
+
</Text>
|
|
78
|
+
) : null}
|
|
79
|
+
</VStack>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ConvexError } from "convex/values";
|
|
2
|
+
|
|
3
|
+
import { ErrorText } from "./status-text";
|
|
4
|
+
|
|
5
|
+
export function formatError(err: unknown): string {
|
|
6
|
+
if (err instanceof ConvexError) {
|
|
7
|
+
const data = err.data as unknown;
|
|
8
|
+
if (typeof data === "object" && data !== null && "message" in data) {
|
|
9
|
+
const msg = (data as { message?: unknown }).message;
|
|
10
|
+
if (typeof msg === "string" && msg.length > 0) return msg;
|
|
11
|
+
}
|
|
12
|
+
return err.message;
|
|
13
|
+
}
|
|
14
|
+
if (err instanceof Error) return err.message;
|
|
15
|
+
return "An unexpected error occurred";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ConvexErrorView({ error, testID }: { error: unknown; testID?: string }) {
|
|
19
|
+
if (error === undefined || error === null) return null;
|
|
20
|
+
return <ErrorText testID={testID}>{formatError(error)}</ErrorText>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { AccessibilityInfo } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { router, type ErrorBoundaryProps } from "expo-router";
|
|
5
|
+
import { Host, VStack, Text, Button, Image, Spacer } from "@expo/ui/swift-ui";
|
|
6
|
+
import {
|
|
7
|
+
accessibilityHidden,
|
|
8
|
+
foregroundStyle,
|
|
9
|
+
buttonStyle,
|
|
10
|
+
frame,
|
|
11
|
+
padding,
|
|
12
|
+
multilineTextAlignment,
|
|
13
|
+
tint,
|
|
14
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
15
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
16
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
17
|
+
import { ProminentButton } from "@/components/ui/prominent-button";
|
|
18
|
+
import { useColors } from "@/hooks/use-theme";
|
|
19
|
+
import { TouchTarget } from "@/constants/layout";
|
|
20
|
+
|
|
21
|
+
export function AppErrorBoundary({
|
|
22
|
+
error,
|
|
23
|
+
retry,
|
|
24
|
+
testID,
|
|
25
|
+
}: ErrorBoundaryProps & { testID?: string }) {
|
|
26
|
+
const dfont = useDynamicFont();
|
|
27
|
+
const symbolSize = useSymbolSize();
|
|
28
|
+
const colors = useColors();
|
|
29
|
+
const insets = useSafeAreaInsets();
|
|
30
|
+
|
|
31
|
+
// VoiceOver users won't notice the visual change to a destructive surface
|
|
32
|
+
// unless we explicitly announce. Fires once on mount per crash. Logging the
|
|
33
|
+
// error here too keeps it off the render path: a mounted boundary re-renders
|
|
34
|
+
// on theme/fontScale changes, and a render-body log would re-fire each time.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (__DEV__) console.error("[ErrorBoundary]", error);
|
|
37
|
+
AccessibilityInfo.announceForAccessibility("Error: something went wrong");
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Host style={{ flex: 1 }}>
|
|
43
|
+
<VStack
|
|
44
|
+
spacing={20}
|
|
45
|
+
alignment="center"
|
|
46
|
+
modifiers={[
|
|
47
|
+
padding({ horizontal: 24, top: insets.top + 32, bottom: insets.bottom + 32 }),
|
|
48
|
+
tint(colors.primary as string),
|
|
49
|
+
]}
|
|
50
|
+
>
|
|
51
|
+
<Spacer />
|
|
52
|
+
<Image
|
|
53
|
+
systemName="exclamationmark.triangle"
|
|
54
|
+
size={symbolSize(72)}
|
|
55
|
+
color={colors.destructive as string}
|
|
56
|
+
modifiers={[accessibilityHidden(true)]}
|
|
57
|
+
/>
|
|
58
|
+
<Text modifiers={[dfont({ size: 28, weight: "bold" }), multilineTextAlignment("center")]}>
|
|
59
|
+
Something went wrong
|
|
60
|
+
</Text>
|
|
61
|
+
<Text
|
|
62
|
+
testID={testID}
|
|
63
|
+
modifiers={[
|
|
64
|
+
dfont({ size: 16 }),
|
|
65
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
66
|
+
multilineTextAlignment("center"),
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
Don't worry. Let's get you back on track.
|
|
70
|
+
</Text>
|
|
71
|
+
<VStack spacing={12} modifiers={[frame({ maxWidth: Infinity })]}>
|
|
72
|
+
<ProminentButton testID="error-boundary-retry" label="Try Again" onPress={retry} />
|
|
73
|
+
<Button
|
|
74
|
+
testID="error-boundary-home"
|
|
75
|
+
label="Go Home"
|
|
76
|
+
modifiers={[
|
|
77
|
+
buttonStyle("plain"),
|
|
78
|
+
dfont({ size: 16, weight: "medium" }),
|
|
79
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
80
|
+
frame({ minHeight: TouchTarget.min }),
|
|
81
|
+
]}
|
|
82
|
+
onPress={() => router.replace("/")}
|
|
83
|
+
/>
|
|
84
|
+
</VStack>
|
|
85
|
+
<Spacer />
|
|
86
|
+
</VStack>
|
|
87
|
+
</Host>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Image as ExpoImage } from "expo-image";
|
|
2
2
|
import { Host, ProgressView, Spacer, VStack, RNHostView } from "@expo/ui/swift-ui";
|
|
3
|
-
import { progressViewStyle, tint } from "@expo/ui/swift-ui/modifiers";
|
|
3
|
+
import { accessibilityLabel, progressViewStyle, tint } from "@expo/ui/swift-ui/modifiers";
|
|
4
4
|
|
|
5
5
|
import { assets } from "@/lib/assets";
|
|
6
6
|
import { useColors, useThemedAsset } from "@/hooks/use-theme";
|
|
7
7
|
|
|
8
|
-
export function LoadingScreen() {
|
|
8
|
+
export function LoadingScreen({ testID }: { testID?: string } = {}) {
|
|
9
9
|
const colors = useColors();
|
|
10
10
|
const brandIcon = useThemedAsset(assets.brandIconLight, assets.brandIconDark);
|
|
11
11
|
return (
|
|
12
12
|
<Host
|
|
13
|
+
testID={testID}
|
|
13
14
|
style={{ flex: 1, backgroundColor: colors.background as string }}
|
|
14
15
|
useViewportSizeMeasurement
|
|
15
16
|
>
|
|
@@ -20,10 +21,10 @@ export function LoadingScreen() {
|
|
|
20
21
|
source={brandIcon}
|
|
21
22
|
style={{ width: 80, height: 80 }}
|
|
22
23
|
contentFit="contain"
|
|
23
|
-
accessibilityLabel="
|
|
24
|
+
accessibilityLabel=""
|
|
24
25
|
/>
|
|
25
26
|
</RNHostView>
|
|
26
|
-
<ProgressView modifiers={[progressViewStyle("circular")]} />
|
|
27
|
+
<ProgressView modifiers={[progressViewStyle("circular"), accessibilityLabel("Loading")]} />
|
|
27
28
|
<Spacer />
|
|
28
29
|
</VStack>
|
|
29
30
|
</Host>
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View, type
|
|
2
|
+
import { StyleSheet, View, type ViewProps } from "react-native";
|
|
3
3
|
import { BlurView, type BlurTint } from "expo-blur";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
GlassView,
|
|
6
|
+
isGlassEffectAPIAvailable,
|
|
7
|
+
isLiquidGlassAvailable,
|
|
8
|
+
type GlassStyle,
|
|
9
|
+
} from "expo-glass-effect";
|
|
10
|
+
|
|
11
|
+
import { useReduceTransparency } from "@/hooks/use-reduce-transparency";
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
|
-
* HIG-aware translucent surface. Picks the right backing per OS
|
|
14
|
+
* HIG-aware translucent surface. Picks the right backing per OS and
|
|
15
|
+
* accessibility state:
|
|
16
|
+
*
|
|
17
|
+
* Reduce Transparency on -> solid `tintColor` background (no blur)
|
|
18
|
+
* iOS 26+ -> `GlassView` (true Liquid Glass via UIVisualEffectView)
|
|
19
|
+
* iOS 16.4-25 -> `BlurView` (UIVisualEffectView blur + tint overlay)
|
|
20
|
+
* anything else -> solid `tintColor` fallback
|
|
8
21
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
22
|
+
* iOS 26 `GlassView` honors Reduce Transparency natively, so the explicit
|
|
23
|
+
* check only changes the iOS 16.4-25 path. But we route both through the
|
|
24
|
+
* same solid-fallback for consistency and so the iOS 26 path stays cheap
|
|
25
|
+
* when the user has opted out of blur.
|
|
12
26
|
*
|
|
13
27
|
* Apple's HIG reserves materials for the navigation layer that floats above
|
|
14
28
|
* content: tab bars, navigation bars, toolbars, sheets, popovers, alerts,
|
|
@@ -49,23 +63,42 @@ const GLASS_STYLE: Record<MaterialVariant, GlassStyle> = {
|
|
|
49
63
|
|
|
50
64
|
const TINT_OVERLAY_OPACITY = 0.35;
|
|
51
65
|
|
|
66
|
+
export type MaterialProps = ViewProps & {
|
|
67
|
+
children?: ReactNode;
|
|
68
|
+
variant?: MaterialVariant;
|
|
69
|
+
tintColor?: string;
|
|
70
|
+
isInteractive?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
52
73
|
export function Material({
|
|
53
74
|
children,
|
|
54
|
-
style,
|
|
55
75
|
variant = "regular",
|
|
56
76
|
tintColor,
|
|
57
77
|
isInteractive = false,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
...viewProps
|
|
79
|
+
}: MaterialProps) {
|
|
80
|
+
const reduceTransparency = useReduceTransparency();
|
|
81
|
+
|
|
82
|
+
if (reduceTransparency) {
|
|
83
|
+
return (
|
|
84
|
+
<View
|
|
85
|
+
{...viewProps}
|
|
86
|
+
style={[viewProps.style, { backgroundColor: tintColor ?? "rgba(0,0,0,0.85)" }]}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// `isLiquidGlassAvailable()` confirms the SDK + Info.plist support Liquid
|
|
94
|
+
// Glass; `isGlassEffectAPIAvailable()` confirms the runtime device actually
|
|
95
|
+
// has the API. Some iOS 26 beta builds pass the version check without the
|
|
96
|
+
// runtime API and crash on GlassView. Both must be true. See
|
|
97
|
+
// https://github.com/expo/expo/issues/40911.
|
|
98
|
+
if (isLiquidGlassAvailable() && isGlassEffectAPIAvailable()) {
|
|
66
99
|
return (
|
|
67
100
|
<GlassView
|
|
68
|
-
|
|
101
|
+
{...viewProps}
|
|
69
102
|
glassEffectStyle={GLASS_STYLE[variant]}
|
|
70
103
|
tintColor={tintColor}
|
|
71
104
|
isInteractive={isInteractive}
|
|
@@ -76,7 +109,7 @@ export function Material({
|
|
|
76
109
|
}
|
|
77
110
|
|
|
78
111
|
return (
|
|
79
|
-
<BlurView
|
|
112
|
+
<BlurView {...viewProps} intensity={BLUR_INTENSITY[variant]} tint={BLUR_TINT[variant]}>
|
|
80
113
|
{tintColor ? (
|
|
81
114
|
<View
|
|
82
115
|
style={[
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
2
|
+
import { Host, Text } from "@expo/ui/swift-ui";
|
|
3
|
+
import { foregroundStyle } from "@expo/ui/swift-ui/modifiers";
|
|
4
|
+
|
|
5
|
+
import { Material } from "@/components/ui/material";
|
|
6
|
+
import { useNetwork } from "@/hooks/use-network";
|
|
7
|
+
import { Spacing, FontSize } from "@/constants/layout";
|
|
8
|
+
import { Radius } from "@/constants/theme";
|
|
9
|
+
import { ZIndex } from "@/constants/ui";
|
|
10
|
+
import { useColors } from "@/hooks/use-theme";
|
|
11
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
12
|
+
|
|
13
|
+
// HIG: notification banners overlay the navigation layer with a translucent
|
|
14
|
+
// material so context behind the alert remains visible. `Material` carries
|
|
15
|
+
// positioning, the live-region announcement, and the chrome surface in one
|
|
16
|
+
// shot. The visible label renders through `Host` so it uses SwiftUI's text
|
|
17
|
+
// system and respects Dynamic Type.
|
|
18
|
+
export function OfflineBanner({ testID }: { testID?: string } = {}) {
|
|
19
|
+
const { isOffline } = useNetwork();
|
|
20
|
+
const insets = useSafeAreaInsets();
|
|
21
|
+
const colors = useColors();
|
|
22
|
+
const dfont = useDynamicFont();
|
|
23
|
+
|
|
24
|
+
if (!isOffline) return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Material
|
|
28
|
+
accessibilityLiveRegion="assertive"
|
|
29
|
+
accessibilityRole="alert"
|
|
30
|
+
accessibilityLabel="You're offline"
|
|
31
|
+
variant="chrome"
|
|
32
|
+
tintColor={colors.destructive as string}
|
|
33
|
+
style={{
|
|
34
|
+
position: "absolute",
|
|
35
|
+
top: insets.top + Spacing.xs,
|
|
36
|
+
left: Spacing.md,
|
|
37
|
+
right: Spacing.md,
|
|
38
|
+
zIndex: ZIndex.offlineBanner,
|
|
39
|
+
borderRadius: Radius.full,
|
|
40
|
+
overflow: "hidden",
|
|
41
|
+
paddingVertical: Spacing.sm,
|
|
42
|
+
paddingHorizontal: Spacing.lg,
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Host matchContents>
|
|
47
|
+
<Text
|
|
48
|
+
testID={testID}
|
|
49
|
+
modifiers={[
|
|
50
|
+
dfont({ size: FontSize["3xl"], weight: "bold" }),
|
|
51
|
+
foregroundStyle(colors.destructiveForeground as string),
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
You're offline
|
|
55
|
+
</Text>
|
|
56
|
+
</Host>
|
|
57
|
+
</Material>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -13,9 +13,6 @@ import { useDynamicFont } from "@/lib/dynamic-font";
|
|
|
13
13
|
import { Button as ButtonTokens } from "@/constants/layout";
|
|
14
14
|
import { useColors } from "@/hooks/use-theme";
|
|
15
15
|
|
|
16
|
-
// Full-width prominent action button. Capsule shape, shadcn `primary` fill,
|
|
17
|
-
// `primaryForeground` Geist bold label.
|
|
18
|
-
//
|
|
19
16
|
// Why this isn't `buttonStyle("borderedProminent")`:
|
|
20
17
|
// SwiftUI's borderedProminent paints the bg with the tint color but hardcodes
|
|
21
18
|
// the label foreground to `.white`. Our shadcn `primary` is near-white in dark
|
|
@@ -28,28 +25,28 @@ import { useColors } from "@/hooks/use-theme";
|
|
|
28
25
|
// SwiftUI's Button label is content-sized. `frame(maxWidth:.infinity)` on the
|
|
29
26
|
// Button itself wraps the styled button in an invisible flex frame without
|
|
30
27
|
// expanding it. Putting the frame on the LABEL inside the button is the fix.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// the SwiftUI button's content-sizing logic, leaving the button content-sized.
|
|
35
|
-
// A large finite number behaves as effectively infinite (capped by the parent
|
|
36
|
-
// VStack's available width) and is honored by the bridge.
|
|
28
|
+
// The value is `Infinity` (SwiftUI's `.frame(maxWidth: .infinity)` fill idiom);
|
|
29
|
+
// it survives the @expo/ui modifier bridge intact and is what Expo ships in its
|
|
30
|
+
// own ScrollView/BottomSheet and documents in tabview.mdx.
|
|
37
31
|
export function ProminentButton({
|
|
38
32
|
label,
|
|
39
33
|
onPress,
|
|
40
34
|
disabled,
|
|
35
|
+
testID,
|
|
41
36
|
}: {
|
|
42
37
|
label: string;
|
|
43
38
|
onPress: () => void;
|
|
44
39
|
disabled?: boolean;
|
|
40
|
+
testID?: string;
|
|
45
41
|
}) {
|
|
46
42
|
const dfont = useDynamicFont();
|
|
47
43
|
const colors = useColors();
|
|
48
44
|
return (
|
|
49
45
|
<Button
|
|
46
|
+
testID={testID}
|
|
50
47
|
modifiers={[
|
|
51
48
|
buttonStyle("plain"),
|
|
52
|
-
frame({ maxWidth:
|
|
49
|
+
frame({ maxWidth: Infinity }),
|
|
53
50
|
background(colors.primary as string),
|
|
54
51
|
clipShape("capsule"),
|
|
55
52
|
disabledModifier(disabled ?? false),
|
|
@@ -58,7 +55,7 @@ export function ProminentButton({
|
|
|
58
55
|
>
|
|
59
56
|
<Text
|
|
60
57
|
modifiers={[
|
|
61
|
-
frame({ maxWidth:
|
|
58
|
+
frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
|
|
62
59
|
multilineTextAlignment("center"),
|
|
63
60
|
dfont({ size: ButtonTokens.fontSize, weight: ButtonTokens.fontWeight }),
|
|
64
61
|
foregroundStyle(colors.primaryForeground as string),
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { VStack, HStack, Spacer, Text } from "@expo/ui/swift-ui";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
accessibilityHidden,
|
|
4
|
+
background,
|
|
5
|
+
clipShape,
|
|
6
|
+
cornerRadius,
|
|
7
|
+
frame,
|
|
8
|
+
padding,
|
|
9
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
3
10
|
|
|
4
11
|
import { Spacing } from "@/constants/layout";
|
|
5
12
|
import { useColors } from "@/hooks/use-theme";
|
|
@@ -7,8 +14,11 @@ import { useColors } from "@/hooks/use-theme";
|
|
|
7
14
|
// Skeleton placeholders for initial query loads. SwiftUI-native: filled
|
|
8
15
|
// muted-color boxes laid out in the shape of the screen they're standing
|
|
9
16
|
// in for. No animation. SwiftUI's `Host` doesn't ergonomically support
|
|
10
|
-
// per-tick opacity tweens, and static skeletons satisfy the
|
|
11
|
-
//
|
|
17
|
+
// per-tick opacity tweens, and static skeletons satisfy the Reduce Motion
|
|
18
|
+
// accessibility setting trivially (nothing to suppress). The whitespace
|
|
19
|
+
// `Text` inside each bar forces SwiftUI to render the framed VStack with
|
|
20
|
+
// its background fill, and the placeholders use `accessibilityHidden(true)`
|
|
21
|
+
// (shipped upstream in expo/expo#46579) to drop them from the spoken hierarchy.
|
|
12
22
|
|
|
13
23
|
type BarProps = {
|
|
14
24
|
width: number | "fill";
|
|
@@ -24,9 +34,10 @@ function Bar({ width, height, radius = 6 }: BarProps): React.ReactNode {
|
|
|
24
34
|
frame(width === "fill" ? { maxWidth: Infinity, height } : { width, height }),
|
|
25
35
|
background(colors.muted as string),
|
|
26
36
|
cornerRadius(radius),
|
|
37
|
+
accessibilityHidden(true),
|
|
27
38
|
]}
|
|
28
39
|
>
|
|
29
|
-
<Text> </Text>
|
|
40
|
+
<Text modifiers={[accessibilityHidden(true)]}> </Text>
|
|
30
41
|
</VStack>
|
|
31
42
|
);
|
|
32
43
|
}
|
|
@@ -39,18 +50,22 @@ function Circle({ size }: { size: number }): React.ReactNode {
|
|
|
39
50
|
frame({ width: size, height: size }),
|
|
40
51
|
background(colors.muted as string),
|
|
41
52
|
clipShape("circle"),
|
|
53
|
+
accessibilityHidden(true),
|
|
42
54
|
]}
|
|
43
55
|
>
|
|
44
|
-
<Text> </Text>
|
|
56
|
+
<Text modifiers={[accessibilityHidden(true)]}> </Text>
|
|
45
57
|
</VStack>
|
|
46
58
|
);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
// avatar row + display-name row + email row + sign-in-method row.
|
|
51
|
-
export function SkeletonProfile(): React.ReactNode {
|
|
61
|
+
export function SkeletonProfile({ testID }: { testID?: string } = {}): React.ReactNode {
|
|
52
62
|
return (
|
|
53
|
-
<VStack
|
|
63
|
+
<VStack
|
|
64
|
+
testID={testID}
|
|
65
|
+
alignment="leading"
|
|
66
|
+
spacing={Spacing.xl}
|
|
67
|
+
modifiers={[padding({ all: 24 })]}
|
|
68
|
+
>
|
|
54
69
|
<HStack spacing={Spacing.lg}>
|
|
55
70
|
<Circle size={72} />
|
|
56
71
|
<VStack alignment="leading" spacing={Spacing.sm}>
|
|
@@ -75,11 +90,14 @@ export function SkeletonProfile(): React.ReactNode {
|
|
|
75
90
|
);
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
// device-by-device shape in `app/(app)/sessions.tsx`.
|
|
80
|
-
export function SkeletonSessions(): React.ReactNode {
|
|
93
|
+
export function SkeletonSessions({ testID }: { testID?: string } = {}): React.ReactNode {
|
|
81
94
|
return (
|
|
82
|
-
<VStack
|
|
95
|
+
<VStack
|
|
96
|
+
testID={testID}
|
|
97
|
+
alignment="leading"
|
|
98
|
+
spacing={Spacing.md}
|
|
99
|
+
modifiers={[padding({ all: 24 })]}
|
|
100
|
+
>
|
|
83
101
|
<SkeletonSessionRow />
|
|
84
102
|
<SkeletonSessionRow />
|
|
85
103
|
<SkeletonSessionRow />
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { AccessibilityInfo } from "react-native";
|
|
3
|
+
import { HStack, Image, Text } from "@expo/ui/swift-ui";
|
|
4
|
+
import { accessibilityHidden, foregroundStyle } from "@expo/ui/swift-ui/modifiers";
|
|
5
|
+
|
|
6
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
7
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
8
|
+
import { Colors } from "@/constants/theme";
|
|
9
|
+
|
|
10
|
+
type Props = { children: string; size?: number; testID?: string };
|
|
11
|
+
|
|
12
|
+
function announce(prefix: string, message: string) {
|
|
13
|
+
AccessibilityInfo.announceForAccessibility(`${prefix}: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ErrorText({ children, size = 14, testID }: Props) {
|
|
17
|
+
const dfont = useDynamicFont();
|
|
18
|
+
const symbolSize = useSymbolSize();
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
announce("Error", children);
|
|
21
|
+
}, [children]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<HStack spacing={6} alignment="center">
|
|
25
|
+
<Image
|
|
26
|
+
systemName="exclamationmark.triangle.fill"
|
|
27
|
+
size={symbolSize(size)}
|
|
28
|
+
color={Colors.destructive as string}
|
|
29
|
+
modifiers={[accessibilityHidden(true)]}
|
|
30
|
+
/>
|
|
31
|
+
<Text
|
|
32
|
+
testID={testID}
|
|
33
|
+
modifiers={[dfont({ size }), foregroundStyle(Colors.destructive as string)]}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Text>
|
|
37
|
+
</HStack>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function SuccessText({ children, size = 14, testID }: Props) {
|
|
42
|
+
const dfont = useDynamicFont();
|
|
43
|
+
const symbolSize = useSymbolSize();
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
announce("Success", children);
|
|
46
|
+
}, [children]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<HStack spacing={6} alignment="center">
|
|
50
|
+
<Image
|
|
51
|
+
systemName="checkmark.circle.fill"
|
|
52
|
+
size={symbolSize(size)}
|
|
53
|
+
color={Colors.success as string}
|
|
54
|
+
modifiers={[accessibilityHidden(true)]}
|
|
55
|
+
/>
|
|
56
|
+
<Text
|
|
57
|
+
testID={testID}
|
|
58
|
+
modifiers={[dfont({ size }), foregroundStyle(Colors.success as string)]}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</Text>
|
|
62
|
+
</HStack>
|
|
63
|
+
);
|
|
64
|
+
}
|