@ramonclaudio/create-vexpo 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +10 -10
  2. package/dist/index.js +8 -7
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
  5. package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
  6. package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
  7. package/dist/templates/default/.eas/workflows/release.yml +3 -7
  8. package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
  9. package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
  10. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
  11. package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
  12. package/dist/templates/default/.github/workflows/check.yml +20 -12
  13. package/dist/templates/default/.maestro/launch.yaml +19 -10
  14. package/dist/templates/default/AGENTS.md +25 -8
  15. package/dist/templates/default/DESIGN.md +14 -10
  16. package/dist/templates/default/README.md +83 -78
  17. package/dist/templates/default/SETUP.md +159 -152
  18. package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
  19. package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
  20. package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
  21. package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
  22. package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
  23. package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
  24. package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
  25. package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
  26. package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
  27. package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
  28. package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
  29. package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
  30. package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
  31. package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
  32. package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
  33. package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
  34. package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
  35. package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
  36. package/dist/templates/default/_env.example +7 -7
  37. package/dist/templates/default/_gitattributes +1 -1
  38. package/dist/templates/default/_gitignore +17 -2
  39. package/dist/templates/default/_npmrc +7 -0
  40. package/dist/templates/default/_oxlintrc.json +1 -1
  41. package/dist/templates/default/app-store/accessibility.config.json +20 -0
  42. package/dist/templates/default/app-store/privacy.config.json +27 -0
  43. package/dist/templates/default/app.config.ts +105 -33
  44. package/dist/templates/default/app.json +1 -9
  45. package/dist/templates/default/convex/_generated/api.d.ts +12 -0
  46. package/dist/templates/default/convex/admin.ts +0 -13
  47. package/dist/templates/default/convex/appAttest.ts +467 -0
  48. package/dist/templates/default/convex/appAttestStore.ts +141 -0
  49. package/dist/templates/default/convex/apple.ts +53 -0
  50. package/dist/templates/default/convex/auth.ts +6 -45
  51. package/dist/templates/default/convex/constants.ts +2 -7
  52. package/dist/templates/default/convex/crons.ts +12 -5
  53. package/dist/templates/default/convex/email.ts +4 -24
  54. package/dist/templates/default/convex/env.ts +0 -4
  55. package/dist/templates/default/convex/errors.ts +0 -7
  56. package/dist/templates/default/convex/functions.ts +0 -26
  57. package/dist/templates/default/convex/http.ts +3 -5
  58. package/dist/templates/default/convex/log.ts +2 -25
  59. package/dist/templates/default/convex/pushSender.ts +145 -0
  60. package/dist/templates/default/convex/pushTokens.ts +110 -13
  61. package/dist/templates/default/convex/rateLimit.ts +8 -39
  62. package/dist/templates/default/convex/schema.ts +48 -5
  63. package/dist/templates/default/convex/tsconfig.json +1 -0
  64. package/dist/templates/default/convex/users.ts +143 -61
  65. package/dist/templates/default/convex/validators.ts +1 -38
  66. package/dist/templates/default/convex/webhook.ts +1 -31
  67. package/dist/templates/default/convex.json +1 -2
  68. package/dist/templates/default/metro.config.js +9 -1
  69. package/dist/templates/default/package.json +67 -70
  70. package/dist/templates/default/plugins/README.md +5 -1
  71. package/dist/templates/default/scripts/README.md +9 -9
  72. package/dist/templates/default/scripts/_run.mjs +3 -20
  73. package/dist/templates/default/scripts/clean.ts +81 -69
  74. package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
  75. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
  76. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
  77. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
  78. package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
  79. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
  80. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
  81. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
  82. package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
  83. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
  84. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
  85. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
  86. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
  87. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
  88. package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
  89. package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
  90. package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
  91. package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
  92. package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
  93. package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
  94. package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
  95. package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
  96. package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
  97. package/dist/templates/default/src/app/+native-intent.tsx +25 -0
  98. package/dist/templates/default/src/app/+not-found.tsx +43 -0
  99. package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
  100. package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
  101. package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
  102. package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
  103. package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
  104. package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
  105. package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
  106. package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
  107. package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
  108. package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
  109. package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
  110. package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
  111. package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
  112. package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
  113. package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
  114. package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
  115. package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
  116. package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
  117. package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
  118. package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
  119. package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
  120. package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
  121. package/dist/templates/default/src/hooks/use-network.ts +34 -0
  122. package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
  123. package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
  124. package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
  125. package/dist/templates/default/src/lib/appAttest.ts +78 -0
  126. package/dist/templates/default/src/lib/assets.ts +9 -0
  127. package/dist/templates/default/src/lib/deep-link.ts +82 -0
  128. package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
  129. package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
  130. package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
  131. package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
  132. package/dist/templates/default/src/lib/masks.ts +21 -0
  133. package/dist/templates/default/src/lib/native-state.ts +20 -0
  134. package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
  135. package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
  136. package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
  137. package/dist/templates/default/src/lib/text-style.ts +20 -0
  138. package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
  139. package/dist/templates/default/store.config.json +1 -1
  140. package/dist/templates/default/tsconfig.json +3 -1
  141. package/dist/templates/default/vitest.config.ts +8 -1
  142. package/package.json +5 -5
  143. package/dist/templates/default/app/(app)/_layout.tsx +0 -73
  144. package/dist/templates/default/app/(app)/debug.tsx +0 -389
  145. package/dist/templates/default/app/(app)/sessions.tsx +0 -191
  146. package/dist/templates/default/app/(app)/welcome.tsx +0 -140
  147. package/dist/templates/default/app/+native-intent.tsx +0 -14
  148. package/dist/templates/default/app/+not-found.tsx +0 -51
  149. package/dist/templates/default/bun.lock +0 -1860
  150. package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
  151. package/dist/templates/default/components/ui/convex-error.tsx +0 -32
  152. package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
  153. package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
  154. package/dist/templates/default/components/ui/status-text.tsx +0 -49
  155. package/dist/templates/default/components/ui/update-banner.tsx +0 -82
  156. package/dist/templates/default/fingerprint.config.js +0 -9
  157. package/dist/templates/default/hooks/use-debounce.ts +0 -20
  158. package/dist/templates/default/hooks/use-deep-link.ts +0 -43
  159. package/dist/templates/default/hooks/use-network.ts +0 -11
  160. package/dist/templates/default/lib/assets.ts +0 -17
  161. package/dist/templates/default/lib/deep-link.ts +0 -71
  162. package/dist/templates/default/patches/PR-368.patch +0 -91
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
  165. /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
  166. /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
  167. /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
  168. /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
  169. /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
  170. /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
  171. /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
  172. /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
  173. /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
  174. /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
@@ -1,7 +1,8 @@
1
1
  import { Stack } from "expo-router";
2
2
 
3
3
  import { useColors } from "@/hooks/use-theme";
4
- import { useReducedMotion } from "@/hooks/use-reduced-motion";
4
+ import { useMotionScreenOptions } from "@/hooks/use-motion-screen-options";
5
+ import { FontFamily } from "@/constants/layout";
5
6
  import { HeaderTint } from "@/constants/theme";
6
7
  import { LoadingScreen } from "@/components/ui/loading-screen";
7
8
 
@@ -10,26 +11,29 @@ export const unstable_settings = {
10
11
  };
11
12
 
12
13
  export function SuspenseFallback() {
13
- return <LoadingScreen />;
14
+ return <LoadingScreen testID="settings-loading" />;
14
15
  }
15
16
 
16
17
  export default function SettingsLayout() {
17
18
  const colors = useColors();
18
- const reduceMotion = useReducedMotion();
19
+ const motion = useMotionScreenOptions("default");
19
20
 
20
21
  return (
21
22
  <Stack
22
23
  screenOptions={{
24
+ ...motion,
23
25
  headerShown: false,
24
26
  contentStyle: { backgroundColor: colors.background as string },
25
- animation: reduceMotion ? "fade" : "default",
26
- animationDuration: reduceMotion ? 150 : undefined,
27
27
  }}
28
28
  >
29
29
  <Stack.Screen name="index" />
30
30
  <Stack.Screen name="preferences" options={{ headerShown: true }}>
31
31
  <Stack.Header transparent />
32
- <Stack.Screen.Title style={{ color: HeaderTint as string }}>Preferences</Stack.Screen.Title>
32
+ <Stack.Screen.Title
33
+ style={{ color: HeaderTint as string, fontFamily: FontFamily.semiBold }}
34
+ >
35
+ Preferences
36
+ </Stack.Screen.Title>
33
37
  <Stack.Screen.BackButton>Settings</Stack.Screen.BackButton>
34
38
  </Stack.Screen>
35
39
  </Stack>
@@ -1,9 +1,9 @@
1
1
  import { useState, type ComponentProps } from "react";
2
2
  import Constants from "expo-constants";
3
3
  import * as Clipboard from "expo-clipboard";
4
- import * as LocalAuthentication from "expo-local-authentication";
4
+ import { useDeleteAccount } from "@/hooks/use-delete-account";
5
5
  import { Image as ExpoImage, useImage } from "expo-image";
6
- import { router, usePreventZoomTransitionDismissal, type Href } from "expo-router";
6
+ import { router, type Href } from "expo-router";
7
7
 
8
8
  const PROFILE_HREF = "/profile" as Href;
9
9
  const DEBUG_HREF = "/debug" as Href;
@@ -18,6 +18,7 @@ import {
18
18
  Spacer,
19
19
  Image,
20
20
  RNHostView,
21
+ Alert,
21
22
  ConfirmationDialog,
22
23
  } from "@expo/ui/swift-ui";
23
24
  import {
@@ -27,7 +28,7 @@ import {
27
28
  foregroundStyle,
28
29
  frame,
29
30
  padding,
30
- onTapGesture,
31
+ accessibilityHidden,
31
32
  accessibilityLabel,
32
33
  lineLimit,
33
34
  truncationMode,
@@ -36,12 +37,14 @@ import {
36
37
  tint,
37
38
  } from "@expo/ui/swift-ui/modifiers";
38
39
  import { useDynamicFont } from "@/lib/dynamic-font";
40
+ import { useSymbolSize } from "@/lib/dynamic-symbol-size";
39
41
  import { Button as ButtonTokens } from "@/constants/layout";
40
42
 
41
43
  import { api } from "@/convex/_generated/api";
42
44
  import { authClient } from "@/lib/auth-client";
43
45
  import { haptics } from "@/lib/haptics";
44
46
  import { announce } from "@/lib/a11y";
47
+ import { ErrorText } from "@/components/ui/status-text";
45
48
  import { useColors } from "@/hooks/use-theme";
46
49
  import { useDebugEnabled } from "@/lib/preferences";
47
50
 
@@ -49,17 +52,16 @@ const HEADER_AVATAR_SIZE = 56;
49
52
 
50
53
  export default function SettingsScreen() {
51
54
  const dfont = useDynamicFont();
55
+ const symbolSize = useSymbolSize();
52
56
  const colors = useColors();
53
57
  const me = useQuery(api.users.getMe);
54
58
  const removeAllTokens = useMutation(api.pushTokens.removeAll);
55
- const deleteAccountMutation = useMutation(api.users.deleteAccount);
59
+ const { deleteAccount, deleteError } = useDeleteAccount();
56
60
 
57
61
  const [showSignOut, setShowSignOut] = useState(false);
58
62
  const [showDeleteAccount, setShowDeleteAccount] = useState(false);
59
63
  const [debugOn] = useDebugEnabled();
60
64
 
61
- usePreventZoomTransitionDismissal();
62
-
63
65
  const navigate = (path: Href) => {
64
66
  haptics.light();
65
67
  router.push(path);
@@ -67,8 +69,6 @@ export default function SettingsScreen() {
67
69
 
68
70
  const handleSignOut = async () => {
69
71
  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
72
  try {
73
73
  await removeAllTokens();
74
74
  } catch (err) {
@@ -77,16 +77,6 @@ export default function SettingsScreen() {
77
77
  await authClient.signOut();
78
78
  };
79
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
80
  const version = Constants.expoConfig?.version ?? "1.0.0";
91
81
 
92
82
  const handleCopyVersion = async () => {
@@ -98,12 +88,14 @@ export default function SettingsScreen() {
98
88
 
99
89
  type SFSymbol = NonNullable<ComponentProps<typeof Image>["systemName"]>;
100
90
  const rowButton = ({
91
+ testID,
101
92
  label,
102
93
  systemImage,
103
94
  onPress,
104
95
  role,
105
96
  fg,
106
97
  }: {
98
+ testID: string;
107
99
  label: string;
108
100
  systemImage: SFSymbol;
109
101
  onPress: () => void;
@@ -115,9 +107,10 @@ export default function SettingsScreen() {
115
107
  (role === "destructive" ? (colors.destructive as string) : (colors.foreground as string));
116
108
  return (
117
109
  <Button
110
+ testID={testID}
118
111
  modifiers={[
119
112
  buttonStyle("plain"),
120
- frame({ maxWidth: 10000 }),
113
+ frame({ maxWidth: Infinity }),
121
114
  background(colors.muted as string),
122
115
  clipShape("capsule"),
123
116
  ]}
@@ -127,17 +120,27 @@ export default function SettingsScreen() {
127
120
  spacing={12}
128
121
  alignment="center"
129
122
  modifiers={[
130
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
123
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
131
124
  padding({ horizontal: 16 }),
132
125
  ]}
133
126
  >
134
- <Image systemName={systemImage} size={18} color={labelColor} />
127
+ <Image
128
+ systemName={systemImage}
129
+ size={symbolSize(18)}
130
+ color={labelColor}
131
+ modifiers={[accessibilityHidden(true)]}
132
+ />
135
133
  <Text modifiers={[dfont({ size: 16, weight: "medium" }), foregroundStyle(labelColor)]}>
136
134
  {label}
137
135
  </Text>
138
136
  <Spacer />
139
137
  {role !== "destructive" ? (
140
- <Image systemName="chevron.right" size={13} color={colors.mutedForeground as string} />
138
+ <Image
139
+ systemName="chevron.right"
140
+ size={symbolSize(13)}
141
+ color={colors.mutedForeground as string}
142
+ modifiers={[accessibilityHidden(true)]}
143
+ />
141
144
  ) : null}
142
145
  </HStack>
143
146
  </Button>
@@ -145,7 +148,7 @@ export default function SettingsScreen() {
145
148
  };
146
149
 
147
150
  return (
148
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
151
+ <Host testID="settings-screen" style={{ flex: 1, backgroundColor: colors.background }}>
149
152
  <ScrollView
150
153
  modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
151
154
  >
@@ -154,32 +157,32 @@ export default function SettingsScreen() {
154
157
  alignment="leading"
155
158
  modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
156
159
  >
157
- {/* Profile header */}
158
160
  <Button
161
+ testID="settings-profile"
159
162
  modifiers={[
160
163
  buttonStyle("plain"),
161
- frame({ maxWidth: 10000 }),
164
+ frame({ maxWidth: Infinity }),
162
165
  background(colors.muted as string),
163
166
  clipShape("capsule"),
164
- onTapGesture(() => {
165
- haptics.light();
166
- navigate(PROFILE_HREF);
167
- }),
168
167
  accessibilityLabel("Open profile"),
169
168
  ]}
170
- onPress={() => navigate(PROFILE_HREF)}
169
+ onPress={() => {
170
+ haptics.light();
171
+ navigate(PROFILE_HREF);
172
+ }}
171
173
  >
172
174
  <HStack
173
175
  spacing={16}
174
176
  alignment="center"
175
177
  modifiers={[
176
- frame({ maxWidth: 10000, height: 80 }),
178
+ frame({ maxWidth: Infinity, minHeight: 80 }),
177
179
  padding({ leading: 8, trailing: 16 }),
178
180
  ]}
179
181
  >
180
182
  <ProfileHeaderAvatar avatarUrl={me?.avatarUrl ?? null} />
181
183
  <VStack alignment="leading" spacing={2}>
182
184
  <Text
185
+ testID="settings-profile-name"
183
186
  modifiers={[
184
187
  dfont({ size: 17, weight: "semibold" }),
185
188
  foregroundStyle(colors.foreground as string),
@@ -191,6 +194,7 @@ export default function SettingsScreen() {
191
194
  </Text>
192
195
  {me?.email ? (
193
196
  <Text
197
+ testID="settings-profile-email"
194
198
  modifiers={[
195
199
  dfont({ size: 14 }),
196
200
  foregroundStyle(colors.mutedForeground as string),
@@ -206,19 +210,22 @@ export default function SettingsScreen() {
206
210
  <Spacer />
207
211
  <Image
208
212
  systemName="chevron.right"
209
- size={13}
213
+ size={symbolSize(13)}
210
214
  color={colors.mutedForeground as string}
215
+ modifiers={[accessibilityHidden(true)]}
211
216
  />
212
217
  </HStack>
213
218
  </Button>
214
219
 
215
220
  <VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
216
221
  {rowButton({
222
+ testID: "settings-sessions",
217
223
  label: "Sessions",
218
224
  systemImage: "list.bullet.rectangle.portrait",
219
225
  onPress: () => navigate("/sessions"),
220
226
  })}
221
227
  {rowButton({
228
+ testID: "settings-preferences",
222
229
  label: "Preferences",
223
230
  systemImage: "slider.horizontal.3",
224
231
  onPress: () => navigate("/settings/preferences"),
@@ -227,22 +234,26 @@ export default function SettingsScreen() {
227
234
 
228
235
  <VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
229
236
  {rowButton({
237
+ testID: "settings-help",
230
238
  label: "Help & Feedback",
231
- systemImage: "bubble.left",
239
+ systemImage: "questionmark.bubble.fill",
232
240
  onPress: () => navigate("/help"),
233
241
  })}
234
242
  {rowButton({
243
+ testID: "settings-privacy",
235
244
  label: "Privacy",
236
- systemImage: "hand.raised",
245
+ systemImage: "lock.shield.fill",
237
246
  onPress: () => navigate("/privacy"),
238
247
  })}
239
248
  {rowButton({
249
+ testID: "settings-copy-version",
240
250
  label: "Copy version",
241
251
  systemImage: "doc.on.doc",
242
252
  onPress: handleCopyVersion,
243
253
  })}
244
254
  {debugOn
245
255
  ? rowButton({
256
+ testID: "settings-debug",
246
257
  label: "Debug",
247
258
  systemImage: "ant.circle",
248
259
  onPress: () => navigate(DEBUG_HREF),
@@ -259,6 +270,7 @@ export default function SettingsScreen() {
259
270
  >
260
271
  <ConfirmationDialog.Trigger>
261
272
  {rowButton({
273
+ testID: "settings-sign-out",
262
274
  label: "Sign out",
263
275
  systemImage: "rectangle.portrait.and.arrow.right",
264
276
  onPress: () => setShowSignOut(true),
@@ -266,8 +278,13 @@ export default function SettingsScreen() {
266
278
  })}
267
279
  </ConfirmationDialog.Trigger>
268
280
  <ConfirmationDialog.Actions>
269
- <Button label="Sign Out" role="destructive" onPress={handleSignOut} />
270
- <Button label="Cancel" role="cancel" />
281
+ <Button
282
+ testID="settings-sign-out-confirm"
283
+ label="Sign Out"
284
+ role="destructive"
285
+ onPress={handleSignOut}
286
+ />
287
+ <Button testID="settings-sign-out-cancel" label="Cancel" role="cancel" />
271
288
  </ConfirmationDialog.Actions>
272
289
  <ConfirmationDialog.Message>
273
290
  <Text modifiers={[dfont({ size: 16 })]}>
@@ -276,35 +293,45 @@ export default function SettingsScreen() {
276
293
  </ConfirmationDialog.Message>
277
294
  </ConfirmationDialog>
278
295
 
279
- <ConfirmationDialog
296
+ {/* upstream expo/expo#45700: Alert component, SwiftUI .alert(...) on iOS 15+ */}
297
+ <Alert
280
298
  title="Delete account?"
281
299
  isPresented={showDeleteAccount}
282
300
  onIsPresentedChange={setShowDeleteAccount}
283
- titleVisibility="visible"
284
301
  >
285
- <ConfirmationDialog.Trigger>
302
+ <Alert.Trigger>
286
303
  {rowButton({
304
+ testID: "settings-delete-account",
287
305
  label: "Delete account",
288
306
  systemImage: "trash",
289
307
  onPress: () => setShowDeleteAccount(true),
290
308
  role: "destructive",
291
309
  })}
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>
310
+ </Alert.Trigger>
311
+ <Alert.Actions>
312
+ <Button
313
+ testID="settings-delete-account-confirm"
314
+ label="Delete Account"
315
+ role="destructive"
316
+ onPress={deleteAccount}
317
+ />
318
+ <Button testID="settings-delete-account-cancel" label="Cancel" role="cancel" />
319
+ </Alert.Actions>
320
+ <Alert.Message>
298
321
  <Text modifiers={[dfont({ size: 16 })]}>
299
- This permanently deletes your account and all data. This cannot be undone.
322
+ Your account is scheduled for permanent deletion in 30 days. Sign in within that
323
+ window to restore it.
300
324
  </Text>
301
- </ConfirmationDialog.Message>
302
- </ConfirmationDialog>
325
+ </Alert.Message>
326
+ </Alert>
303
327
  </VStack>
304
328
 
305
- <HStack modifiers={[frame({ maxWidth: 10000 }), padding({ top: 16 })]}>
329
+ {deleteError ? <ErrorText testID="settings-delete-error">{deleteError}</ErrorText> : null}
330
+
331
+ <HStack modifiers={[frame({ maxWidth: Infinity }), padding({ top: 16 })]}>
306
332
  <Spacer />
307
333
  <Text
334
+ testID="settings-version"
308
335
  modifiers={[dfont({ size: 12 }), foregroundStyle(colors.tertiaryLabel as string)]}
309
336
  >
310
337
  v{version}
@@ -327,7 +354,10 @@ function ProfileHeaderAvatar({ avatarUrl }: { avatarUrl: string | null }) {
327
354
  systemName="person.crop.circle.fill"
328
355
  size={HEADER_AVATAR_SIZE}
329
356
  color={colors.mutedForeground as string}
330
- modifiers={[frame({ width: HEADER_AVATAR_SIZE, height: HEADER_AVATAR_SIZE })]}
357
+ modifiers={[
358
+ frame({ width: HEADER_AVATAR_SIZE, height: HEADER_AVATAR_SIZE }),
359
+ accessibilityHidden(true),
360
+ ]}
331
361
  />
332
362
  );
333
363
  }
@@ -341,7 +371,7 @@ function RemoteAvatar({ url, size }: { url: string; size: number }) {
341
371
  systemName="person.crop.circle.fill"
342
372
  size={size}
343
373
  color={colors.mutedForeground as string}
344
- modifiers={[frame({ width: size, height: size })]}
374
+ modifiers={[frame({ width: size, height: size }), accessibilityHidden(true)]}
345
375
  />
346
376
  );
347
377
  }
@@ -10,9 +10,12 @@ import {
10
10
  Image,
11
11
  } from "@expo/ui/swift-ui";
12
12
  import {
13
+ accessibilityHidden,
14
+ accessibilityLabel,
13
15
  background,
14
16
  clipShape,
15
17
  controlSize,
18
+ dynamicTypeSize,
16
19
  foregroundStyle,
17
20
  frame,
18
21
  padding,
@@ -22,6 +25,7 @@ import {
22
25
  tint,
23
26
  } from "@expo/ui/swift-ui/modifiers";
24
27
  import { Button as ButtonTokens } from "@/constants/layout";
28
+ import { DynamicType } from "@/constants/ui";
25
29
 
26
30
  import { haptics } from "@/lib/haptics";
27
31
  import { useColors, useThemeMode, type ThemeMode } from "@/hooks/use-theme";
@@ -32,6 +36,7 @@ import {
32
36
  type ReduceMotionPref,
33
37
  } from "@/lib/preferences";
34
38
  import { useDynamicFont } from "@/lib/dynamic-font";
39
+ import { useSymbolSize } from "@/lib/dynamic-symbol-size";
35
40
 
36
41
  const MODE_BY_INDEX: ThemeMode[] = ["light", "dark", "system"];
37
42
  const INDEX_BY_MODE: Record<ThemeMode, number> = { light: 0, dark: 1, system: 2 };
@@ -45,6 +50,7 @@ const INDEX_BY_MOTION: Record<ReduceMotionPref, number> = {
45
50
 
46
51
  export default function PreferencesScreen() {
47
52
  const dfont = useDynamicFont();
53
+ const symbolSize = useSymbolSize();
48
54
  const colors = useColors();
49
55
  const { mode, setMode } = useThemeMode();
50
56
  const [hapticsOn, setHapticsOn] = useHapticsEnabled();
@@ -64,11 +70,13 @@ export default function PreferencesScreen() {
64
70
  ];
65
71
 
66
72
  const toggleRow = ({
73
+ testID,
67
74
  icon,
68
75
  label,
69
76
  value,
70
77
  onChange,
71
78
  }: {
79
+ testID: string;
72
80
  icon: NonNullable<React.ComponentProps<typeof Image>["systemName"]>;
73
81
  label: string;
74
82
  value: boolean;
@@ -78,13 +86,18 @@ export default function PreferencesScreen() {
78
86
  spacing={12}
79
87
  alignment="center"
80
88
  modifiers={[
81
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
89
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
82
90
  padding({ horizontal: 16 }),
83
91
  background(colors.muted as string),
84
92
  clipShape("capsule"),
85
93
  ]}
86
94
  >
87
- <Image systemName={icon} size={18} color={colors.foreground as string} />
95
+ <Image
96
+ systemName={icon}
97
+ size={symbolSize(18)}
98
+ color={colors.foreground as string}
99
+ modifiers={[accessibilityHidden(true)]}
100
+ />
88
101
  <Text
89
102
  modifiers={[
90
103
  dfont({ size: 16, weight: "medium" }),
@@ -94,12 +107,17 @@ export default function PreferencesScreen() {
94
107
  {label}
95
108
  </Text>
96
109
  <Spacer />
97
- <Toggle isOn={value} onIsOnChange={onChange} modifiers={[tint(colors.primary as string)]} />
110
+ <Toggle
111
+ testID={testID}
112
+ isOn={value}
113
+ onIsOnChange={onChange}
114
+ modifiers={[tint(colors.primary as string), accessibilityLabel(label)]}
115
+ />
98
116
  </HStack>
99
117
  );
100
118
 
101
119
  return (
102
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
120
+ <Host testID="preferences-screen" style={{ flex: 1, backgroundColor: colors.background }}>
103
121
  <ScrollView
104
122
  modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
105
123
  >
@@ -111,10 +129,15 @@ export default function PreferencesScreen() {
111
129
  <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
112
130
  <Text modifiers={sectionLabelModifiers}>APPEARANCE</Text>
113
131
  <Picker
132
+ testID="preferences-appearance"
114
133
  modifiers={[
115
134
  pickerStyle("segmented"),
116
135
  controlSize("large"),
117
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
136
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
137
+ // upstream expo/expo#46540: fixed segments can't reflow, so cap
138
+ // Dynamic Type before the labels truncate at AX sizes.
139
+ dynamicTypeSize({ max: DynamicType.control }),
140
+ accessibilityLabel("Appearance"),
118
141
  ]}
119
142
  selection={INDEX_BY_MODE[mode]}
120
143
  onSelectionChange={(v) => {
@@ -122,19 +145,39 @@ export default function PreferencesScreen() {
122
145
  setMode(MODE_BY_INDEX[v as number] ?? "system");
123
146
  }}
124
147
  >
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>
148
+ <Text
149
+ testID="preferences-appearance-light"
150
+ modifiers={[tag(0), dfont({ size: 14, weight: "medium" })]}
151
+ >
152
+ Light
153
+ </Text>
154
+ <Text
155
+ testID="preferences-appearance-dark"
156
+ modifiers={[tag(1), dfont({ size: 14, weight: "medium" })]}
157
+ >
158
+ Dark
159
+ </Text>
160
+ <Text
161
+ testID="preferences-appearance-system"
162
+ modifiers={[tag(2), dfont({ size: 14, weight: "medium" })]}
163
+ >
164
+ System
165
+ </Text>
128
166
  </Picker>
129
167
  </VStack>
130
168
 
131
169
  <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
132
170
  <Text modifiers={sectionLabelModifiers}>REDUCE MOTION</Text>
133
171
  <Picker
172
+ testID="preferences-reduce-motion"
134
173
  modifiers={[
135
174
  pickerStyle("segmented"),
136
175
  controlSize("large"),
137
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
176
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
177
+ // upstream expo/expo#46540: fixed segments can't reflow, so cap
178
+ // Dynamic Type before the labels truncate at AX sizes.
179
+ dynamicTypeSize({ max: DynamicType.control }),
180
+ accessibilityLabel("Reduce motion"),
138
181
  ]}
139
182
  selection={INDEX_BY_MOTION[motion]}
140
183
  onSelectionChange={(v) => {
@@ -142,15 +185,31 @@ export default function PreferencesScreen() {
142
185
  setMotion(MOTION_BY_INDEX[v as number] ?? "system");
143
186
  }}
144
187
  >
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>
188
+ <Text
189
+ testID="preferences-reduce-motion-system"
190
+ modifiers={[tag(0), dfont({ size: 14, weight: "medium" })]}
191
+ >
192
+ System
193
+ </Text>
194
+ <Text
195
+ testID="preferences-reduce-motion-always"
196
+ modifiers={[tag(1), dfont({ size: 14, weight: "medium" })]}
197
+ >
198
+ Always
199
+ </Text>
200
+ <Text
201
+ testID="preferences-reduce-motion-never"
202
+ modifiers={[tag(2), dfont({ size: 14, weight: "medium" })]}
203
+ >
204
+ Never
205
+ </Text>
148
206
  </Picker>
149
207
  </VStack>
150
208
 
151
209
  <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
152
210
  <Text modifiers={sectionLabelModifiers}>HAPTICS</Text>
153
211
  {toggleRow({
212
+ testID: "preferences-haptics",
154
213
  icon: "iphone.radiowaves.left.and.right",
155
214
  label: "Haptic feedback",
156
215
  value: hapticsOn,
@@ -164,6 +223,7 @@ export default function PreferencesScreen() {
164
223
  <VStack spacing={8} alignment="leading" modifiers={[frame({ maxWidth: Infinity })]}>
165
224
  <Text modifiers={sectionLabelModifiers}>DEBUG</Text>
166
225
  {toggleRow({
226
+ testID: "preferences-debug",
167
227
  icon: "ant.circle.fill",
168
228
  label: "Debug mode",
169
229
  value: debugOn,