@ramonclaudio/create-vexpo 0.1.3 → 0.1.5
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/LICENSE +21 -0
- package/README.md +29 -15
- package/dist/index.js +44 -14
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +17 -1
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +3 -3
- package/dist/templates/default/.maestro/auth.yaml +229 -0
- package/dist/templates/default/.maestro/launch.yaml +5 -5
- package/dist/templates/default/.maestro/tour.yaml +294 -0
- package/dist/templates/default/.maestro/zz-delete-restore.yaml +174 -0
- package/dist/templates/default/AGENTS.md +3 -2
- package/dist/templates/default/DESIGN.md +41 -41
- package/dist/templates/default/README.md +46 -40
- package/dist/templates/default/SETUP.md +34 -19
- package/dist/templates/default/_easignore +0 -1
- package/dist/templates/default/_env.example +15 -10
- package/dist/templates/default/app.config.ts +5 -5
- package/dist/templates/default/convex/pushTokens.ts +1 -26
- package/dist/templates/default/convex/rateLimit.ts +1 -21
- package/dist/templates/default/convex/users.ts +1 -49
- package/dist/templates/default/convex/validators.ts +0 -10
- package/dist/templates/default/package.json +1 -1
- package/dist/templates/default/scripts/README.md +24 -8
- package/dist/templates/default/scripts/clean.ts +3 -3
- package/dist/templates/default/scripts/gen-update-cert.mjs +3 -1
- package/dist/templates/default/src/app/(app)/_layout.tsx +15 -1
- package/dist/templates/default/src/app/(app)/auth/forgot-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/reset-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/sign-up.tsx +3 -1
- package/dist/templates/default/src/app/(app)/privacy.tsx +3 -2
- package/dist/templates/default/src/app/(app)/restore-account.tsx +2 -2
- package/dist/templates/default/src/app/(app)/sessions.tsx +15 -5
- package/dist/templates/default/src/components/ui/convex-error.tsx +0 -7
- package/dist/templates/default/src/constants/ui.ts +0 -11
- package/dist/templates/default/src/lib/dev-menu.ts +11 -2
- package/dist/templates/default/src/lib/preferences.ts +9 -0
- package/package.json +3 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Signed-in app tour: walk the authenticated surface without mutating auth.
|
|
2
|
+
# Search -> Settings -> Preferences (appearance + haptics) -> Privacy
|
|
3
|
+
# (analytics toggle) -> Profile (edit + save bio) -> Sessions, then screenshot.
|
|
4
|
+
#
|
|
5
|
+
# Requires an existing signed-in session: this flow launches WITHOUT clearState
|
|
6
|
+
# and asserts the home tab. It does NOT sign in. Run `auth.yaml` first to seed a
|
|
7
|
+
# session. On EAS folder runs Maestro executes flows alphabetically, and
|
|
8
|
+
# `app-tour` sorts BEFORE `auth`, so this file is named `tour.yaml` ('t' > 'a')
|
|
9
|
+
# to guarantee it runs AFTER auth.yaml. As a safety net the flow fails fast with
|
|
10
|
+
# a clear message if it lands on the sign-in screen instead of the tabs.
|
|
11
|
+
#
|
|
12
|
+
# @expo/ui SwiftUI driving notes (same as auth.yaml/launch.yaml):
|
|
13
|
+
# - Inner SwiftUI Text testIDs do NOT surface to Maestro; only Host roots and
|
|
14
|
+
# real controls (TextField, Button, Toggle, Picker) do. Assert screens by
|
|
15
|
+
# their Host `id`, assert static copy by `text`.
|
|
16
|
+
# - Capsule list rows hold a Spacer in the middle, so tap them by label TEXT,
|
|
17
|
+
# not id. ProminentButtons and full-content Buttons hold content in the
|
|
18
|
+
# center, so tap those by `id`.
|
|
19
|
+
# - Segmented Picker segments put their label in the middle: tap by label TEXT
|
|
20
|
+
# (`tapOn: "Dark"`), not by the inner Text testID.
|
|
21
|
+
# - Toggles are real controls: tap by `id`.
|
|
22
|
+
# - The keyboard covers lower fields. Forms use
|
|
23
|
+
# scrollDismissesKeyboard("interactively"), so a downward swipe dismisses it
|
|
24
|
+
# (hideKeyboard is flaky on iOS, don't use it).
|
|
25
|
+
# - waitForAnimationToEnd between navigations; assert-then-tap for tab bars;
|
|
26
|
+
# extendedWaitUntil with generous timeouts after anything that hits Convex.
|
|
27
|
+
#
|
|
28
|
+
# Env vars:
|
|
29
|
+
# MAESTRO_APP_ID bundle id (EAS injects from EXPO_PUBLIC_APP_BUNDLE_ID)
|
|
30
|
+
#
|
|
31
|
+
# Run locally (needs a JDK on PATH, and a signed-in simulator session):
|
|
32
|
+
# MAESTRO_APP_ID=$(grep '^EXPO_PUBLIC_APP_BUNDLE_ID=' .env.local | cut -d= -f2) \
|
|
33
|
+
# maestro test .maestro/tour.yaml
|
|
34
|
+
# Or run the whole suite in order (auth seeds the session for the tour):
|
|
35
|
+
# MAESTRO_APP_ID=... maestro test .maestro/
|
|
36
|
+
# Run on EAS: via `.eas/workflows/e2e-tests.yml` (workflow_dispatch only).
|
|
37
|
+
appId: ${MAESTRO_APP_ID}
|
|
38
|
+
---
|
|
39
|
+
# Resume on whatever was last open (no clearState). Give the JS bundle and the
|
|
40
|
+
# auth guard time to settle.
|
|
41
|
+
- launchApp
|
|
42
|
+
- waitForAnimationToEnd:
|
|
43
|
+
timeout: 15000
|
|
44
|
+
|
|
45
|
+
# Dev-client builds may show the one-time developer-menu intro sheet after a
|
|
46
|
+
# relaunch. Drag it down to dismiss. Release builds skip this block.
|
|
47
|
+
- runFlow:
|
|
48
|
+
when:
|
|
49
|
+
visible: "This is the developer menu.*"
|
|
50
|
+
commands:
|
|
51
|
+
- swipe:
|
|
52
|
+
start: 50%, 55%
|
|
53
|
+
end: 50%, 95%
|
|
54
|
+
- waitForAnimationToEnd:
|
|
55
|
+
timeout: 5000
|
|
56
|
+
|
|
57
|
+
# The intro sheet can appear late while Metro is still bundling, so check a
|
|
58
|
+
# second time after the bundle settles.
|
|
59
|
+
- waitForAnimationToEnd:
|
|
60
|
+
timeout: 10000
|
|
61
|
+
- runFlow:
|
|
62
|
+
when:
|
|
63
|
+
visible: "This is the developer menu.*"
|
|
64
|
+
commands:
|
|
65
|
+
- swipe:
|
|
66
|
+
start: 50%, 55%
|
|
67
|
+
end: 50%, 95%
|
|
68
|
+
- waitForAnimationToEnd:
|
|
69
|
+
timeout: 5000
|
|
70
|
+
|
|
71
|
+
# Safety net: if the session is missing we land on sign-in. Fail loudly so the
|
|
72
|
+
# failure points at "run auth.yaml first", not at a mystery missing element.
|
|
73
|
+
- assertNotVisible:
|
|
74
|
+
id: "sign-in-screen"
|
|
75
|
+
|
|
76
|
+
# launchApp may resume on a nested screen. Get back to the tabs root via the
|
|
77
|
+
# Home tab, then assert the home Host id. Native tab taps can be flaky, so
|
|
78
|
+
# assert-then-tap.
|
|
79
|
+
- assertVisible: "Home"
|
|
80
|
+
- tapOn: "Home"
|
|
81
|
+
- waitForAnimationToEnd:
|
|
82
|
+
timeout: 5000
|
|
83
|
+
- extendedWaitUntil:
|
|
84
|
+
visible:
|
|
85
|
+
id: "home-screen"
|
|
86
|
+
timeout: 20000
|
|
87
|
+
|
|
88
|
+
# --- 1. Search tab -------------------------------------------------------
|
|
89
|
+
# Search is a native large-title Stack.SearchBar above a Host list. Tap the tab,
|
|
90
|
+
# assert the screen root, then drive the search field by its placeholder.
|
|
91
|
+
- assertVisible: "Search"
|
|
92
|
+
- tapOn: "Search"
|
|
93
|
+
- waitForAnimationToEnd:
|
|
94
|
+
timeout: 5000
|
|
95
|
+
- extendedWaitUntil:
|
|
96
|
+
visible:
|
|
97
|
+
id: "search-screen"
|
|
98
|
+
timeout: 15000
|
|
99
|
+
|
|
100
|
+
# Tap the native search field (matched by its placeholder), type a query, then
|
|
101
|
+
# swipe-dismiss the keyboard. The list filters live; assert the screen survives.
|
|
102
|
+
- tapOn: "Search screens"
|
|
103
|
+
- inputText: "privacy"
|
|
104
|
+
- swipe:
|
|
105
|
+
start: 50%, 35%
|
|
106
|
+
end: 50%, 85%
|
|
107
|
+
- waitForAnimationToEnd:
|
|
108
|
+
timeout: 3000
|
|
109
|
+
- assertVisible:
|
|
110
|
+
id: "search-screen"
|
|
111
|
+
# A result for "privacy" should rank. The result row carries a stable id.
|
|
112
|
+
- assertVisible:
|
|
113
|
+
id: "search-result-privacy"
|
|
114
|
+
# Active search replaces the tab bar, close it ("Close" a11y label) so the
|
|
115
|
+
# tabs come back before navigating on.
|
|
116
|
+
- tapOn: "Close"
|
|
117
|
+
- waitForAnimationToEnd:
|
|
118
|
+
timeout: 3000
|
|
119
|
+
|
|
120
|
+
# --- 2. Settings tab -----------------------------------------------------
|
|
121
|
+
- assertVisible: "Settings"
|
|
122
|
+
- tapOn: "Settings"
|
|
123
|
+
- waitForAnimationToEnd:
|
|
124
|
+
timeout: 5000
|
|
125
|
+
- extendedWaitUntil:
|
|
126
|
+
visible:
|
|
127
|
+
id: "settings-screen"
|
|
128
|
+
timeout: 15000
|
|
129
|
+
|
|
130
|
+
# --- 3. Preferences ------------------------------------------------------
|
|
131
|
+
# Open via the settings row. Capsule row with a Spacer, so tap by label text.
|
|
132
|
+
- assertVisible: "Preferences"
|
|
133
|
+
- tapOn: "Preferences"
|
|
134
|
+
- waitForAnimationToEnd:
|
|
135
|
+
timeout: 5000
|
|
136
|
+
- extendedWaitUntil:
|
|
137
|
+
visible:
|
|
138
|
+
id: "preferences-screen"
|
|
139
|
+
timeout: 15000
|
|
140
|
+
- assertVisible: "APPEARANCE"
|
|
141
|
+
|
|
142
|
+
# Appearance is a segmented Picker. Segment labels live in the middle of each
|
|
143
|
+
# segment, so tap by text: System -> Dark -> back to System.
|
|
144
|
+
- tapOn: "Dark"
|
|
145
|
+
- waitForAnimationToEnd:
|
|
146
|
+
timeout: 3000
|
|
147
|
+
- tapOn: "System"
|
|
148
|
+
- waitForAnimationToEnd:
|
|
149
|
+
timeout: 3000
|
|
150
|
+
|
|
151
|
+
# Haptics is a real Toggle in a capsule row, so tap by id. Toggle off, toggle
|
|
152
|
+
# back on. The screen redraws under it but the Host id stays stable.
|
|
153
|
+
- assertVisible: "HAPTICS"
|
|
154
|
+
- tapOn:
|
|
155
|
+
id: "preferences-haptics"
|
|
156
|
+
- waitForAnimationToEnd:
|
|
157
|
+
timeout: 2000
|
|
158
|
+
- tapOn:
|
|
159
|
+
id: "preferences-haptics"
|
|
160
|
+
- waitForAnimationToEnd:
|
|
161
|
+
timeout: 2000
|
|
162
|
+
|
|
163
|
+
# Back to Settings.
|
|
164
|
+
- swipe:
|
|
165
|
+
start: 2%, 50%
|
|
166
|
+
end: 90%, 50%
|
|
167
|
+
- waitForAnimationToEnd:
|
|
168
|
+
timeout: 5000
|
|
169
|
+
- extendedWaitUntil:
|
|
170
|
+
visible:
|
|
171
|
+
id: "settings-screen"
|
|
172
|
+
timeout: 15000
|
|
173
|
+
|
|
174
|
+
# --- 4. Privacy ----------------------------------------------------------
|
|
175
|
+
# Open via the settings row (capsule, tap by text).
|
|
176
|
+
- assertVisible: "Privacy"
|
|
177
|
+
- tapOn: "Privacy"
|
|
178
|
+
- waitForAnimationToEnd:
|
|
179
|
+
timeout: 5000
|
|
180
|
+
- extendedWaitUntil:
|
|
181
|
+
visible:
|
|
182
|
+
id: "privacy-screen"
|
|
183
|
+
timeout: 15000
|
|
184
|
+
|
|
185
|
+
# Share Analytics is a real Toggle in a capsule row, so tap by id. This is a
|
|
186
|
+
# persisted preference (useShareAnalytics). Flip it off, assert the screen
|
|
187
|
+
# holds, flip it back on so the tour leaves no state behind.
|
|
188
|
+
- assertVisible: "Share Analytics"
|
|
189
|
+
- tapOn:
|
|
190
|
+
id: "privacy-share-analytics"
|
|
191
|
+
- waitForAnimationToEnd:
|
|
192
|
+
timeout: 2000
|
|
193
|
+
- assertVisible:
|
|
194
|
+
id: "privacy-screen"
|
|
195
|
+
- tapOn:
|
|
196
|
+
id: "privacy-share-analytics"
|
|
197
|
+
- waitForAnimationToEnd:
|
|
198
|
+
timeout: 2000
|
|
199
|
+
|
|
200
|
+
# Back to Settings.
|
|
201
|
+
- swipe:
|
|
202
|
+
start: 2%, 50%
|
|
203
|
+
end: 90%, 50%
|
|
204
|
+
- waitForAnimationToEnd:
|
|
205
|
+
timeout: 5000
|
|
206
|
+
- extendedWaitUntil:
|
|
207
|
+
visible:
|
|
208
|
+
id: "settings-screen"
|
|
209
|
+
timeout: 15000
|
|
210
|
+
|
|
211
|
+
# --- 5. Profile ----------------------------------------------------------
|
|
212
|
+
# Open via the settings profile row. Full-content Button, so tap by id.
|
|
213
|
+
- tapOn:
|
|
214
|
+
id: "settings-profile"
|
|
215
|
+
- waitForAnimationToEnd:
|
|
216
|
+
timeout: 5000
|
|
217
|
+
- extendedWaitUntil:
|
|
218
|
+
visible:
|
|
219
|
+
id: "profile-screen"
|
|
220
|
+
timeout: 15000
|
|
221
|
+
|
|
222
|
+
# Edit the Bio field. It is a plain TextField (no newPassword autofill, no eye
|
|
223
|
+
# toggle needed). Tap to focus, append text so `hasChanges` flips true, then
|
|
224
|
+
# swipe-dismiss the keyboard. The avatar button exists but the image picker
|
|
225
|
+
# opens a system sheet Maestro can't drive, so it is left untouched.
|
|
226
|
+
- tapOn:
|
|
227
|
+
id: "profile-bio"
|
|
228
|
+
- inputText: " e2e tour"
|
|
229
|
+
- swipe:
|
|
230
|
+
start: 50%, 35%
|
|
231
|
+
end: 50%, 85%
|
|
232
|
+
- waitForAnimationToEnd:
|
|
233
|
+
timeout: 3000
|
|
234
|
+
|
|
235
|
+
# Save. The inline Save button only renders once `hasChanges` is true and holds
|
|
236
|
+
# its label in the center, so tap by id. (A toolbar Save button also exists, but
|
|
237
|
+
# the inline ProminentButton is the reliable target.)
|
|
238
|
+
- scrollUntilVisible:
|
|
239
|
+
element:
|
|
240
|
+
id: "profile-save"
|
|
241
|
+
direction: DOWN
|
|
242
|
+
- tapOn:
|
|
243
|
+
id: "profile-save"
|
|
244
|
+
|
|
245
|
+
# updateProfile hits Convex. SuccessText is an inner Text (no id surface), so
|
|
246
|
+
# prove the save landed structurally: the inline Save button only renders while
|
|
247
|
+
# hasChanges is true, so it disappears after a successful save.
|
|
248
|
+
- extendedWaitUntil:
|
|
249
|
+
notVisible:
|
|
250
|
+
id: "profile-save"
|
|
251
|
+
timeout: 20000
|
|
252
|
+
- assertVisible: ".*e2e tour.*"
|
|
253
|
+
- assertVisible: "Saved"
|
|
254
|
+
|
|
255
|
+
# Back to Settings.
|
|
256
|
+
- swipe:
|
|
257
|
+
start: 2%, 50%
|
|
258
|
+
end: 90%, 50%
|
|
259
|
+
- waitForAnimationToEnd:
|
|
260
|
+
timeout: 5000
|
|
261
|
+
- extendedWaitUntil:
|
|
262
|
+
visible:
|
|
263
|
+
id: "settings-screen"
|
|
264
|
+
timeout: 15000
|
|
265
|
+
|
|
266
|
+
# --- 6. Sessions ---------------------------------------------------------
|
|
267
|
+
# Open via the settings row (capsule, tap by text).
|
|
268
|
+
- assertVisible: "Sessions"
|
|
269
|
+
- tapOn: "Sessions"
|
|
270
|
+
- waitForAnimationToEnd:
|
|
271
|
+
timeout: 5000
|
|
272
|
+
|
|
273
|
+
# listSessions hits Convex; the list renders after the network round-trip.
|
|
274
|
+
- extendedWaitUntil:
|
|
275
|
+
visible:
|
|
276
|
+
id: "sessions-screen"
|
|
277
|
+
timeout: 20000
|
|
278
|
+
# The signed-in device row carries a "This device" badge (text surfaces).
|
|
279
|
+
- extendedWaitUntil:
|
|
280
|
+
visible: "This device"
|
|
281
|
+
timeout: 20000
|
|
282
|
+
|
|
283
|
+
# Back to Settings.
|
|
284
|
+
- swipe:
|
|
285
|
+
start: 2%, 50%
|
|
286
|
+
end: 90%, 50%
|
|
287
|
+
- waitForAnimationToEnd:
|
|
288
|
+
timeout: 5000
|
|
289
|
+
- extendedWaitUntil:
|
|
290
|
+
visible:
|
|
291
|
+
id: "settings-screen"
|
|
292
|
+
timeout: 15000
|
|
293
|
+
|
|
294
|
+
- takeScreenshot: app-tour-final
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Account soft-delete -> restore journey: delete the account from Settings, get
|
|
2
|
+
# kicked to sign-in, sign back in within the 30-day grace window, land on the
|
|
3
|
+
# restore screen, restore, and end up authed on home again.
|
|
4
|
+
#
|
|
5
|
+
# This deployment soft-deletes: `users.deleteAccount` stamps `deletedAt` instead
|
|
6
|
+
# of hard-deleting, then signs out (see src/hooks/use-delete-account.ts). On the
|
|
7
|
+
# next sign-in `getMe` returns the tombstoned user, so the `isAccountDeleted`
|
|
8
|
+
# gate in (app)/_layout.tsx swaps the whole authed tree for `restore-account`.
|
|
9
|
+
# Tapping Restore clears the tombstone and `router.replace("/")` drops us back on
|
|
10
|
+
# the tabs root. Because this flow ends with the account restored and signed in,
|
|
11
|
+
# it leaves the shared test account exactly as auth.yaml left it. The `zz-`
|
|
12
|
+
# prefix makes it sort LAST in alphabetical folder runs so nothing downstream
|
|
13
|
+
# inherits a half-deleted account.
|
|
14
|
+
#
|
|
15
|
+
# Face ID note: `deleteAccount` gates on LocalAuthentication.authenticateAsync
|
|
16
|
+
# before the mutation. The simulator must have a matched/enrolled biometric
|
|
17
|
+
# (Features > Face ID > Enrolled, then Matching Face after the prompt) or the
|
|
18
|
+
# delete silently no-ops (`if (!result.success) return`). On EAS the device
|
|
19
|
+
# profile handles this; locally, trigger "Matching Face" if the run stalls at
|
|
20
|
+
# the delete step.
|
|
21
|
+
#
|
|
22
|
+
# Same @expo/ui SwiftUI conventions as auth.yaml: `testID` maps to the native
|
|
23
|
+
# accessibilityIdentifier and surfaces to Maestro as `resource-id`, so id-based
|
|
24
|
+
# asserts/taps resolve. Driving notes carry over:
|
|
25
|
+
# - Inner SwiftUI Text testIDs do NOT surface (Host roots and real controls
|
|
26
|
+
# do), so assert screens by Host id and static copy by its text.
|
|
27
|
+
# - Capsule rows hold their label left with a Spacer, so tap them by label
|
|
28
|
+
# text. ProminentButton centers its content, so tap it by id.
|
|
29
|
+
# - The delete confirm is a native Alert action; tap it by its exact label
|
|
30
|
+
# "Delete Account" (capital A), distinct from the row's "Delete account".
|
|
31
|
+
# - Dismiss the keyboard with a downward swipe, never hideKeyboard.
|
|
32
|
+
# - SecureField + strong-password autofill swallows typed input, so reveal via
|
|
33
|
+
# the `-visibility` eye toggle (which auto-focuses) before typing.
|
|
34
|
+
# - waitForAnimationToEnd between navigations; extendedWaitUntil after any
|
|
35
|
+
# live Convex network call (sign-in, delete, restore).
|
|
36
|
+
#
|
|
37
|
+
# Env vars (MUST match the account auth.yaml created and signed into):
|
|
38
|
+
# MAESTRO_APP_ID bundle id (EAS injects from EXPO_PUBLIC_APP_BUNDLE_ID)
|
|
39
|
+
# MAESTRO_TEST_EMAIL the same email auth.yaml signed in with
|
|
40
|
+
# MAESTRO_TEST_PASSWORD the same password (at least 10 chars)
|
|
41
|
+
#
|
|
42
|
+
# Requires a signed-in session: this runs AFTER auth.yaml/tour.yaml leave the
|
|
43
|
+
# app authed, so it launches WITHOUT clearState. Runs LAST in folder order.
|
|
44
|
+
#
|
|
45
|
+
# Run locally (needs a JDK on PATH; run auth.yaml first to seed the session):
|
|
46
|
+
# MAESTRO_APP_ID=$(grep '^EXPO_PUBLIC_APP_BUNDLE_ID=' .env.local | cut -d= -f2) \
|
|
47
|
+
# MAESTRO_TEST_EMAIL="e2e+...@example.com" \
|
|
48
|
+
# MAESTRO_TEST_PASSWORD="maestro-test-pw" \
|
|
49
|
+
# maestro test .maestro/zz-delete-restore.yaml
|
|
50
|
+
# Run on EAS: via `.eas/workflows/e2e-tests.yml` (folder run, sorts last).
|
|
51
|
+
appId: ${MAESTRO_APP_ID}
|
|
52
|
+
---
|
|
53
|
+
# Resume the signed-in session left by auth.yaml. No clearState: this flow
|
|
54
|
+
# depends on the live session and the seeded account.
|
|
55
|
+
- launchApp
|
|
56
|
+
- waitForAnimationToEnd:
|
|
57
|
+
timeout: 15000
|
|
58
|
+
|
|
59
|
+
# Dev-client builds may show the one-time developer-menu intro sheet after a
|
|
60
|
+
# relaunch. Drag it down to dismiss. Release builds skip this block.
|
|
61
|
+
- runFlow:
|
|
62
|
+
when:
|
|
63
|
+
visible: "This is the developer menu.*"
|
|
64
|
+
commands:
|
|
65
|
+
- swipe:
|
|
66
|
+
start: 50%, 55%
|
|
67
|
+
end: 50%, 95%
|
|
68
|
+
- waitForAnimationToEnd:
|
|
69
|
+
timeout: 5000
|
|
70
|
+
|
|
71
|
+
# The intro sheet can appear late while Metro is still bundling, so check a
|
|
72
|
+
# second time after the bundle settles.
|
|
73
|
+
- waitForAnimationToEnd:
|
|
74
|
+
timeout: 10000
|
|
75
|
+
- runFlow:
|
|
76
|
+
when:
|
|
77
|
+
visible: "This is the developer menu.*"
|
|
78
|
+
commands:
|
|
79
|
+
- swipe:
|
|
80
|
+
start: 50%, 55%
|
|
81
|
+
end: 50%, 95%
|
|
82
|
+
- waitForAnimationToEnd:
|
|
83
|
+
timeout: 5000
|
|
84
|
+
|
|
85
|
+
# Authenticated entry: the tabs root mounts on home.
|
|
86
|
+
- extendedWaitUntil:
|
|
87
|
+
visible:
|
|
88
|
+
id: "home-screen"
|
|
89
|
+
timeout: 20000
|
|
90
|
+
|
|
91
|
+
# Into Settings. Native tab taps can be flaky, so assert-then-tap.
|
|
92
|
+
- assertVisible: "Settings"
|
|
93
|
+
- tapOn: "Settings"
|
|
94
|
+
- waitForAnimationToEnd:
|
|
95
|
+
timeout: 5000
|
|
96
|
+
- extendedWaitUntil:
|
|
97
|
+
visible:
|
|
98
|
+
id: "settings-screen"
|
|
99
|
+
timeout: 15000
|
|
100
|
+
|
|
101
|
+
# Delete the account. The row is a destructive capsule (label left, Spacer
|
|
102
|
+
# center), so tap by text.
|
|
103
|
+
- scrollUntilVisible:
|
|
104
|
+
element:
|
|
105
|
+
id: "settings-delete-account"
|
|
106
|
+
direction: DOWN
|
|
107
|
+
- tapOn: "Delete account"
|
|
108
|
+
- waitForAnimationToEnd:
|
|
109
|
+
timeout: 3000
|
|
110
|
+
|
|
111
|
+
# Confirm in the native Alert. The action label is "Delete Account" (capital A),
|
|
112
|
+
# distinct from the row's "Delete account". This triggers the Face ID gate (see
|
|
113
|
+
# header) then the soft-delete mutation and sign-out.
|
|
114
|
+
- assertVisible: "Delete Account"
|
|
115
|
+
- tapOn: "Delete Account"
|
|
116
|
+
|
|
117
|
+
# Soft-delete signs the user out, so the unauthenticated guard mounts the auth
|
|
118
|
+
# group and lands on sign-in. Generous timeout: live network call + Face ID.
|
|
119
|
+
- extendedWaitUntil:
|
|
120
|
+
visible:
|
|
121
|
+
id: "sign-in-screen"
|
|
122
|
+
timeout: 30000
|
|
123
|
+
|
|
124
|
+
# Sign back in WITHIN the grace window with the same credentials. Method toggle
|
|
125
|
+
# defaults to Email.
|
|
126
|
+
- tapOn:
|
|
127
|
+
id: "sign-in-email"
|
|
128
|
+
- inputText: ${MAESTRO_TEST_EMAIL}
|
|
129
|
+
- swipe:
|
|
130
|
+
start: 50%, 35%
|
|
131
|
+
end: 50%, 85%
|
|
132
|
+
# Strong-password autofill dodge: reveal via the eye toggle, which auto-focuses.
|
|
133
|
+
- tapOn:
|
|
134
|
+
id: "sign-in-email-password-visibility"
|
|
135
|
+
- waitForAnimationToEnd:
|
|
136
|
+
timeout: 3000
|
|
137
|
+
- tapOn:
|
|
138
|
+
id: "sign-in-email-password"
|
|
139
|
+
- inputText: ${MAESTRO_TEST_PASSWORD}
|
|
140
|
+
- swipe:
|
|
141
|
+
start: 50%, 35%
|
|
142
|
+
end: 50%, 85%
|
|
143
|
+
- scrollUntilVisible:
|
|
144
|
+
element:
|
|
145
|
+
id: "sign-in-submit"
|
|
146
|
+
direction: DOWN
|
|
147
|
+
- tapOn:
|
|
148
|
+
id: "sign-in-submit"
|
|
149
|
+
|
|
150
|
+
# Soft-deleted within the grace window: `getMe` returns the tombstoned user, so
|
|
151
|
+
# the isAccountDeleted gate routes to restore-account instead of the tabs.
|
|
152
|
+
# Generous timeout: live network call.
|
|
153
|
+
- extendedWaitUntil:
|
|
154
|
+
visible:
|
|
155
|
+
id: "restore-account-screen"
|
|
156
|
+
timeout: 30000
|
|
157
|
+
|
|
158
|
+
# Assert the grace-window copy by a stable text fragment. The full line reads
|
|
159
|
+
# "Your account is set to be permanently deleted on <date>. Restore now to keep
|
|
160
|
+
# your account and all of its data." Match the date-independent phrase.
|
|
161
|
+
- assertVisible: ".*permanently deleted on.*"
|
|
162
|
+
|
|
163
|
+
# Restore. The button is a ProminentButton (content centered), so tap by id.
|
|
164
|
+
- tapOn:
|
|
165
|
+
id: "restore-account-restore"
|
|
166
|
+
|
|
167
|
+
# Restore clears the tombstone and router.replace("/") drops us back on the tabs
|
|
168
|
+
# root. Generous timeout: live network call.
|
|
169
|
+
- extendedWaitUntil:
|
|
170
|
+
visible:
|
|
171
|
+
id: "home-screen"
|
|
172
|
+
timeout: 30000
|
|
173
|
+
|
|
174
|
+
- takeScreenshot: delete-restore-final
|
|
@@ -76,8 +76,9 @@ rebrand` fills them in. App Review will reject builds with placeholder
|
|
|
76
76
|
- **Pre-approved commands:** `.claude/settings.json` allows read-only
|
|
77
77
|
`git`/`expo`/`eas`/`convex`/`vexpo` calls + the project's `npm run`
|
|
78
78
|
scripts (`typecheck`, `lint`, `test`, `format`, `dev`, `fp`) without
|
|
79
|
-
per-step permission prompts.
|
|
80
|
-
`
|
|
79
|
+
per-step permission prompts. The file carries only an allowlist, no
|
|
80
|
+
denylist, so anything not on it (`git push`, `git reset`, `npm install`,
|
|
81
|
+
`expo deploy`, and the rest) prompts by default.
|
|
81
82
|
- **EAS Convex bootstrap:** `eas integrations:convex:connect` is the
|
|
82
83
|
upstream SDK 56 path for provisioning a Convex backend, writing
|
|
83
84
|
`CONVEX_DEPLOY_KEY` + `EXPO_PUBLIC_CONVEX_URL`, and registering the env
|
|
@@ -91,7 +91,7 @@ Calm, monochrome, native. The vexpo system pairs a shadcn neutral palette (prese
|
|
|
91
91
|
|
|
92
92
|
The brand commits to Geist Variable as the single typeface for both UI and marketing assets. Color carries no brand meaning. `destructive` is the only chromatic token, reserved for irreversible actions and validation errors. Brand expression flows through type weight, spacing, rounding, and the iOS material system.
|
|
93
93
|
|
|
94
|
-
Ground truth lives in `constants/theme.ts` (color palette as `DynamicColorIOS`), `constants/layout.ts` (spacing, font sizes, line heights, fonts),
|
|
94
|
+
Ground truth lives in `constants/theme.ts` (color palette as `DynamicColorIOS`), `constants/layout.ts` (spacing, font sizes, line heights, fonts), `constants/ui.ts` (opacity, shadows, durations, sizes), and `components/ui/material.tsx` (the `BLUR_INTENSITY`, `BLUR_TINT`, and `GLASS_STYLE` maps that drive each material variant). The tokens here are the public contract for agents and contributors. The constants files are the implementation.
|
|
95
95
|
|
|
96
96
|
## Colors
|
|
97
97
|
|
|
@@ -214,7 +214,7 @@ Depth is signaled by surface tint and the iOS material system, not by drop shado
|
|
|
214
214
|
|
|
215
215
|
Materials apply a vibrancy effect that lets background content show through with controlled blur and saturation. Apple's HIG calls this the "navigation layer" and explicitly reserves it for chrome. Don't put materials on form sections or content cards.
|
|
216
216
|
|
|
217
|
-
The blur intensity values are calibrated. Don't lower them. The 35% tint overlay on the `BlurView` fallback path is also calibrated, the smallest tint that still reads as the requested color while preserving the blur.
|
|
217
|
+
The blur intensity values (`ultraThin` 30, `thin` 50, `regular` 70, `thick` 90, `chrome` 100) live in the `BLUR_INTENSITY` map in `components/ui/material.tsx`. They are calibrated. Don't lower them. The 35% tint overlay on the `BlurView` fallback path is also calibrated, the smallest tint that still reads as the requested color while preserving the blur.
|
|
218
218
|
|
|
219
219
|
## Shapes / Radius
|
|
220
220
|
|
|
@@ -254,53 +254,53 @@ The system uses `@expo/ui/swift-ui` primitives exclusively for native rendering.
|
|
|
254
254
|
|
|
255
255
|
### Native primitives (use directly)
|
|
256
256
|
|
|
257
|
-
| Primitive | What it is | Where it lands
|
|
258
|
-
| :-------------------------- | :---------------------------------------------------------------------------- |
|
|
259
|
-
| `Host` | SwiftUI host view, top-level wrapper for any screen using `@expo/ui/swift-ui` | Every screen root
|
|
260
|
-
| `VStack` / `HStack` | SwiftUI stacks with spacing + alignment | Layout primitives
|
|
261
|
-
| `Form` / `Section` | Native iOS Form with grouped sections |
|
|
262
|
-
| `Picker` | Segmented or wheel picker (use `pickerStyle("segmented")` for inline) | Theme mode, motion preference
|
|
263
|
-
| `Toggle` | Native iOS toggle | Boolean preferences
|
|
264
|
-
| `Button` | Native button with role variants (default, cancel, destructive) | All actions
|
|
265
|
-
| `TextField` / `SecureField` | Native text input | Forms
|
|
266
|
-
| `Text` | SwiftUI text with modifier composition | All typography
|
|
267
|
-
| `Image` | SF Symbol renderer (`systemName="..."`) | All inline icons
|
|
268
|
-
| `ConfirmationDialog` | Native iOS action sheet | Sign-out, delete account, photo picker
|
|
269
|
-
| `BottomSheet` | Native sheet with detents |
|
|
270
|
-
| `Spacer` | Flexible space | Layout
|
|
271
|
-
| `ProgressView` | Spinner or determinate progress | Loading states
|
|
272
|
-
| `ContentUnavailableView` | Empty state with SF Symbol + title + description | Empty home, no results
|
|
257
|
+
| Primitive | What it is | Where it lands |
|
|
258
|
+
| :-------------------------- | :---------------------------------------------------------------------------- | :----------------------------------------------------------------------------- |
|
|
259
|
+
| `Host` | SwiftUI host view, top-level wrapper for any screen using `@expo/ui/swift-ui` | Every screen root |
|
|
260
|
+
| `VStack` / `HStack` | SwiftUI stacks with spacing + alignment | Layout primitives |
|
|
261
|
+
| `Form` / `Section` | Native iOS Form with grouped sections | Available but unused. Screens hand-build capsule rows with `VStack` instead |
|
|
262
|
+
| `Picker` | Segmented or wheel picker (use `pickerStyle("segmented")` for inline) | Theme mode, motion preference |
|
|
263
|
+
| `Toggle` | Native iOS toggle | Boolean preferences |
|
|
264
|
+
| `Button` | Native button with role variants (default, cancel, destructive) | All actions |
|
|
265
|
+
| `TextField` / `SecureField` | Native text input | Forms |
|
|
266
|
+
| `Text` | SwiftUI text with modifier composition | All typography |
|
|
267
|
+
| `Image` | SF Symbol renderer (`systemName="..."`) | All inline icons |
|
|
268
|
+
| `ConfirmationDialog` | Native iOS action sheet | Sign-out, delete account, photo picker |
|
|
269
|
+
| `BottomSheet` | Native sheet with detents | Available but unused. Modal screens use `presentation: "modal"` routes instead |
|
|
270
|
+
| `Spacer` | Flexible space | Layout |
|
|
271
|
+
| `ProgressView` | Spinner or determinate progress | Loading states |
|
|
272
|
+
| `ContentUnavailableView` | Empty state with SF Symbol + title + description | Empty home, no results |
|
|
273
273
|
|
|
274
274
|
Text inputs bind `text` to a `useNativeState("")` and mask synchronously: a `"worklet"` `onTextChange` rewrites the field on the same frame the keystroke lands (digits-only OTP, lowercase usernames), so the raw character never paints. Reusable masks live in `lib/masks.ts`.
|
|
275
275
|
|
|
276
276
|
### Custom composition (in `components/ui/`)
|
|
277
277
|
|
|
278
|
-
| Component | Purpose | Notes
|
|
279
|
-
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
280
|
-
| `Material` | Translucent surface with iOS 26+ Liquid Glass / iOS 16.4-25 BlurView fallback | Reserve for navigation chrome only
|
|
281
|
-
| `OfflineBanner` | Top-of-screen notification banner using `Material` chrome variant | Shows when network unavailable
|
|
282
|
-
| `LoadingScreen` | Brand-icon + spinner, themed by appearance | Suspense fallback
|
|
283
|
-
| `ErrorBoundary` | Top-level crash boundary with brand recovery UI | Wraps each route segment
|
|
284
|
-
| `
|
|
285
|
-
| `SkeletonProfile` / `SkeletonSessions` | Static loading placeholders. No animation, so nothing to suppress under Reduce Motion. Filler bars carry an empty `accessibilityLabel` so VoiceOver skips the placeholder shapes. | Profile loading, sessions loading
|
|
286
|
-
| `StatusText` | `ErrorText` + `SuccessText` with accessibility announcements | Form feedback
|
|
278
|
+
| Component | Purpose | Notes |
|
|
279
|
+
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ |
|
|
280
|
+
| `Material` | Translucent surface with iOS 26+ Liquid Glass / iOS 16.4-25 BlurView fallback | Reserve for navigation chrome only |
|
|
281
|
+
| `OfflineBanner` | Top-of-screen notification banner using `Material` chrome variant | Shows when network unavailable |
|
|
282
|
+
| `LoadingScreen` | Brand-icon + spinner, themed by appearance | Suspense fallback |
|
|
283
|
+
| `ErrorBoundary` | Top-level crash boundary with brand recovery UI | Wraps each route segment |
|
|
284
|
+
| `formatError` | Maps a Convex error to user-readable copy (`components/ui/convex-error.tsx`) | Used by `profile/index.tsx` and `use-delete-account.ts` |
|
|
285
|
+
| `SkeletonProfile` / `SkeletonSessions` | Static loading placeholders. No animation, so nothing to suppress under Reduce Motion. Filler bars carry an empty `accessibilityLabel` so VoiceOver skips the placeholder shapes. | Profile loading, sessions loading |
|
|
286
|
+
| `StatusText` | `ErrorText` + `SuccessText` with accessibility announcements | Form feedback |
|
|
287
287
|
|
|
288
288
|
### Custom hooks (in `hooks/`)
|
|
289
289
|
|
|
290
|
-
| Hook | Purpose
|
|
291
|
-
| :-------------------------------- |
|
|
292
|
-
| `useThemeMode` / `useColorScheme` | App-level light/dark/system override on top of OS appearance
|
|
293
|
-
| `useColors` | Returns the `Colors` palette (currently constant, kept as a hook for future per-theme variants)
|
|
294
|
-
| `useThemedAsset` | Picks light or dark asset based on active appearance
|
|
295
|
-
| `useDynamicFont` |
|
|
296
|
-
| `useReducedMotion` | Combines OS Reduce Motion + in-app override. Drives animation duration / disable
|
|
297
|
-
| `useNetwork` | Online / offline state for `OfflineBanner`
|
|
298
|
-
| `useNotifications` | Push token registration + foreground handler
|
|
299
|
-
| `useDeepLinkHandler` |
|
|
300
|
-
| `
|
|
301
|
-
| `useOnboarding` | First-launch welcome flow gate
|
|
302
|
-
| `useDebounce` | Standard debounce
|
|
303
|
-
| `useNavigationTracking` | Analytics / route logging
|
|
290
|
+
| Hook | Purpose |
|
|
291
|
+
| :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------- |
|
|
292
|
+
| `useThemeMode` / `useColorScheme` | App-level light/dark/system override on top of OS appearance |
|
|
293
|
+
| `useColors` | Returns the `Colors` palette (currently constant, kept as a hook for future per-theme variants) |
|
|
294
|
+
| `useThemedAsset` | Picks light or dark asset based on active appearance |
|
|
295
|
+
| `useDynamicFont` | Maps the declared font size to a SwiftUI `textStyle` passed to `@expo/ui` `font()`, so iOS Dynamic Type scales it natively |
|
|
296
|
+
| `useReducedMotion` | Combines OS Reduce Motion + in-app override. Drives animation duration / disable |
|
|
297
|
+
| `useNetwork` | Online / offline state for `OfflineBanner` |
|
|
298
|
+
| `useNotifications` | Push token registration + foreground handler |
|
|
299
|
+
| `useDeepLinkHandler` | Resumes a deep link the auth guard deferred until sign-in. Live applinks route through `+native-intent.tsx` |
|
|
300
|
+
| `useAppUpdates` | EAS Update check + apply with branded UI |
|
|
301
|
+
| `useOnboarding` | First-launch welcome flow gate |
|
|
302
|
+
| `useDebounce` | Standard debounce |
|
|
303
|
+
| `useNavigationTracking` | Analytics / route logging |
|
|
304
304
|
|
|
305
305
|
## Do's and Don'ts
|
|
306
306
|
|