@ramonclaudio/create-vexpo 0.1.3 → 0.1.4

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 (35) hide show
  1. package/README.md +25 -15
  2. package/dist/index.js +44 -14
  3. package/dist/templates/default/.eas/workflows/e2e-tests.yml +14 -1
  4. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +3 -3
  5. package/dist/templates/default/.maestro/auth.yaml +229 -0
  6. package/dist/templates/default/.maestro/launch.yaml +5 -5
  7. package/dist/templates/default/.maestro/tour.yaml +294 -0
  8. package/dist/templates/default/.maestro/zz-delete-restore.yaml +174 -0
  9. package/dist/templates/default/AGENTS.md +3 -2
  10. package/dist/templates/default/DESIGN.md +41 -41
  11. package/dist/templates/default/README.md +38 -41
  12. package/dist/templates/default/SETUP.md +34 -19
  13. package/dist/templates/default/_easignore +0 -1
  14. package/dist/templates/default/_env.example +15 -10
  15. package/dist/templates/default/app.config.ts +5 -5
  16. package/dist/templates/default/convex/pushTokens.ts +1 -26
  17. package/dist/templates/default/convex/rateLimit.ts +1 -21
  18. package/dist/templates/default/convex/users.ts +1 -49
  19. package/dist/templates/default/convex/validators.ts +0 -10
  20. package/dist/templates/default/package.json +1 -1
  21. package/dist/templates/default/scripts/README.md +24 -8
  22. package/dist/templates/default/scripts/clean.ts +3 -3
  23. package/dist/templates/default/scripts/gen-update-cert.mjs +3 -1
  24. package/dist/templates/default/src/app/(app)/_layout.tsx +15 -1
  25. package/dist/templates/default/src/app/(app)/auth/forgot-password.tsx +3 -0
  26. package/dist/templates/default/src/app/(app)/auth/reset-password.tsx +3 -0
  27. package/dist/templates/default/src/app/(app)/auth/sign-up.tsx +3 -1
  28. package/dist/templates/default/src/app/(app)/privacy.tsx +3 -2
  29. package/dist/templates/default/src/app/(app)/restore-account.tsx +2 -2
  30. package/dist/templates/default/src/app/(app)/sessions.tsx +15 -5
  31. package/dist/templates/default/src/components/ui/convex-error.tsx +0 -7
  32. package/dist/templates/default/src/constants/ui.ts +0 -11
  33. package/dist/templates/default/src/lib/dev-menu.ts +11 -2
  34. package/dist/templates/default/src/lib/preferences.ts +9 -0
  35. package/package.json +3 -2
package/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  [![npm](https://img.shields.io/npm/v/@ramonclaudio/create-vexpo)](https://www.npmjs.com/package/@ramonclaudio/create-vexpo)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Scaffold a new [vexpo](https://github.com/ramonclaudio/vexpo) project: Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Real auth (email + password, email OTP, Apple Sign In), APNs push, OTA updates, App Store submission. Strict TypeScript, native SwiftUI via `@expo/ui/swift-ui`, no NativeWind.
6
+ Scaffold a new [vexpo](https://github.com/ramonclaudio/vexpo) project: an Expo SDK 56 iOS app with Convex, Better Auth, and Resend wired in for backend, auth, and email. Push, OTA updates, and App Store submission all run through EAS.
7
+
8
+ This is the opinionated stack I reach for on every new app, Expo and Convex and Better Auth sitting on top of EAS, and I wanted anyone to be able to start from it without a day of wiring. The CLI walks you through creating a Convex account or linking one you already have, so you go from empty folder to a running app without leaving the terminal.
7
9
 
8
10
  ## Usage
9
11
 
@@ -20,31 +22,39 @@ cd my-app
20
22
 
21
23
  npx vexpo lite # 60 seconds: Convex + Better Auth, simulator-ready
22
24
  npx vexpo lite --new # same + Convex signup walkthrough for first-time users
23
- npx vexpo full # full provisioning: TestFlight-ready (Convex, Better Auth, Resend, Apple Sign In, EAS, rebrand)
24
- npx vexpo full --new # same + Apple/Convex/Expo/Resend signup walkthrough
25
+ npx vexpo full # full provisioning: TestFlight-ready
26
+ npx vexpo full --new # same + Apple, Convex, Expo, and Resend signup walkthrough
25
27
  ```
26
28
 
27
- `npx vexpo lite` is the dev-mode shortcut. No Apple Developer account, no domain, no EAS, no Resend. Boots in the iOS Simulator in ~60 seconds. Add `--new` if you don't have a Convex account yet.
29
+ `npx vexpo lite` is the dev-mode shortcut. No Apple Developer account, no domain, no EAS, no Resend. Boots in the iOS Simulator in about 60 seconds. Add `--new` if you don't have a Convex account yet.
30
+
31
+ `npx vexpo full` validates and provisions everything in order: Convex, Better Auth, Resend, Apple Sign In, EAS, and a rebrand. About 30 minutes hands-on plus Apple-side wait times. It prints the `eas build` command at the end for you to run when ready.
28
32
 
29
- `npx vexpo full` walks Apple Developer / Expo / Convex / Resend signups (with `--new`), validates each, provisions everything in order. ~30 minutes hands-on plus Apple-side wait times. Prints the canonical `eas build` command at the end. You run it when ready.
33
+ ## Pre-reqs
34
+
35
+ - macOS with Xcode, to build and run the app in the iOS Simulator.
36
+ - Bun, or Node 20+.
37
+ - An Apple Developer membership, only when you ship to TestFlight or the App Store. Not needed for local dev with `npx vexpo lite`.
30
38
 
31
39
  ## Options
32
40
 
33
- | Flag | Behavior |
34
- | --------------- | --------------------------------------------------------------------------------------------- |
35
- | `[directory]` | Project directory name (positional). Defaults to `my-vexpo-app` with `-y`, otherwise prompts. |
36
- | `--no-install` | Skip running `<pm> install` after copying the template. |
37
- | `--no-git` | Skip `git init` after install. |
38
- | `--no-setup` | Skip the post-install `npx vexpo lite` / `npx vexpo full` prompt. |
39
- | `-y, --yes` | Accept defaults, skip prompts. |
40
- | `-v, --version` | Print version, exit. |
41
+ | Flag | Behavior |
42
+ | --------------- | -------------------------------------------------------------------------- |
43
+ | `[directory]` | Project directory name (positional). Defaults to `my-vexpo-app` with `-y`. |
44
+ | `--no-install` | Skip installing dependencies after copying the template. |
45
+ | `--no-git` | Skip `git init` after install. |
46
+ | `--no-setup` | Skip the printed next-steps block after install. |
47
+ | `-y, --yes` | Accept defaults, skip prompts. |
48
+ | `-v, --version` | Print version, exit. |
41
49
 
42
50
  ## What gets scaffolded
43
51
 
44
- The CLI copies `templates/default/` from the published tarball, restores npm-stripped dotfiles (`.gitignore`, `.env.example`, `.npmrc`, etc.), rewrites `package.json` (project name, version, drops monorepo metadata), installs dependencies via the detected package manager (`npm`, `bun`, `pnpm`, or `yarn`, sniffed from `npm_config_user_agent`; defaults to `npm`), and initializes a fresh git repo with `feat: initial commit`. No lockfile ships in the tarball: the first install resolves the template's ranges fresh (including the latest in-range `vexpo` CLI) and the generated lockfile lands in the initial commit.
52
+ The CLI copies `templates/default/`, restores the dotfiles npm strips from tarballs (`.gitignore`, `.env.example`, `.npmrc`, others), and rewrites `package.json` for the new project. It installs with the package manager it detects from `npm_config_user_agent` (`npm`, `bun`, `pnpm`, or `yarn`, defaulting to `npm`). Then it initializes a git repo with `feat: initial commit`.
45
53
 
46
- After that, the project is standalone. The operational CLI (`vexpo`) is installed as a devDependency, so `npx vexpo <subcommand>` resolves to the local pinned version. Setup commands aren't in `package.json`. They're one-shot CLI invocations, not runtime scripts.
54
+ No lockfile ships in the tarball. The first install resolves the template's ranges fresh, including the latest in-range `vexpo` CLI, and the generated lockfile lands in the initial commit. The `vexpo` CLI installs as a devDependency, so `npx vexpo <subcommand>` resolves to the local pinned version.
47
55
 
48
56
  ## Repo
49
57
 
50
58
  [github.com/ramonclaudio/vexpo](https://github.com/ramonclaudio/vexpo)
59
+
60
+ Development happens in the monorepo. See [CONTRIBUTING.md](https://github.com/ramonclaudio/vexpo/blob/main/CONTRIBUTING.md) on GitHub.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import prompts from 'prompts';
11
11
 
12
12
  // package.json
13
13
  var package_default = {
14
- version: "0.1.3"};
14
+ version: "0.1.4"};
15
15
 
16
16
  // src/index.ts
17
17
  var here = dirname(fileURLToPath(import.meta.url));
@@ -19,7 +19,7 @@ var TEMPLATE_DIR = join(here, "templates", "default");
19
19
  async function main() {
20
20
  const program = new Command().name("create-vexpo").description(
21
21
  "Scaffold a new vexpo project. Expo SDK 56 + Convex + Better Auth + Resend, wired for iOS."
22
- ).argument("[directory]", "project directory name").option("--no-install", "skip installing dependencies").option("--no-git", "skip git init").option("--no-setup", "skip the `npx vexpo lite` / `npx vexpo full` prompt after install").option("-y, --yes", "accept defaults, skip prompts").version(package_default.version, "-v, --version").parse();
22
+ ).argument("[directory]", "project directory name").option("--no-install", "skip installing dependencies").option("--no-git", "skip git init").option("--no-setup", "skip the printed next-steps block after install").option("-y, --yes", "accept defaults, skip prompts").version(package_default.version, "-v, --version").parse();
23
23
  const flags = program.opts();
24
24
  const argDir = program.args[0];
25
25
  intro();
@@ -43,30 +43,50 @@ Target ${target} already exists. Pick a different name.`));
43
43
  copySpin.fail("Template copy failed");
44
44
  throw err;
45
45
  }
46
+ let depsReady = !flags.install;
46
47
  if (flags.install) {
47
48
  const installSpin = ora(`Installing dependencies with ${kleur.cyan(pm)}`).start();
48
49
  try {
49
- await execa(pm, ["install"], { cwd: target, stdio: "ignore" });
50
+ await execa(pm, ["install"], { cwd: target, stdout: "ignore" });
50
51
  installSpin.succeed(`Installed with ${pm}`);
51
- } catch {
52
- installSpin.warn(`Install skipped. Run ${kleur.cyan(`${pm} install`)} manually.`);
52
+ depsReady = true;
53
+ } catch (err) {
54
+ installSpin.fail(`Install failed. Run ${kleur.cyan(`${pm} install`)} manually.`);
55
+ const stderr = installFailureStderr(err);
56
+ if (stderr) console.error(kleur.gray(tail(stderr)));
53
57
  }
54
58
  }
55
59
  if (flags.git) {
56
60
  const gitSpin = ora("Initializing git").start();
57
61
  try {
58
62
  await execa("git", ["init", "--initial-branch=main"], { cwd: target, stdio: "ignore" });
59
- await execa("git", ["add", "-A"], { cwd: target, stdio: "ignore" });
60
- await execa("git", ["commit", "-m", "feat: initial commit", "--no-gpg-sign"], {
61
- cwd: target,
62
- stdio: "ignore"
63
- });
64
- gitSpin.succeed("Git repo initialized");
63
+ if (depsReady) {
64
+ await execa("git", ["add", "-A"], { cwd: target, stdio: "ignore" });
65
+ const identity = await execa("git", ["config", "user.email"], {
66
+ cwd: target,
67
+ reject: false
68
+ });
69
+ if (identity.exitCode !== 0 || !identity.stdout.trim()) {
70
+ gitSpin.warn("Git repo initialized, commit skipped (no git identity)");
71
+ console.error(
72
+ kleur.gray(" Set git config user.name and user.email, then commit yourself.")
73
+ );
74
+ } else {
75
+ await execa("git", ["commit", "-m", "feat: initial commit", "--no-gpg-sign"], {
76
+ cwd: target,
77
+ stdio: "ignore"
78
+ });
79
+ gitSpin.succeed("Git repo initialized");
80
+ }
81
+ } else {
82
+ gitSpin.warn("Git repo initialized, commit skipped (install failed)");
83
+ console.error(kleur.gray(` Commit yourself after ${pm} install lands.`));
84
+ }
65
85
  } catch {
66
86
  gitSpin.warn("Git init skipped");
67
87
  }
68
88
  }
69
- if (flags.setup) nextSteps(target, flags, pm);
89
+ if (flags.setup) nextSteps(target, pm, depsReady);
70
90
  }
71
91
  var NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
72
92
  var NAME_HINT = "lowercase letters, numbers, dashes; must start alphanumeric";
@@ -157,12 +177,22 @@ function intro() {
157
177
  console.log();
158
178
  console.log(kleur.bold().cyan("create-vexpo") + kleur.gray(` v${package_default.version}`));
159
179
  }
160
- function nextSteps(target, flags, pm) {
180
+ function installFailureStderr(err) {
181
+ if (err && typeof err === "object" && "stderr" in err) {
182
+ const stderr = err.stderr;
183
+ if (typeof stderr === "string") return stderr.trim();
184
+ }
185
+ return "";
186
+ }
187
+ function tail(text, n) {
188
+ return text.split("\n").slice(-20).join("\n");
189
+ }
190
+ function nextSteps(target, pm, depsReady) {
161
191
  const cdPath = relative(process.cwd(), target) || ".";
162
192
  console.log();
163
193
  console.log(kleur.bold("Next steps:"));
164
194
  console.log(kleur.gray(" cd ") + kleur.cyan(cdPath));
165
- if (!flags.install) console.log(kleur.gray(` ${pm} install`));
195
+ if (!depsReady) console.log(kleur.gray(` ${pm} install`));
166
196
  console.log(
167
197
  kleur.gray(
168
198
  ` npx vexpo lite ${kleur.dim("# dev mode: Convex + Better Auth, 60s to simulator")}`
@@ -11,6 +11,9 @@ name: E2E tests
11
11
  # `MAESTRO_APP_ID` resolves to the project's bundle id from the EAS development
12
12
  # environment (populated by `vexpo full` or `eas env:create`). The flow
13
13
  # yaml at .maestro/launch.yaml reads it as `appId: ${MAESTRO_APP_ID}`.
14
+ #
15
+ # The auth flow (.maestro/auth.yaml) signs a fresh account up against the
16
+ # dev Convex deployment, so `test_creds` mints a unique email per run.
14
17
 
15
18
  # Auto-run on PRs is OFF by default to conserve EAS build credits (each run
16
19
  # builds an iOS Simulator binary). Trigger manually, or restore the
@@ -30,13 +33,23 @@ jobs:
30
33
  platform: ios
31
34
  profile: development:simulator
32
35
 
36
+ test_creds:
37
+ name: Mint unique test credentials
38
+ outputs:
39
+ email: ${{ steps.creds.outputs.email }}
40
+ steps:
41
+ - id: creds
42
+ run: set-output email "maestro-e2e-$(date +%s)@example.com"
43
+
33
44
  maestro:
34
45
  name: Run Maestro flows
35
- needs: [build_ios_simulator]
46
+ needs: [build_ios_simulator, test_creds]
36
47
  type: maestro
37
48
  environment: development
38
49
  env:
39
50
  MAESTRO_APP_ID: ${{ env.EXPO_PUBLIC_APP_BUNDLE_ID }}
51
+ MAESTRO_TEST_EMAIL: ${{ needs.test_creds.outputs.email }}
52
+ MAESTRO_TEST_PASSWORD: maestro-e2e-password
40
53
  params:
41
54
  build_id: ${{ needs.build_ios_simulator.outputs.build_id }}
42
55
  flow_path: [".maestro"]
@@ -1,8 +1,8 @@
1
1
  name: Rotate Apple Sign In JWT
2
2
 
3
- # Apple caps client_secret JWTs at 180 days. We re-sign every 90 days so
4
- # we always have plenty of headroom and never get caught with an expired
5
- # token. Replaces the GitHub Actions equivalent. runs on EAS infrastructure
3
+ # Apple caps client_secret JWTs at 180 days. We re-sign quarterly (the 1st of
4
+ # Jan, Apr, Jul, Oct) so we always have plenty of headroom and never get caught
5
+ # with an expired token. Replaces the GitHub Actions equivalent. runs on EAS infrastructure
6
6
  # and reads secrets from EAS env vars (production, secret visibility) rather
7
7
  # than GitHub repo secrets.
8
8
  #
@@ -0,0 +1,229 @@
1
+ # Auth journey: sign up (auto-verify lite path) -> land authed -> sign out -> sign back in.
2
+ #
3
+ # This deployment has REQUIRE_EMAIL_VERIFICATION unset, so `getEnabledProviders`
4
+ # returns emailFeatures: false. Sign-up creates a verified account and Better
5
+ # Auth's autoSignIn returns a session in the same call, so there is no OTP step:
6
+ # the user lands signed in. A fresh signed-in user trips the first-launch gate
7
+ # in (app)/_layout.tsx and gets redirected to /welcome; this flow completes that
8
+ # onboarding before asserting the tabs.
9
+ #
10
+ # Same @expo/ui SwiftUI conventions as launch.yaml: `testID` maps to the native
11
+ # accessibilityIdentifier and surfaces to Maestro as `resource-id`, so id-based
12
+ # asserts/taps resolve. Driving notes carry over:
13
+ # - ProminentButton submits hold content in the center, so tap them by `id`.
14
+ # - Segmented Picker rows put their label text in the middle, so tap the
15
+ # sign-in/sign-up toggle by its label text (`tapOn: "Sign up"`), not by id.
16
+ # - The sign-out row is a full-width Button with its label in the center, so
17
+ # tap it by `id`.
18
+ # - waitForAnimationToEnd between navigations; assert-then-tap for tab bars.
19
+ # - Use extendedWaitUntil with a generous timeout after sign-up/sign-in: those
20
+ # hit the live Convex deployment over the network.
21
+ #
22
+ # Env vars:
23
+ # MAESTRO_APP_ID bundle id (EAS injects from EXPO_PUBLIC_APP_BUNDLE_ID)
24
+ # MAESTRO_TEST_EMAIL unique email per run (a fresh account is created)
25
+ # MAESTRO_TEST_PASSWORD password, at least 10 chars (signUpSchema)
26
+ #
27
+ # Local re-runs: Better Auth keeps the session in the keychain, which
28
+ # clearState does NOT wipe. Reset it first or the relaunch comes up signed in:
29
+ # xcrun simctl keychain booted reset
30
+ #
31
+ # Run locally (needs a JDK on PATH):
32
+ # MAESTRO_APP_ID=$(grep '^EXPO_PUBLIC_APP_BUNDLE_ID=' .env.local | cut -d= -f2) \
33
+ # MAESTRO_TEST_EMAIL="e2e+$(date +%s)@example.com" \
34
+ # MAESTRO_TEST_PASSWORD="maestro-test-pw" \
35
+ # maestro test .maestro/auth.yaml
36
+ # Run on EAS: via `.eas/workflows/e2e-tests.yml` (workflow_dispatch only).
37
+ appId: ${MAESTRO_APP_ID}
38
+ ---
39
+ - launchApp:
40
+ clearState: true
41
+ - waitForAnimationToEnd:
42
+ timeout: 15000
43
+
44
+ # Dev-client builds: clearState also wipes the dev client's last-opened-bundle
45
+ # memory, so the launcher shows the server picker instead of the app. Tap the
46
+ # running Metro server when the picker appears. Release builds skip this block.
47
+ - runFlow:
48
+ when:
49
+ visible: "DEVELOPMENT SERVERS"
50
+ commands:
51
+ - tapOn:
52
+ text: "http://.*:8081"
53
+ - waitForAnimationToEnd:
54
+ timeout: 15000
55
+
56
+ # Dev-client builds also show a one-time "developer menu" intro sheet after
57
+ # clearState. Drag it down to dismiss. Release builds skip this block too.
58
+ - runFlow:
59
+ when:
60
+ visible: "This is the developer menu.*"
61
+ commands:
62
+ - swipe:
63
+ start: 50%, 55%
64
+ end: 50%, 95%
65
+ - waitForAnimationToEnd:
66
+ timeout: 5000
67
+
68
+ # The intro sheet can appear late while Metro is still bundling, so check a
69
+ # second time after the bundle settles.
70
+ - waitForAnimationToEnd:
71
+ timeout: 10000
72
+ - runFlow:
73
+ when:
74
+ visible: "This is the developer menu.*"
75
+ commands:
76
+ - swipe:
77
+ start: 50%, 55%
78
+ end: 50%, 95%
79
+ - waitForAnimationToEnd:
80
+ timeout: 5000
81
+
82
+ # Signed out: the auth guard mounts the `auth` group, whose stack starts at
83
+ # sign-in. Wait for the screen root before touching anything.
84
+ - extendedWaitUntil:
85
+ visible:
86
+ id: "sign-in-screen"
87
+ timeout: 20000
88
+ # Inner SwiftUI Text testIDs do not surface to Maestro (Host roots and real
89
+ # controls do), so static copy is asserted by its text.
90
+ - assertVisible: "Enter your credentials.*"
91
+
92
+ # Cross to sign-up via the segmented toggle. Tap the segment by label text, not
93
+ # id (the Picker row holds its label in the middle).
94
+ - tapOn: "Sign up"
95
+ - waitForAnimationToEnd:
96
+ timeout: 5000
97
+ - extendedWaitUntil:
98
+ visible:
99
+ id: "sign-up-screen"
100
+ timeout: 15000
101
+ - assertVisible: "Create your account"
102
+
103
+ # Fill name, email, password. Username is optional and skipped. Tapping the
104
+ # field by id focuses it, then `inputText` types into the focused field.
105
+ # The keyboard covers the lower fields once it is up. Both auth screens use
106
+ # scrollDismissesKeyboard("interactively"), so a downward drag dismisses the
107
+ # keyboard between fields (hideKeyboard is flaky on iOS, don't use it).
108
+ - tapOn:
109
+ id: "sign-up-name"
110
+ - inputText: "Maestro E2E"
111
+ - swipe:
112
+ start: 50%, 35%
113
+ end: 50%, 85%
114
+ - scrollUntilVisible:
115
+ element:
116
+ id: "sign-up-email"
117
+ direction: DOWN
118
+ - tapOn:
119
+ id: "sign-up-email"
120
+ - inputText: ${MAESTRO_TEST_EMAIL}
121
+ - swipe:
122
+ start: 50%, 35%
123
+ end: 50%, 85%
124
+ - scrollUntilVisible:
125
+ element:
126
+ id: "sign-up-password"
127
+ direction: DOWN
128
+ # iOS Automatic Strong Password hijacks SecureFields with newPassword content
129
+ # type in the simulator and swallows typed input. The eye toggle swaps to a
130
+ # plain TextField (and auto-focuses it), which types fine.
131
+ - tapOn:
132
+ id: "sign-up-password-visibility"
133
+ - waitForAnimationToEnd:
134
+ timeout: 3000
135
+ - tapOn:
136
+ id: "sign-up-password"
137
+ - inputText: ${MAESTRO_TEST_PASSWORD}
138
+ - swipe:
139
+ start: 50%, 35%
140
+ end: 50%, 85%
141
+
142
+ # Submit. ProminentButton center holds content, so tap by id.
143
+ - scrollUntilVisible:
144
+ element:
145
+ id: "sign-up-submit"
146
+ direction: DOWN
147
+ - tapOn:
148
+ id: "sign-up-submit"
149
+
150
+ # Auto-verify lite path: account is created verified and the session returns in
151
+ # the same call, so the app signs in. A fresh authed user is redirected to the
152
+ # first-launch welcome onboarding. Generous timeout: this is a live network call.
153
+ - extendedWaitUntil:
154
+ visible:
155
+ id: "welcome-screen"
156
+ timeout: 30000
157
+
158
+ # Complete onboarding. Three steps; the button is "Next" until the last step,
159
+ # then "Get Started" (both share id welcome-continue). Skip jumps straight to
160
+ # the app. Tap Skip by label text to dismiss the gate in one move.
161
+ - assertVisible: "Skip"
162
+ - tapOn: "Skip"
163
+ - waitForAnimationToEnd:
164
+ timeout: 5000
165
+
166
+ # Authenticated: the tabs root mounts on home.
167
+ - extendedWaitUntil:
168
+ visible:
169
+ id: "home-screen"
170
+ timeout: 20000
171
+
172
+ # Sign out via Settings. Native tab taps can be flaky, so assert-then-tap.
173
+ - assertVisible: "Settings"
174
+ - tapOn: "Settings"
175
+ - waitForAnimationToEnd:
176
+ timeout: 5000
177
+ - extendedWaitUntil:
178
+ visible:
179
+ id: "settings-screen"
180
+ timeout: 15000
181
+
182
+ # The sign-out row is a capsule row (label left, Spacer center), so tap by text.
183
+ - tapOn: "Sign out"
184
+ - waitForAnimationToEnd:
185
+ timeout: 3000
186
+ # Confirm in the native ConfirmationDialog. The action label is "Sign Out"
187
+ # (capital O), distinct from the row's "Sign out".
188
+ - assertVisible: "Sign Out"
189
+ - tapOn: "Sign Out"
190
+
191
+ # Back to the unauthenticated entry.
192
+ - extendedWaitUntil:
193
+ visible:
194
+ id: "sign-in-screen"
195
+ timeout: 20000
196
+
197
+ # Sign back in with the same credentials. Method toggle defaults to Email.
198
+ - tapOn:
199
+ id: "sign-in-email"
200
+ - inputText: ${MAESTRO_TEST_EMAIL}
201
+ - swipe:
202
+ start: 50%, 35%
203
+ end: 50%, 85%
204
+ # Same strong-password autofill dodge as sign-up: reveal, which auto-focuses.
205
+ - tapOn:
206
+ id: "sign-in-email-password-visibility"
207
+ - waitForAnimationToEnd:
208
+ timeout: 3000
209
+ - tapOn:
210
+ id: "sign-in-email-password"
211
+ - inputText: ${MAESTRO_TEST_PASSWORD}
212
+ - swipe:
213
+ start: 50%, 35%
214
+ end: 50%, 85%
215
+ - scrollUntilVisible:
216
+ element:
217
+ id: "sign-in-submit"
218
+ direction: DOWN
219
+ - tapOn:
220
+ id: "sign-in-submit"
221
+
222
+ # Authenticated again. Onboarding was already marked seen, so this lands on the
223
+ # tabs root directly. Generous timeout: live network call.
224
+ - extendedWaitUntil:
225
+ visible:
226
+ id: "home-screen"
227
+ timeout: 30000
228
+
229
+ - takeScreenshot: auth-final
@@ -12,12 +12,12 @@
12
12
  # not by `id`, or the tap lands on empty space. Native tab-bar taps can be flaky;
13
13
  # assert-then-tap or retry.
14
14
  #
15
- # Maestro auto-derives the appId from the build's bundle id, so the same flow
16
- # runs against the dev simulator binary (`com.example.<pkg>` fallback in
17
- # app.config.ts) and the production binary (`com.<your-team>.<app>`) without edits.
15
+ # `appId` reads ${MAESTRO_APP_ID}. EAS injects it from EXPO_PUBLIC_APP_BUNDLE_ID
16
+ # (see .eas/workflows/e2e-tests.yml), so the same flow runs against any fork's
17
+ # bundle id without edits. Locally, pass it from .env.local (needs a JDK on PATH):
18
18
  #
19
- # Run locally: `maestro test .maestro/launch.yaml`
20
- # Run on EAS: triggered by `.eas/workflows/e2e-tests.yml` on every PR.
19
+ # Run locally: MAESTRO_APP_ID=$(grep '^EXPO_PUBLIC_APP_BUNDLE_ID=' .env.local | cut -d= -f2) maestro test .maestro/launch.yaml
20
+ # Run on EAS: via `.eas/workflows/e2e-tests.yml` (workflow_dispatch only).
21
21
  appId: ${MAESTRO_APP_ID}
22
22
  ---
23
23
  - launchApp: