@ramonclaudio/create-vexpo 0.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.
- package/README.md +50 -0
- package/dist/index.js +183 -0
- package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
- package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
- package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
- package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
- package/dist/templates/default/.eas/workflows/release.yml +44 -0
- package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
- package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
- package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
- package/dist/templates/default/.github/workflows/check.yml +28 -0
- package/dist/templates/default/.maestro/launch.yaml +18 -0
- package/dist/templates/default/AGENTS.md +79 -0
- package/dist/templates/default/DESIGN.md +331 -0
- package/dist/templates/default/LICENSE +21 -0
- package/dist/templates/default/README.md +153 -0
- package/dist/templates/default/SETUP.md +618 -0
- package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
- package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
- package/dist/templates/default/_easignore +22 -0
- package/dist/templates/default/_editorconfig +9 -0
- package/dist/templates/default/_env.example +34 -0
- package/dist/templates/default/_fingerprintignore +24 -0
- package/dist/templates/default/_gitattributes +7 -0
- package/dist/templates/default/_gitignore +69 -0
- package/dist/templates/default/_oxfmtrc.json +3 -0
- package/dist/templates/default/_oxlintrc.json +34 -0
- package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
- package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
- package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
- package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
- package/dist/templates/default/app/(app)/_layout.tsx +73 -0
- package/dist/templates/default/app/(app)/debug.tsx +389 -0
- package/dist/templates/default/app/(app)/help.tsx +254 -0
- package/dist/templates/default/app/(app)/linked.tsx +116 -0
- package/dist/templates/default/app/(app)/privacy.tsx +159 -0
- package/dist/templates/default/app/(app)/profile.tsx +915 -0
- package/dist/templates/default/app/(app)/sessions.tsx +191 -0
- package/dist/templates/default/app/(app)/welcome.tsx +140 -0
- package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
- package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
- package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
- package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
- package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
- package/dist/templates/default/app/+native-intent.tsx +14 -0
- package/dist/templates/default/app/+not-found.tsx +51 -0
- package/dist/templates/default/app/_layout.tsx +102 -0
- package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
- package/dist/templates/default/app-store/screenshots/README.md +13 -0
- package/dist/templates/default/app.config.ts +201 -0
- package/dist/templates/default/app.json +11 -0
- package/dist/templates/default/assets/brand-icon-dark.png +0 -0
- package/dist/templates/default/assets/brand-icon-light.png +0 -0
- package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
- package/dist/templates/default/assets/icon.png +0 -0
- package/dist/templates/default/assets/sounds/notification.wav +0 -0
- package/dist/templates/default/assets/splash-image-dark.png +0 -0
- package/dist/templates/default/assets/splash-image-light.png +0 -0
- package/dist/templates/default/bun.lock +1860 -0
- package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
- package/dist/templates/default/components/auth/password-field.tsx +121 -0
- package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
- package/dist/templates/default/components/ui/convex-error.tsx +32 -0
- package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
- package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
- package/dist/templates/default/components/ui/material.tsx +94 -0
- package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
- package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
- package/dist/templates/default/components/ui/skeleton.tsx +107 -0
- package/dist/templates/default/components/ui/status-text.tsx +49 -0
- package/dist/templates/default/components/ui/update-banner.tsx +82 -0
- package/dist/templates/default/constants/layout.ts +102 -0
- package/dist/templates/default/constants/theme.ts +401 -0
- package/dist/templates/default/constants/ui.ts +77 -0
- package/dist/templates/default/convex/_generated/api.d.ts +77 -0
- package/dist/templates/default/convex/_generated/api.js +23 -0
- package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
- package/dist/templates/default/convex/_generated/server.d.ts +143 -0
- package/dist/templates/default/convex/_generated/server.js +93 -0
- package/dist/templates/default/convex/admin.ts +102 -0
- package/dist/templates/default/convex/auth.config.ts +6 -0
- package/dist/templates/default/convex/auth.ts +335 -0
- package/dist/templates/default/convex/constants.ts +46 -0
- package/dist/templates/default/convex/convex.config.ts +11 -0
- package/dist/templates/default/convex/crons.ts +42 -0
- package/dist/templates/default/convex/email.ts +109 -0
- package/dist/templates/default/convex/env.ts +31 -0
- package/dist/templates/default/convex/errors.ts +33 -0
- package/dist/templates/default/convex/functions.ts +54 -0
- package/dist/templates/default/convex/http.ts +176 -0
- package/dist/templates/default/convex/log.ts +81 -0
- package/dist/templates/default/convex/pushTokens.ts +114 -0
- package/dist/templates/default/convex/rateLimit.ts +92 -0
- package/dist/templates/default/convex/schema.ts +28 -0
- package/dist/templates/default/convex/tsconfig.json +18 -0
- package/dist/templates/default/convex/users.ts +279 -0
- package/dist/templates/default/convex/validators.ts +74 -0
- package/dist/templates/default/convex/webhook.ts +193 -0
- package/dist/templates/default/convex.json +6 -0
- package/dist/templates/default/eas.json +56 -0
- package/dist/templates/default/fingerprint.config.js +9 -0
- package/dist/templates/default/hooks/use-debounce.ts +20 -0
- package/dist/templates/default/hooks/use-deep-link.ts +43 -0
- package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
- package/dist/templates/default/hooks/use-network.ts +11 -0
- package/dist/templates/default/hooks/use-notifications.ts +107 -0
- package/dist/templates/default/hooks/use-onboarding.ts +15 -0
- package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
- package/dist/templates/default/hooks/use-theme.ts +53 -0
- package/dist/templates/default/hooks/use-updates.ts +86 -0
- package/dist/templates/default/lib/a11y.ts +5 -0
- package/dist/templates/default/lib/app.ts +14 -0
- package/dist/templates/default/lib/assets.ts +17 -0
- package/dist/templates/default/lib/auth-client.ts +21 -0
- package/dist/templates/default/lib/convex-auth.tsx +79 -0
- package/dist/templates/default/lib/deep-link.ts +71 -0
- package/dist/templates/default/lib/dev-menu.ts +119 -0
- package/dist/templates/default/lib/device.ts +40 -0
- package/dist/templates/default/lib/dynamic-font.ts +49 -0
- package/dist/templates/default/lib/env.ts +10 -0
- package/dist/templates/default/lib/haptics.ts +24 -0
- package/dist/templates/default/lib/notifications.ts +276 -0
- package/dist/templates/default/lib/preferences.ts +45 -0
- package/dist/templates/default/lib/schemas.ts +137 -0
- package/dist/templates/default/lib/storage.ts +47 -0
- package/dist/templates/default/lib/updates.ts +107 -0
- package/dist/templates/default/metro.config.js +14 -0
- package/dist/templates/default/package.json +129 -0
- package/dist/templates/default/patches/PR-368.patch +91 -0
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- package/dist/templates/default/plugins/README.md +9 -0
- package/dist/templates/default/plugins/with-auto-signing.js +45 -0
- package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
- package/dist/templates/default/scripts/README.md +36 -0
- package/dist/templates/default/scripts/_run.mjs +77 -0
- package/dist/templates/default/scripts/clean.ts +543 -0
- package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
- package/dist/templates/default/store.config.json +58 -0
- package/dist/templates/default/tsconfig.json +13 -0
- package/dist/templates/default/vitest.config.ts +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from "react";
|
|
2
|
+
import Constants from "expo-constants";
|
|
3
|
+
import * as Clipboard from "expo-clipboard";
|
|
4
|
+
import * as LocalAuthentication from "expo-local-authentication";
|
|
5
|
+
import { Image as ExpoImage, useImage } from "expo-image";
|
|
6
|
+
import { router, usePreventZoomTransitionDismissal, type Href } from "expo-router";
|
|
7
|
+
|
|
8
|
+
const PROFILE_HREF = "/profile" as Href;
|
|
9
|
+
const DEBUG_HREF = "/debug" as Href;
|
|
10
|
+
import { useMutation, useQuery } from "convex/react";
|
|
11
|
+
import {
|
|
12
|
+
Host,
|
|
13
|
+
ScrollView,
|
|
14
|
+
Button,
|
|
15
|
+
Text,
|
|
16
|
+
HStack,
|
|
17
|
+
VStack,
|
|
18
|
+
Spacer,
|
|
19
|
+
Image,
|
|
20
|
+
RNHostView,
|
|
21
|
+
ConfirmationDialog,
|
|
22
|
+
} from "@expo/ui/swift-ui";
|
|
23
|
+
import {
|
|
24
|
+
background,
|
|
25
|
+
buttonStyle,
|
|
26
|
+
clipShape,
|
|
27
|
+
foregroundStyle,
|
|
28
|
+
frame,
|
|
29
|
+
padding,
|
|
30
|
+
onTapGesture,
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
lineLimit,
|
|
33
|
+
truncationMode,
|
|
34
|
+
textSelection,
|
|
35
|
+
scrollDismissesKeyboard,
|
|
36
|
+
tint,
|
|
37
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
38
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
39
|
+
import { Button as ButtonTokens } from "@/constants/layout";
|
|
40
|
+
|
|
41
|
+
import { api } from "@/convex/_generated/api";
|
|
42
|
+
import { authClient } from "@/lib/auth-client";
|
|
43
|
+
import { haptics } from "@/lib/haptics";
|
|
44
|
+
import { announce } from "@/lib/a11y";
|
|
45
|
+
import { useColors } from "@/hooks/use-theme";
|
|
46
|
+
import { useDebugEnabled } from "@/lib/preferences";
|
|
47
|
+
|
|
48
|
+
const HEADER_AVATAR_SIZE = 56;
|
|
49
|
+
|
|
50
|
+
export default function SettingsScreen() {
|
|
51
|
+
const dfont = useDynamicFont();
|
|
52
|
+
const colors = useColors();
|
|
53
|
+
const me = useQuery(api.users.getMe);
|
|
54
|
+
const removeAllTokens = useMutation(api.pushTokens.removeAll);
|
|
55
|
+
const deleteAccountMutation = useMutation(api.users.deleteAccount);
|
|
56
|
+
|
|
57
|
+
const [showSignOut, setShowSignOut] = useState(false);
|
|
58
|
+
const [showDeleteAccount, setShowDeleteAccount] = useState(false);
|
|
59
|
+
const [debugOn] = useDebugEnabled();
|
|
60
|
+
|
|
61
|
+
usePreventZoomTransitionDismissal();
|
|
62
|
+
|
|
63
|
+
const navigate = (path: Href) => {
|
|
64
|
+
haptics.light();
|
|
65
|
+
router.push(path);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleSignOut = async () => {
|
|
69
|
+
haptics.medium();
|
|
70
|
+
// Push-token cleanup is best-effort. A stale token gets garbage-collected
|
|
71
|
+
// by `pushTokens.cleanupStale` after 30 days, so don't gate sign-out on it.
|
|
72
|
+
try {
|
|
73
|
+
await removeAllTokens();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (__DEV__) console.warn("[signOut] removeAllTokens failed:", err);
|
|
76
|
+
}
|
|
77
|
+
await authClient.signOut();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleDeleteAccount = async () => {
|
|
81
|
+
haptics.error();
|
|
82
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
83
|
+
promptMessage: "Confirm with Face ID",
|
|
84
|
+
});
|
|
85
|
+
if (!result.success) return;
|
|
86
|
+
await deleteAccountMutation();
|
|
87
|
+
await authClient.signOut();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const version = Constants.expoConfig?.version ?? "1.0.0";
|
|
91
|
+
|
|
92
|
+
const handleCopyVersion = async () => {
|
|
93
|
+
haptics.light();
|
|
94
|
+
await Clipboard.setStringAsync(`v${version}`);
|
|
95
|
+
haptics.success();
|
|
96
|
+
announce("Version copied");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type SFSymbol = NonNullable<ComponentProps<typeof Image>["systemName"]>;
|
|
100
|
+
const rowButton = ({
|
|
101
|
+
label,
|
|
102
|
+
systemImage,
|
|
103
|
+
onPress,
|
|
104
|
+
role,
|
|
105
|
+
fg,
|
|
106
|
+
}: {
|
|
107
|
+
label: string;
|
|
108
|
+
systemImage: SFSymbol;
|
|
109
|
+
onPress: () => void;
|
|
110
|
+
role?: "destructive";
|
|
111
|
+
fg?: string;
|
|
112
|
+
}) => {
|
|
113
|
+
const labelColor =
|
|
114
|
+
fg ??
|
|
115
|
+
(role === "destructive" ? (colors.destructive as string) : (colors.foreground as string));
|
|
116
|
+
return (
|
|
117
|
+
<Button
|
|
118
|
+
modifiers={[
|
|
119
|
+
buttonStyle("plain"),
|
|
120
|
+
frame({ maxWidth: 10000 }),
|
|
121
|
+
background(colors.muted as string),
|
|
122
|
+
clipShape("capsule"),
|
|
123
|
+
]}
|
|
124
|
+
onPress={onPress}
|
|
125
|
+
>
|
|
126
|
+
<HStack
|
|
127
|
+
spacing={12}
|
|
128
|
+
alignment="center"
|
|
129
|
+
modifiers={[
|
|
130
|
+
frame({ maxWidth: 10000, height: ButtonTokens.height }),
|
|
131
|
+
padding({ horizontal: 16 }),
|
|
132
|
+
]}
|
|
133
|
+
>
|
|
134
|
+
<Image systemName={systemImage} size={18} color={labelColor} />
|
|
135
|
+
<Text modifiers={[dfont({ size: 16, weight: "medium" }), foregroundStyle(labelColor)]}>
|
|
136
|
+
{label}
|
|
137
|
+
</Text>
|
|
138
|
+
<Spacer />
|
|
139
|
+
{role !== "destructive" ? (
|
|
140
|
+
<Image systemName="chevron.right" size={13} color={colors.mutedForeground as string} />
|
|
141
|
+
) : null}
|
|
142
|
+
</HStack>
|
|
143
|
+
</Button>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
149
|
+
<ScrollView
|
|
150
|
+
modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
|
|
151
|
+
>
|
|
152
|
+
<VStack
|
|
153
|
+
spacing={12}
|
|
154
|
+
alignment="leading"
|
|
155
|
+
modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
|
|
156
|
+
>
|
|
157
|
+
{/* Profile header */}
|
|
158
|
+
<Button
|
|
159
|
+
modifiers={[
|
|
160
|
+
buttonStyle("plain"),
|
|
161
|
+
frame({ maxWidth: 10000 }),
|
|
162
|
+
background(colors.muted as string),
|
|
163
|
+
clipShape("capsule"),
|
|
164
|
+
onTapGesture(() => {
|
|
165
|
+
haptics.light();
|
|
166
|
+
navigate(PROFILE_HREF);
|
|
167
|
+
}),
|
|
168
|
+
accessibilityLabel("Open profile"),
|
|
169
|
+
]}
|
|
170
|
+
onPress={() => navigate(PROFILE_HREF)}
|
|
171
|
+
>
|
|
172
|
+
<HStack
|
|
173
|
+
spacing={16}
|
|
174
|
+
alignment="center"
|
|
175
|
+
modifiers={[
|
|
176
|
+
frame({ maxWidth: 10000, height: 80 }),
|
|
177
|
+
padding({ leading: 8, trailing: 16 }),
|
|
178
|
+
]}
|
|
179
|
+
>
|
|
180
|
+
<ProfileHeaderAvatar avatarUrl={me?.avatarUrl ?? null} />
|
|
181
|
+
<VStack alignment="leading" spacing={2}>
|
|
182
|
+
<Text
|
|
183
|
+
modifiers={[
|
|
184
|
+
dfont({ size: 17, weight: "semibold" }),
|
|
185
|
+
foregroundStyle(colors.foreground as string),
|
|
186
|
+
lineLimit(2),
|
|
187
|
+
truncationMode("tail"),
|
|
188
|
+
]}
|
|
189
|
+
>
|
|
190
|
+
{me?.name ?? "Loading..."}
|
|
191
|
+
</Text>
|
|
192
|
+
{me?.email ? (
|
|
193
|
+
<Text
|
|
194
|
+
modifiers={[
|
|
195
|
+
dfont({ size: 14 }),
|
|
196
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
197
|
+
lineLimit(1),
|
|
198
|
+
truncationMode("middle"),
|
|
199
|
+
textSelection(true),
|
|
200
|
+
]}
|
|
201
|
+
>
|
|
202
|
+
{me.email}
|
|
203
|
+
</Text>
|
|
204
|
+
) : null}
|
|
205
|
+
</VStack>
|
|
206
|
+
<Spacer />
|
|
207
|
+
<Image
|
|
208
|
+
systemName="chevron.right"
|
|
209
|
+
size={13}
|
|
210
|
+
color={colors.mutedForeground as string}
|
|
211
|
+
/>
|
|
212
|
+
</HStack>
|
|
213
|
+
</Button>
|
|
214
|
+
|
|
215
|
+
<VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
|
|
216
|
+
{rowButton({
|
|
217
|
+
label: "Sessions",
|
|
218
|
+
systemImage: "list.bullet.rectangle.portrait",
|
|
219
|
+
onPress: () => navigate("/sessions"),
|
|
220
|
+
})}
|
|
221
|
+
{rowButton({
|
|
222
|
+
label: "Preferences",
|
|
223
|
+
systemImage: "slider.horizontal.3",
|
|
224
|
+
onPress: () => navigate("/settings/preferences"),
|
|
225
|
+
})}
|
|
226
|
+
</VStack>
|
|
227
|
+
|
|
228
|
+
<VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
|
|
229
|
+
{rowButton({
|
|
230
|
+
label: "Help & Feedback",
|
|
231
|
+
systemImage: "bubble.left",
|
|
232
|
+
onPress: () => navigate("/help"),
|
|
233
|
+
})}
|
|
234
|
+
{rowButton({
|
|
235
|
+
label: "Privacy",
|
|
236
|
+
systemImage: "hand.raised",
|
|
237
|
+
onPress: () => navigate("/privacy"),
|
|
238
|
+
})}
|
|
239
|
+
{rowButton({
|
|
240
|
+
label: "Copy version",
|
|
241
|
+
systemImage: "doc.on.doc",
|
|
242
|
+
onPress: handleCopyVersion,
|
|
243
|
+
})}
|
|
244
|
+
{debugOn
|
|
245
|
+
? rowButton({
|
|
246
|
+
label: "Debug",
|
|
247
|
+
systemImage: "ant.circle",
|
|
248
|
+
onPress: () => navigate(DEBUG_HREF),
|
|
249
|
+
})
|
|
250
|
+
: null}
|
|
251
|
+
</VStack>
|
|
252
|
+
|
|
253
|
+
<VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
|
|
254
|
+
<ConfirmationDialog
|
|
255
|
+
title="Sign out?"
|
|
256
|
+
isPresented={showSignOut}
|
|
257
|
+
onIsPresentedChange={setShowSignOut}
|
|
258
|
+
titleVisibility="visible"
|
|
259
|
+
>
|
|
260
|
+
<ConfirmationDialog.Trigger>
|
|
261
|
+
{rowButton({
|
|
262
|
+
label: "Sign out",
|
|
263
|
+
systemImage: "rectangle.portrait.and.arrow.right",
|
|
264
|
+
onPress: () => setShowSignOut(true),
|
|
265
|
+
role: "destructive",
|
|
266
|
+
})}
|
|
267
|
+
</ConfirmationDialog.Trigger>
|
|
268
|
+
<ConfirmationDialog.Actions>
|
|
269
|
+
<Button label="Sign Out" role="destructive" onPress={handleSignOut} />
|
|
270
|
+
<Button label="Cancel" role="cancel" />
|
|
271
|
+
</ConfirmationDialog.Actions>
|
|
272
|
+
<ConfirmationDialog.Message>
|
|
273
|
+
<Text modifiers={[dfont({ size: 16 })]}>
|
|
274
|
+
You will need to sign in again to access your account.
|
|
275
|
+
</Text>
|
|
276
|
+
</ConfirmationDialog.Message>
|
|
277
|
+
</ConfirmationDialog>
|
|
278
|
+
|
|
279
|
+
<ConfirmationDialog
|
|
280
|
+
title="Delete account?"
|
|
281
|
+
isPresented={showDeleteAccount}
|
|
282
|
+
onIsPresentedChange={setShowDeleteAccount}
|
|
283
|
+
titleVisibility="visible"
|
|
284
|
+
>
|
|
285
|
+
<ConfirmationDialog.Trigger>
|
|
286
|
+
{rowButton({
|
|
287
|
+
label: "Delete account",
|
|
288
|
+
systemImage: "trash",
|
|
289
|
+
onPress: () => setShowDeleteAccount(true),
|
|
290
|
+
role: "destructive",
|
|
291
|
+
})}
|
|
292
|
+
</ConfirmationDialog.Trigger>
|
|
293
|
+
<ConfirmationDialog.Actions>
|
|
294
|
+
<Button label="Delete Forever" role="destructive" onPress={handleDeleteAccount} />
|
|
295
|
+
<Button label="Cancel" role="cancel" />
|
|
296
|
+
</ConfirmationDialog.Actions>
|
|
297
|
+
<ConfirmationDialog.Message>
|
|
298
|
+
<Text modifiers={[dfont({ size: 16 })]}>
|
|
299
|
+
This permanently deletes your account and all data. This cannot be undone.
|
|
300
|
+
</Text>
|
|
301
|
+
</ConfirmationDialog.Message>
|
|
302
|
+
</ConfirmationDialog>
|
|
303
|
+
</VStack>
|
|
304
|
+
|
|
305
|
+
<HStack modifiers={[frame({ maxWidth: 10000 }), padding({ top: 16 })]}>
|
|
306
|
+
<Spacer />
|
|
307
|
+
<Text
|
|
308
|
+
modifiers={[dfont({ size: 12 }), foregroundStyle(colors.tertiaryLabel as string)]}
|
|
309
|
+
>
|
|
310
|
+
v{version}
|
|
311
|
+
</Text>
|
|
312
|
+
<Spacer />
|
|
313
|
+
</HStack>
|
|
314
|
+
</VStack>
|
|
315
|
+
</ScrollView>
|
|
316
|
+
</Host>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function ProfileHeaderAvatar({ avatarUrl }: { avatarUrl: string | null }) {
|
|
321
|
+
const colors = useColors();
|
|
322
|
+
if (avatarUrl) {
|
|
323
|
+
return <RemoteAvatar key={avatarUrl} url={avatarUrl} size={HEADER_AVATAR_SIZE} />;
|
|
324
|
+
}
|
|
325
|
+
return (
|
|
326
|
+
<Image
|
|
327
|
+
systemName="person.crop.circle.fill"
|
|
328
|
+
size={HEADER_AVATAR_SIZE}
|
|
329
|
+
color={colors.mutedForeground as string}
|
|
330
|
+
modifiers={[frame({ width: HEADER_AVATAR_SIZE, height: HEADER_AVATAR_SIZE })]}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function RemoteAvatar({ url, size }: { url: string; size: number }) {
|
|
336
|
+
const colors = useColors();
|
|
337
|
+
const image = useImage(url, { maxWidth: size * 4 });
|
|
338
|
+
if (!image) {
|
|
339
|
+
return (
|
|
340
|
+
<Image
|
|
341
|
+
systemName="person.crop.circle.fill"
|
|
342
|
+
size={size}
|
|
343
|
+
color={colors.mutedForeground as string}
|
|
344
|
+
modifiers={[frame({ width: size, height: size })]}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return (
|
|
349
|
+
<RNHostView matchContents>
|
|
350
|
+
<ExpoImage
|
|
351
|
+
source={image}
|
|
352
|
+
style={{
|
|
353
|
+
width: size,
|
|
354
|
+
height: size,
|
|
355
|
+
borderRadius: size / 2,
|
|
356
|
+
}}
|
|
357
|
+
contentFit="cover"
|
|
358
|
+
accessibilityLabel="Profile photo"
|
|
359
|
+
/>
|
|
360
|
+
</RNHostView>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Host,
|
|
3
|
+
ScrollView,
|
|
4
|
+
Picker,
|
|
5
|
+
Toggle,
|
|
6
|
+
Text,
|
|
7
|
+
VStack,
|
|
8
|
+
HStack,
|
|
9
|
+
Spacer,
|
|
10
|
+
Image,
|
|
11
|
+
} from "@expo/ui/swift-ui";
|
|
12
|
+
import {
|
|
13
|
+
background,
|
|
14
|
+
clipShape,
|
|
15
|
+
controlSize,
|
|
16
|
+
foregroundStyle,
|
|
17
|
+
frame,
|
|
18
|
+
padding,
|
|
19
|
+
pickerStyle,
|
|
20
|
+
scrollDismissesKeyboard,
|
|
21
|
+
tag,
|
|
22
|
+
tint,
|
|
23
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
24
|
+
import { Button as ButtonTokens } from "@/constants/layout";
|
|
25
|
+
|
|
26
|
+
import { haptics } from "@/lib/haptics";
|
|
27
|
+
import { useColors, useThemeMode, type ThemeMode } from "@/hooks/use-theme";
|
|
28
|
+
import {
|
|
29
|
+
useDebugEnabled,
|
|
30
|
+
useHapticsEnabled,
|
|
31
|
+
useReduceMotionPref,
|
|
32
|
+
type ReduceMotionPref,
|
|
33
|
+
} from "@/lib/preferences";
|
|
34
|
+
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
35
|
+
|
|
36
|
+
const MODE_BY_INDEX: ThemeMode[] = ["light", "dark", "system"];
|
|
37
|
+
const INDEX_BY_MODE: Record<ThemeMode, number> = { light: 0, dark: 1, system: 2 };
|
|
38
|
+
|
|
39
|
+
const MOTION_BY_INDEX: ReduceMotionPref[] = ["system", "always", "never"];
|
|
40
|
+
const INDEX_BY_MOTION: Record<ReduceMotionPref, number> = {
|
|
41
|
+
system: 0,
|
|
42
|
+
always: 1,
|
|
43
|
+
never: 2,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default function PreferencesScreen() {
|
|
47
|
+
const dfont = useDynamicFont();
|
|
48
|
+
const colors = useColors();
|
|
49
|
+
const { mode, setMode } = useThemeMode();
|
|
50
|
+
const [hapticsOn, setHapticsOn] = useHapticsEnabled();
|
|
51
|
+
const [motion, setMotion] = useReduceMotionPref();
|
|
52
|
+
const [debugOn, setDebugOn] = useDebugEnabled();
|
|
53
|
+
|
|
54
|
+
const sectionLabelModifiers = [
|
|
55
|
+
dfont({ size: 13, weight: "semibold" }),
|
|
56
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
57
|
+
padding({ horizontal: 8 }),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const helperModifiers = [
|
|
61
|
+
dfont({ size: 13 }),
|
|
62
|
+
foregroundStyle(colors.mutedForeground as string),
|
|
63
|
+
padding({ horizontal: 8 }),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const toggleRow = ({
|
|
67
|
+
icon,
|
|
68
|
+
label,
|
|
69
|
+
value,
|
|
70
|
+
onChange,
|
|
71
|
+
}: {
|
|
72
|
+
icon: NonNullable<React.ComponentProps<typeof Image>["systemName"]>;
|
|
73
|
+
label: string;
|
|
74
|
+
value: boolean;
|
|
75
|
+
onChange: (v: boolean) => void;
|
|
76
|
+
}) => (
|
|
77
|
+
<HStack
|
|
78
|
+
spacing={12}
|
|
79
|
+
alignment="center"
|
|
80
|
+
modifiers={[
|
|
81
|
+
frame({ maxWidth: 10000, height: ButtonTokens.height }),
|
|
82
|
+
padding({ horizontal: 16 }),
|
|
83
|
+
background(colors.muted as string),
|
|
84
|
+
clipShape("capsule"),
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
<Image systemName={icon} size={18} color={colors.foreground as string} />
|
|
88
|
+
<Text
|
|
89
|
+
modifiers={[
|
|
90
|
+
dfont({ size: 16, weight: "medium" }),
|
|
91
|
+
foregroundStyle(colors.foreground as string),
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
{label}
|
|
95
|
+
</Text>
|
|
96
|
+
<Spacer />
|
|
97
|
+
<Toggle isOn={value} onIsOnChange={onChange} modifiers={[tint(colors.primary as string)]} />
|
|
98
|
+
</HStack>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
103
|
+
<ScrollView
|
|
104
|
+
modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
|
|
105
|
+
>
|
|
106
|
+
<VStack
|
|
107
|
+
spacing={20}
|
|
108
|
+
alignment="leading"
|
|
109
|
+
modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
|
|
110
|
+
>
|
|
111
|
+
<VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
112
|
+
<Text modifiers={sectionLabelModifiers}>APPEARANCE</Text>
|
|
113
|
+
<Picker
|
|
114
|
+
modifiers={[
|
|
115
|
+
pickerStyle("segmented"),
|
|
116
|
+
controlSize("large"),
|
|
117
|
+
frame({ maxWidth: 10000, height: ButtonTokens.height }),
|
|
118
|
+
]}
|
|
119
|
+
selection={INDEX_BY_MODE[mode]}
|
|
120
|
+
onSelectionChange={(v) => {
|
|
121
|
+
haptics.selection();
|
|
122
|
+
setMode(MODE_BY_INDEX[v as number] ?? "system");
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<Text modifiers={[tag(0), dfont({ size: 14, weight: "medium" })]}>Light</Text>
|
|
126
|
+
<Text modifiers={[tag(1), dfont({ size: 14, weight: "medium" })]}>Dark</Text>
|
|
127
|
+
<Text modifiers={[tag(2), dfont({ size: 14, weight: "medium" })]}>System</Text>
|
|
128
|
+
</Picker>
|
|
129
|
+
</VStack>
|
|
130
|
+
|
|
131
|
+
<VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
132
|
+
<Text modifiers={sectionLabelModifiers}>REDUCE MOTION</Text>
|
|
133
|
+
<Picker
|
|
134
|
+
modifiers={[
|
|
135
|
+
pickerStyle("segmented"),
|
|
136
|
+
controlSize("large"),
|
|
137
|
+
frame({ maxWidth: 10000, height: ButtonTokens.height }),
|
|
138
|
+
]}
|
|
139
|
+
selection={INDEX_BY_MOTION[motion]}
|
|
140
|
+
onSelectionChange={(v) => {
|
|
141
|
+
haptics.selection();
|
|
142
|
+
setMotion(MOTION_BY_INDEX[v as number] ?? "system");
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<Text modifiers={[tag(0), dfont({ size: 14, weight: "medium" })]}>System</Text>
|
|
146
|
+
<Text modifiers={[tag(1), dfont({ size: 14, weight: "medium" })]}>Always</Text>
|
|
147
|
+
<Text modifiers={[tag(2), dfont({ size: 14, weight: "medium" })]}>Never</Text>
|
|
148
|
+
</Picker>
|
|
149
|
+
</VStack>
|
|
150
|
+
|
|
151
|
+
<VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
152
|
+
<Text modifiers={sectionLabelModifiers}>HAPTICS</Text>
|
|
153
|
+
{toggleRow({
|
|
154
|
+
icon: "iphone.radiowaves.left.and.right",
|
|
155
|
+
label: "Haptic feedback",
|
|
156
|
+
value: hapticsOn,
|
|
157
|
+
onChange: (v) => {
|
|
158
|
+
setHapticsOn(v);
|
|
159
|
+
if (v) haptics.light();
|
|
160
|
+
},
|
|
161
|
+
})}
|
|
162
|
+
</VStack>
|
|
163
|
+
|
|
164
|
+
<VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
|
|
165
|
+
<Text modifiers={sectionLabelModifiers}>DEBUG</Text>
|
|
166
|
+
{toggleRow({
|
|
167
|
+
icon: "ant.circle.fill",
|
|
168
|
+
label: "Debug mode",
|
|
169
|
+
value: debugOn,
|
|
170
|
+
onChange: (v) => {
|
|
171
|
+
setDebugOn(v);
|
|
172
|
+
haptics.light();
|
|
173
|
+
},
|
|
174
|
+
})}
|
|
175
|
+
<Text modifiers={helperModifiers}>
|
|
176
|
+
Reveals a Debug screen with version, device, OTA update, and push diagnostics. Off in
|
|
177
|
+
production builds by default.
|
|
178
|
+
</Text>
|
|
179
|
+
</VStack>
|
|
180
|
+
</VStack>
|
|
181
|
+
</ScrollView>
|
|
182
|
+
</Host>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Stack } from "expo-router";
|
|
2
|
+
|
|
3
|
+
import { useColors } from "@/hooks/use-theme";
|
|
4
|
+
import { useReducedMotion } from "@/hooks/use-reduced-motion";
|
|
5
|
+
import { FontFamily } from "@/constants/layout";
|
|
6
|
+
import { LoadingScreen } from "@/components/ui/loading-screen";
|
|
7
|
+
|
|
8
|
+
export { AppErrorBoundary as ErrorBoundary } from "@/components/ui/error-boundary";
|
|
9
|
+
|
|
10
|
+
export function SuspenseFallback() {
|
|
11
|
+
return <LoadingScreen />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AppLayout() {
|
|
15
|
+
const colors = useColors();
|
|
16
|
+
const reduceMotion = useReducedMotion();
|
|
17
|
+
const headerTint = colors.foreground as string;
|
|
18
|
+
const titleStyle = { color: headerTint, fontFamily: FontFamily.semiBold };
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Stack
|
|
22
|
+
screenOptions={{
|
|
23
|
+
headerShown: false,
|
|
24
|
+
contentStyle: { backgroundColor: colors.background as string },
|
|
25
|
+
headerBackTitle: "Back",
|
|
26
|
+
headerTintColor: headerTint,
|
|
27
|
+
headerShadowVisible: false,
|
|
28
|
+
animation: reduceMotion ? "fade" : "slide_from_right",
|
|
29
|
+
animationDuration: reduceMotion ? 150 : 300,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
33
|
+
<Stack.Screen
|
|
34
|
+
name="welcome"
|
|
35
|
+
options={{ headerShown: false, animation: reduceMotion ? "none" : "fade" }}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<Stack.Screen name="debug">
|
|
39
|
+
<Stack.Header transparent />
|
|
40
|
+
<Stack.Screen.Title style={titleStyle}>Debug</Stack.Screen.Title>
|
|
41
|
+
<Stack.Screen.BackButton withMenu>Settings</Stack.Screen.BackButton>
|
|
42
|
+
</Stack.Screen>
|
|
43
|
+
|
|
44
|
+
<Stack.Screen name="help">
|
|
45
|
+
<Stack.Header transparent />
|
|
46
|
+
<Stack.Screen.Title style={titleStyle}>Help</Stack.Screen.Title>
|
|
47
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
48
|
+
</Stack.Screen>
|
|
49
|
+
|
|
50
|
+
<Stack.Screen name="privacy">
|
|
51
|
+
<Stack.Header transparent />
|
|
52
|
+
<Stack.Screen.Title style={titleStyle}>Privacy</Stack.Screen.Title>
|
|
53
|
+
<Stack.Screen.BackButton displayMode="minimal" withMenu>
|
|
54
|
+
Settings
|
|
55
|
+
</Stack.Screen.BackButton>
|
|
56
|
+
</Stack.Screen>
|
|
57
|
+
|
|
58
|
+
<Stack.Screen name="linked" options={{ headerShown: true, title: "Linked" }} />
|
|
59
|
+
|
|
60
|
+
<Stack.Screen name="profile" options={{ headerShown: true }}>
|
|
61
|
+
<Stack.Header transparent />
|
|
62
|
+
<Stack.Screen.Title style={titleStyle}>Profile</Stack.Screen.Title>
|
|
63
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
64
|
+
</Stack.Screen>
|
|
65
|
+
|
|
66
|
+
<Stack.Screen name="sessions" options={{ headerShown: true }}>
|
|
67
|
+
<Stack.Header transparent />
|
|
68
|
+
<Stack.Screen.Title style={titleStyle}>Sessions</Stack.Screen.Title>
|
|
69
|
+
<Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
|
|
70
|
+
</Stack.Screen>
|
|
71
|
+
</Stack>
|
|
72
|
+
);
|
|
73
|
+
}
|