@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.
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
@@ -11,20 +11,22 @@ import {
11
11
  Spacer,
12
12
  Image,
13
13
  Text,
14
- ContentUnavailableView,
14
+ DisclosureGroup,
15
15
  } from "@expo/ui/swift-ui";
16
16
  import {
17
+ accessibilityHidden,
17
18
  background,
18
19
  buttonStyle,
19
20
  clipShape,
20
21
  cornerRadius,
21
22
  foregroundStyle,
22
23
  frame,
23
- onTapGesture,
24
24
  padding,
25
25
  tint,
26
26
  } from "@expo/ui/swift-ui/modifiers";
27
27
  import { useDynamicFont } from "@/lib/dynamic-font";
28
+ import { useSymbolSize } from "@/lib/dynamic-symbol-size";
29
+ import { ContentUnavailable } from "@/components/ui/content-unavailable";
28
30
  import { Button as ButtonTokens } from "@/constants/layout";
29
31
 
30
32
  import { ErrorText } from "@/components/ui/status-text";
@@ -41,10 +43,12 @@ const support = (Constants.expoConfig?.extra?.support ?? {}) as SupportConfig;
41
43
 
42
44
  const FAQ_ITEMS = [
43
45
  {
46
+ id: "delete-account",
44
47
  question: "How do I delete my account?",
45
48
  answer: "Go to Settings, then Delete Account. This will permanently remove all your data.",
46
49
  },
47
50
  {
51
+ id: "notifications",
48
52
  question: "Why aren't notifications working?",
49
53
  answer:
50
54
  "Make sure notifications are enabled in Settings, then Notifications. You must use a physical device.",
@@ -53,10 +57,15 @@ const FAQ_ITEMS = [
53
57
 
54
58
  export default function HelpScreen() {
55
59
  const dfont = useDynamicFont();
60
+ const symbolSize = useSymbolSize();
56
61
  const colors = useColors();
57
62
  const [searchText, setSearchText] = useState("");
58
63
  const [expanded, setExpanded] = useState<Record<string, boolean>>({});
59
- const [emailError, setEmailError] = useState<string | null>(null);
64
+ const [linkError, setLinkError] = useState<string | null>(null);
65
+ const toggleExpanded = (question: string, next: boolean) => {
66
+ haptics.selection();
67
+ setExpanded((m) => ({ ...m, [question]: next }));
68
+ };
60
69
 
61
70
  const filteredFaq = searchText
62
71
  ? FAQ_ITEMS.filter(
@@ -68,41 +77,51 @@ export default function HelpScreen() {
68
77
 
69
78
  const issuesUrl = support.issuesUrl || support.githubUrl;
70
79
 
71
- const handleOpenIssues = () => {
80
+ const handleOpenIssues = async () => {
72
81
  if (!issuesUrl) return;
73
82
  haptics.light();
74
- openURL(issuesUrl);
83
+ setLinkError(null);
84
+ const canOpen = await canOpenURL(issuesUrl);
85
+ if (canOpen) {
86
+ openURL(issuesUrl);
87
+ } else {
88
+ haptics.error();
89
+ setLinkError("Couldn't open the issues page.");
90
+ }
75
91
  };
76
92
 
77
93
  const handleOpenEmail = async () => {
78
94
  if (!support.email) return;
79
95
  haptics.light();
80
- setEmailError(null);
81
- const url = `mailto:${support.email}?subject=App Support`;
96
+ setLinkError(null);
97
+ const url = `mailto:${support.email}?subject=${encodeURIComponent("App Support")}`;
82
98
  const canOpen = await canOpenURL(url);
83
99
  if (canOpen) {
84
100
  openURL(url);
85
101
  } else {
86
102
  haptics.error();
87
- setEmailError(`No email app configured. Contact ${support.email} directly.`);
103
+ setLinkError(`No email app configured. Contact ${support.email} directly.`);
88
104
  }
89
105
  };
90
106
 
91
107
  type SFSymbol = NonNullable<ComponentProps<typeof Image>["systemName"]>;
92
108
 
93
109
  const rowButton = ({
110
+ testID,
94
111
  label,
95
112
  systemImage,
96
113
  onPress,
97
114
  }: {
115
+ testID: string;
98
116
  label: string;
99
117
  systemImage: SFSymbol;
100
118
  onPress: () => void;
101
119
  }) => (
102
120
  <Button
121
+ testID={testID}
103
122
  modifiers={[
104
123
  buttonStyle("plain"),
105
- frame({ maxWidth: 10000 }),
124
+ frame({ maxWidth: Infinity }),
106
125
  background(colors.muted as string),
107
126
  clipShape("capsule"),
108
127
  ]}
@@ -112,11 +131,16 @@ export default function HelpScreen() {
112
131
  spacing={12}
113
132
  alignment="center"
114
133
  modifiers={[
115
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
134
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
116
135
  padding({ horizontal: 16 }),
117
136
  ]}
118
137
  >
119
- <Image systemName={systemImage} size={18} color={colors.foreground as string} />
138
+ <Image
139
+ systemName={systemImage}
140
+ size={symbolSize(18)}
141
+ color={colors.foreground as string}
142
+ modifiers={[accessibilityHidden(true)]}
143
+ />
120
144
  <Text
121
145
  modifiers={[
122
146
  dfont({ size: 16, weight: "medium" }),
@@ -126,7 +150,12 @@ export default function HelpScreen() {
126
150
  {label}
127
151
  </Text>
128
152
  <Spacer />
129
- <Image systemName="chevron.right" size={13} color={colors.mutedForeground as string} />
153
+ <Image
154
+ systemName="chevron.right"
155
+ size={symbolSize(13)}
156
+ color={colors.mutedForeground as string}
157
+ modifiers={[accessibilityHidden(true)]}
158
+ />
130
159
  </HStack>
131
160
  </Button>
132
161
  );
@@ -148,19 +177,20 @@ export default function HelpScreen() {
148
177
  />
149
178
  </Stack.Toolbar>
150
179
  ) : null}
151
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
180
+ <Host testID="help-screen" style={{ flex: 1, backgroundColor: colors.background }}>
152
181
  <ScrollView modifiers={[tint(colors.primary as string)]}>
153
182
  <VStack
154
183
  spacing={12}
155
184
  alignment="leading"
156
185
  modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
157
186
  >
158
- {emailError ? <ErrorText>{emailError}</ErrorText> : null}
187
+ {linkError ? <ErrorText testID="help-link-error">{linkError}</ErrorText> : null}
159
188
 
160
189
  {(support.email || issuesUrl) && (
161
190
  <VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
162
191
  {support.email
163
192
  ? rowButton({
193
+ testID: "help-email-support",
164
194
  label: "Email Support",
165
195
  systemImage: "envelope.fill",
166
196
  onPress: handleOpenEmail,
@@ -168,6 +198,7 @@ export default function HelpScreen() {
168
198
  : null}
169
199
  {issuesUrl
170
200
  ? rowButton({
201
+ testID: "help-report-issue",
171
202
  label: "Report an Issue",
172
203
  systemImage: "exclamationmark.bubble.fill",
173
204
  onPress: handleOpenIssues,
@@ -177,7 +208,8 @@ export default function HelpScreen() {
177
208
  )}
178
209
 
179
210
  {filteredFaq.length === 0 ? (
180
- <ContentUnavailableView
211
+ <ContentUnavailable
212
+ testID="help-faq-empty"
181
213
  title="No results"
182
214
  systemImage="magnifyingglass"
183
215
  description="Try a different search term"
@@ -185,6 +217,7 @@ export default function HelpScreen() {
185
217
  ) : (
186
218
  <VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
187
219
  <Text
220
+ testID="help-faq-heading"
188
221
  modifiers={[
189
222
  dfont({ size: 13, weight: "semibold" }),
190
223
  foregroundStyle(colors.mutedForeground as string),
@@ -193,57 +226,36 @@ export default function HelpScreen() {
193
226
  >
194
227
  FREQUENTLY ASKED
195
228
  </Text>
196
- {filteredFaq.map((item) => {
197
- const isOpen = !!expanded[item.question];
198
- return (
199
- <VStack
200
- key={item.question}
201
- alignment="leading"
202
- spacing={isOpen ? 8 : 0}
203
- modifiers={[
204
- frame({ maxWidth: 10000 }),
205
- background(colors.muted as string),
206
- cornerRadius(20),
207
- padding({ horizontal: 20, vertical: 12 }),
208
- onTapGesture(() => {
209
- haptics.selection();
210
- setExpanded((m) => ({ ...m, [item.question]: !m[item.question] }));
211
- }),
212
- ]}
229
+ {filteredFaq.map((item) => (
230
+ <VStack
231
+ key={item.question}
232
+ alignment="leading"
233
+ modifiers={[
234
+ frame({ maxWidth: Infinity }),
235
+ padding({ horizontal: 20, vertical: 4 }),
236
+ background(colors.muted as string),
237
+ cornerRadius(20),
238
+ ]}
239
+ >
240
+ <DisclosureGroup
241
+ testID={`help-faq-${item.id}`}
242
+ label={item.question}
243
+ isExpanded={!!expanded[item.question]}
244
+ onIsExpandedChange={(v) => toggleExpanded(item.question, v)}
245
+ modifiers={[dfont({ size: 16, weight: "medium" })]}
213
246
  >
214
- <HStack
215
- spacing={12}
216
- alignment="center"
217
- modifiers={[frame({ maxWidth: 10000 })]}
247
+ <Text
248
+ modifiers={[
249
+ dfont({ size: 14 }),
250
+ foregroundStyle(colors.mutedForeground as string),
251
+ padding({ vertical: 8 }),
252
+ ]}
218
253
  >
219
- <Text
220
- modifiers={[
221
- dfont({ size: 15, weight: "medium" }),
222
- foregroundStyle(colors.foreground as string),
223
- ]}
224
- >
225
- {item.question}
226
- </Text>
227
- <Spacer />
228
- <Image
229
- systemName={isOpen ? "chevron.up" : "chevron.down"}
230
- size={13}
231
- color={colors.mutedForeground as string}
232
- />
233
- </HStack>
234
- {isOpen ? (
235
- <Text
236
- modifiers={[
237
- dfont({ size: 14 }),
238
- foregroundStyle(colors.mutedForeground as string),
239
- ]}
240
- >
241
- {item.answer}
242
- </Text>
243
- ) : null}
244
- </VStack>
245
- );
246
- })}
254
+ {item.answer}
255
+ </Text>
256
+ </DisclosureGroup>
257
+ </VStack>
258
+ ))}
247
259
  </VStack>
248
260
  )}
249
261
  </VStack>
@@ -1,13 +1,5 @@
1
1
  import { useLocalSearchParams } from "expo-router";
2
- import {
3
- Host,
4
- ScrollView,
5
- Text,
6
- VStack,
7
- HStack,
8
- Spacer,
9
- ContentUnavailableView,
10
- } from "@expo/ui/swift-ui";
2
+ import { Host, ScrollView, Text, VStack, LabeledContent } from "@expo/ui/swift-ui";
11
3
  import {
12
4
  background,
13
5
  cornerRadius,
@@ -20,6 +12,7 @@ import {
20
12
  } from "@expo/ui/swift-ui/modifiers";
21
13
  import { useDynamicFont } from "@/lib/dynamic-font";
22
14
 
15
+ import { ContentUnavailable } from "@/components/ui/content-unavailable";
23
16
  import { useColors } from "@/hooks/use-theme";
24
17
 
25
18
  export default function LinkedScreen() {
@@ -30,7 +23,7 @@ export default function LinkedScreen() {
30
23
  const entries = Object.entries(params).filter(([, v]) => v != null);
31
24
 
32
25
  return (
33
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
26
+ <Host testID="linked-screen" style={{ flex: 1, backgroundColor: colors.background }}>
34
27
  <ScrollView
35
28
  modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
36
29
  >
@@ -40,7 +33,7 @@ export default function LinkedScreen() {
40
33
  modifiers={[padding({ horizontal: 24, top: 24, bottom: 40 })]}
41
34
  >
42
35
  <VStack spacing={6} alignment="leading">
43
- <Text modifiers={[dfont({ size: 22, weight: "bold" })]}>
36
+ <Text testID="linked-title" modifiers={[dfont({ size: 22, weight: "bold" })]}>
44
37
  You got here via a deep link
45
38
  </Text>
46
39
  <Text
@@ -65,31 +58,31 @@ export default function LinkedScreen() {
65
58
  spacing={0}
66
59
  alignment="leading"
67
60
  modifiers={[
68
- frame({ maxWidth: 10000 }),
61
+ frame({ maxWidth: Infinity }),
69
62
  background(colors.muted as string),
70
63
  cornerRadius(20),
71
64
  ]}
72
65
  >
73
66
  {entries.map(([key, value]) => (
74
- <HStack
67
+ <LabeledContent
75
68
  key={key}
76
- spacing={12}
77
- alignment="center"
69
+ label={
70
+ <Text
71
+ modifiers={[
72
+ dfont({ size: 15 }),
73
+ foregroundStyle(colors.mutedForeground as string),
74
+ ]}
75
+ >
76
+ {key}
77
+ </Text>
78
+ }
78
79
  modifiers={[
79
- frame({ maxWidth: 10000 }),
80
+ frame({ maxWidth: Infinity }),
80
81
  padding({ horizontal: 16, vertical: 12 }),
81
82
  ]}
82
83
  >
83
84
  <Text
84
- modifiers={[
85
- dfont({ size: 15 }),
86
- foregroundStyle(colors.mutedForeground as string),
87
- ]}
88
- >
89
- {key}
90
- </Text>
91
- <Spacer />
92
- <Text
85
+ testID={`linked-param-${key}`}
93
86
  modifiers={[
94
87
  dfont({ size: 13, design: "monospaced" }),
95
88
  foregroundStyle(colors.foreground as string),
@@ -98,12 +91,13 @@ export default function LinkedScreen() {
98
91
  >
99
92
  {Array.isArray(value) ? value.join(", ") : String(value)}
100
93
  </Text>
101
- </HStack>
94
+ </LabeledContent>
102
95
  ))}
103
96
  </VStack>
104
97
  </VStack>
105
98
  ) : (
106
- <ContentUnavailableView
99
+ <ContentUnavailable
100
+ testID="linked-empty"
107
101
  title="No params"
108
102
  systemImage="link.badge.plus"
109
103
  description="This deep link didn't include any parameters."
@@ -12,6 +12,8 @@ import {
12
12
  Toggle,
13
13
  } from "@expo/ui/swift-ui";
14
14
  import {
15
+ accessibilityHidden,
16
+ accessibilityLabel,
15
17
  background,
16
18
  buttonStyle,
17
19
  clipShape,
@@ -21,6 +23,7 @@ import {
21
23
  tint,
22
24
  } from "@expo/ui/swift-ui/modifiers";
23
25
  import { useDynamicFont } from "@/lib/dynamic-font";
26
+ import { useSymbolSize } from "@/lib/dynamic-symbol-size";
24
27
  import { Button as ButtonTokens } from "@/constants/layout";
25
28
 
26
29
  import { haptics } from "@/lib/haptics";
@@ -28,6 +31,7 @@ import { useColors } from "@/hooks/use-theme";
28
31
 
29
32
  export default function PrivacyScreen() {
30
33
  const dfont = useDynamicFont();
34
+ const symbolSize = useSymbolSize();
31
35
  const colors = useColors();
32
36
  const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
33
37
 
@@ -39,12 +43,14 @@ export default function PrivacyScreen() {
39
43
  type SFSymbol = NonNullable<ComponentProps<typeof Image>["systemName"]>;
40
44
 
41
45
  const rowButton = ({
46
+ testID,
42
47
  label,
43
48
  systemImage,
44
49
  onPress,
45
50
  chevron = true,
46
51
  trailing,
47
52
  }: {
53
+ testID: string;
48
54
  label: string;
49
55
  systemImage: SFSymbol;
50
56
  onPress: () => void;
@@ -52,9 +58,10 @@ export default function PrivacyScreen() {
52
58
  trailing?: React.ReactNode;
53
59
  }) => (
54
60
  <Button
61
+ testID={testID}
55
62
  modifiers={[
56
63
  buttonStyle("plain"),
57
- frame({ maxWidth: 10000 }),
64
+ frame({ maxWidth: Infinity }),
58
65
  background(colors.muted as string),
59
66
  clipShape("capsule"),
60
67
  ]}
@@ -64,11 +71,16 @@ export default function PrivacyScreen() {
64
71
  spacing={12}
65
72
  alignment="center"
66
73
  modifiers={[
67
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
74
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
68
75
  padding({ horizontal: 16 }),
69
76
  ]}
70
77
  >
71
- <Image systemName={systemImage} size={18} color={colors.foreground as string} />
78
+ <Image
79
+ systemName={systemImage}
80
+ size={symbolSize(18)}
81
+ color={colors.foreground as string}
82
+ modifiers={[accessibilityHidden(true)]}
83
+ />
72
84
  <Text
73
85
  modifiers={[
74
86
  dfont({ size: 16, weight: "medium" }),
@@ -80,14 +92,19 @@ export default function PrivacyScreen() {
80
92
  <Spacer />
81
93
  {trailing ??
82
94
  (chevron ? (
83
- <Image systemName="chevron.right" size={13} color={colors.mutedForeground as string} />
95
+ <Image
96
+ systemName="chevron.right"
97
+ size={symbolSize(13)}
98
+ color={colors.mutedForeground as string}
99
+ modifiers={[accessibilityHidden(true)]}
100
+ />
84
101
  ) : null)}
85
102
  </HStack>
86
103
  </Button>
87
104
  );
88
105
 
89
106
  return (
90
- <Host style={{ flex: 1, backgroundColor: colors.background }}>
107
+ <Host testID="privacy-screen" style={{ flex: 1, backgroundColor: colors.background }}>
91
108
  <ScrollView modifiers={[tint(colors.primary as string)]}>
92
109
  <VStack
93
110
  spacing={12}
@@ -96,16 +113,19 @@ export default function PrivacyScreen() {
96
113
  >
97
114
  <VStack spacing={8} modifiers={[frame({ maxWidth: Infinity })]}>
98
115
  {rowButton({
116
+ testID: "privacy-camera-photos",
99
117
  label: "Camera & Photos",
100
118
  systemImage: "camera.fill",
101
119
  onPress: handleOpenSettings,
102
120
  })}
103
121
  {rowButton({
122
+ testID: "privacy-notifications",
104
123
  label: "Notifications",
105
124
  systemImage: "bell.fill",
106
125
  onPress: handleOpenSettings,
107
126
  })}
108
127
  {rowButton({
128
+ testID: "privacy-system-settings",
109
129
  label: "System Settings",
110
130
  systemImage: "gear",
111
131
  onPress: handleOpenSettings,
@@ -117,13 +137,18 @@ export default function PrivacyScreen() {
117
137
  spacing={12}
118
138
  alignment="center"
119
139
  modifiers={[
120
- frame({ maxWidth: 10000, height: ButtonTokens.height }),
140
+ frame({ maxWidth: Infinity, minHeight: ButtonTokens.height }),
121
141
  padding({ horizontal: 16 }),
122
142
  background(colors.muted as string),
123
143
  clipShape("capsule"),
124
144
  ]}
125
145
  >
126
- <Image systemName="chart.bar.fill" size={18} color={colors.foreground as string} />
146
+ <Image
147
+ systemName="chart.bar.fill"
148
+ size={symbolSize(18)}
149
+ color={colors.foreground as string}
150
+ modifiers={[accessibilityHidden(true)]}
151
+ />
127
152
  <Text
128
153
  modifiers={[
129
154
  dfont({ size: 16, weight: "medium" }),
@@ -134,16 +159,18 @@ export default function PrivacyScreen() {
134
159
  </Text>
135
160
  <Spacer />
136
161
  <Toggle
162
+ testID="privacy-share-analytics"
137
163
  isOn={analyticsEnabled}
138
164
  onIsOnChange={(v) => {
139
165
  haptics.selection();
140
166
  setAnalyticsEnabled(v);
141
167
  }}
142
- modifiers={[tint(colors.primary as string)]}
168
+ modifiers={[tint(colors.primary as string), accessibilityLabel("Share analytics")]}
143
169
  />
144
170
  </HStack>
145
171
 
146
172
  <Text
173
+ testID="privacy-data-disclaimer"
147
174
  modifiers={[
148
175
  dfont({ size: 13 }),
149
176
  foregroundStyle(colors.mutedForeground as string),