@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
@@ -3,26 +3,34 @@ name: Check
3
3
  on:
4
4
  pull_request:
5
5
  branches: [main]
6
+ push:
7
+ branches: [main]
6
8
 
7
9
  jobs:
8
10
  check:
9
11
  runs-on: ubuntu-latest
10
12
  steps:
11
- - uses: actions/checkout@v4
12
- - uses: oven-sh/setup-bun@v2
13
- - run: bun install
14
- - run: bunx expo-doctor
15
- - run: bun run typecheck
16
- - run: bun run lint
17
- - run: bun run format:check
18
- - run: bun run test
13
+ - uses: actions/checkout@v6
14
+ - uses: actions/setup-node@v6
15
+ with:
16
+ node-version: 22
17
+ cache: npm
18
+ - run: npm ci
19
+ - run: npx expo-doctor
20
+ - run: npm run typecheck
21
+ - run: npm run lint
22
+ - run: npm run format:check
23
+ - run: npm run test
19
24
 
20
25
  fingerprint:
21
26
  runs-on: ubuntu-latest
22
27
  steps:
23
- - uses: actions/checkout@v4
28
+ - uses: actions/checkout@v6
24
29
  with:
25
30
  fetch-depth: 0
26
- - uses: oven-sh/setup-bun@v2
27
- - run: bun install
28
- - run: bun run fp:diff
31
+ - uses: actions/setup-node@v6
32
+ with:
33
+ node-version: 22
34
+ cache: npm
35
+ - run: npm ci
36
+ - run: npm run fp:diff
@@ -1,9 +1,20 @@
1
- # Smoke test: app launches and the welcome screen renders.
1
+ # Smoke test: the app cold-starts and renders without crashing, plus a screenshot.
2
2
  #
3
- # Maestro auto-derives the appId from the build's bundle id, so the same
4
- # flow runs against the dev simulator binary (`com.example.<pkg>` fallback
5
- # in app.config.ts) and the production binary (`com.<your-team>.<app>`)
6
- # without edits.
3
+ # The UI is @expo/ui SwiftUI. `testID` props map to the native accessibilityIdentifier
4
+ # and DO surface to Maestro as `resource-id`, so `assertVisible: { id: "..." }` resolves
5
+ # against any tagged control or screen. Every interactive control and screen root
6
+ # carries a stable id (grep `testID=` under src/), so real id-based flows are viable,
7
+ # not just this no-crash smoke test.
8
+ #
9
+ # Driving note: Maestro taps an element's center. Buttons whose center holds content
10
+ # (the profile card, the ProminentButton submits) tap fine by `id`. The capsule list
11
+ # rows put a Spacer in the middle, so tap those by their label text (`tapOn: "Debug"`),
12
+ # not by `id`, or the tap lands on empty space. Native tab-bar taps can be flaky;
13
+ # assert-then-tap or retry.
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.
7
18
  #
8
19
  # Run locally: `maestro test .maestro/launch.yaml`
9
20
  # Run on EAS: triggered by `.eas/workflows/e2e-tests.yml` on every PR.
@@ -11,8 +22,6 @@ appId: ${MAESTRO_APP_ID}
11
22
  ---
12
23
  - launchApp:
13
24
  clearState: true
14
- - assertVisible:
15
- text: "Welcome"
16
- timeout: 30000
17
- - assertVisible: "Your new app starts here."
18
- - takeScreenshot: welcome
25
+ - waitForAnimationToEnd:
26
+ timeout: 15000
27
+ - takeScreenshot: launch
@@ -10,10 +10,9 @@ hand. These are conventions, not magic.
10
10
  calls. Everything goes through `convex/` (server) and `convex/react`
11
11
  (client). After running `npx convex ai-files install`, read
12
12
  `convex/_generated/ai/guidelines.md` before touching anything in `convex/`.
13
- - **Auth.** Better Auth via `@convex-dev/better-auth`. Patched locally with
14
- PR #368 until upstream merges (`patches/`). Email verification is gated on
15
- the `REQUIRE_EMAIL_VERIFICATION` Convex env var.
16
- - **Mobile.** Expo SDK 56 canary, RN 0.85, React 19. **iOS only today.**
13
+ - **Auth.** Better Auth via `@convex-dev/better-auth@0.12.3`. Email
14
+ verification is gated on the `REQUIRE_EMAIL_VERIFICATION` Convex env var.
15
+ - **Mobile.** Expo SDK 56, RN 0.85, React 19. **iOS only today.**
17
16
  Native UI exclusively via `@expo/ui/swift-ui`. No NativeWind, no Tailwind,
18
17
  no `react-native-paper`.
19
18
  - **CI/CD.** EAS Workflows (`.eas/workflows/*.yml`) for everything
@@ -45,7 +44,7 @@ hand. These are conventions, not magic.
45
44
 
46
45
  - **Convex functions**: every query/mutation needs both server-side
47
46
  validators and matching client types. The `convex/_generated/` directory is
48
- the contract. Run `bunx convex codegen` after schema or function changes.
47
+ the contract. Run `npx convex codegen` after schema or function changes.
49
48
  - **HTTP routes** (`convex/http.ts`): every public endpoint must use
50
49
  `convex/webhook.ts` `withWebhook()` factory for HMAC verification + body
51
50
  cap + structured logging, or document why it doesn't. Inbound webhooks are
@@ -56,18 +55,36 @@ hand. These are conventions, not magic.
56
55
  `APPLE_KEY_ID`, `APPLE_SERVICES_ID`, `CONVEX_DEPLOY_KEY`).
57
56
  - **Push notifications**: only work on a physical device. iOS Simulator does
58
57
  not deliver APNs. Don't try to test push flows in the simulator.
59
- - **`store.config.json`**: ships with placeholder values. `bunx vexpo
58
+ - **`store.config.json`**: ships with placeholder values. `npx vexpo
60
59
  rebrand` fills them in. App Review will reject builds with placeholder
61
60
  contact info.
62
61
 
63
62
  ## When in doubt
64
63
 
65
- - Run `bunx vexpo doctor` to check that `.env.local`, Convex env, EAS env,
64
+ - Run `npx vexpo doctor` to check that `.env.local`, Convex env, EAS env,
66
65
  and `app.config.ts` agree.
67
- - Use `bunx eas <subcommand>` for canonical EAS operations. **Don't reinvent
66
+ - Use `npx eas <subcommand>` for canonical EAS operations. **Don't reinvent
68
67
  EAS.** That's the vexpo design principle.
69
68
  - Read `SETUP.md` for the long-form orchestration walkthrough.
70
69
 
70
+ ## Agent setup
71
+
72
+ - **Claude Code:** install Expo's official agent skills with
73
+ `/plugin marketplace add expo/skills` then `/plugin install expo`. For
74
+ Codex, Cursor, or any other agent, run `npx skills add expo/skills`. The
75
+ Convex agent skills install separately via `npx convex ai-files install`.
76
+ - **Pre-approved commands:** `.claude/settings.json` allows read-only
77
+ `git`/`expo`/`eas`/`convex`/`vexpo` calls + the project's `npm run`
78
+ scripts (`typecheck`, `lint`, `test`, `format`, `dev`, `fp`) without
79
+ per-step permission prompts. Destructive ops (`git push`, `git reset`,
80
+ `npm install`, `expo deploy`) still ask.
81
+ - **EAS Convex bootstrap:** `eas integrations:convex:connect` is the
82
+ upstream SDK 56 path for provisioning a Convex backend, writing
83
+ `CONVEX_DEPLOY_KEY` + `EXPO_PUBLIC_CONVEX_URL`, and registering the env
84
+ vars across Production/Preview/Development. `npx vexpo full` is the
85
+ broader path that also wires Better Auth, Resend, and App Store identity
86
+ in one shot. Use vexpo for a complete starter, EAS for Convex alone.
87
+
71
88
  <!-- convex-ai-start -->
72
89
 
73
90
  This project uses [Convex](https://convex.dev) as its backend.
@@ -158,7 +158,9 @@ In-app typography uses the same Geist face but caps at Bold (700) for nav titles
158
158
  | Body emphasis | 600 SemiBold | 16 | 24 | 0 |
159
159
  | Caption | 400 Regular | 13-14 | 20-22 | 0 |
160
160
 
161
- Body and label text scales with the user's Larger Text accessibility setting via `lib/dynamic-font.ts`. The `useDynamicFont` hook multiplies the declared size by `useWindowDimensions().fontScale` before passing to `@expo/ui/swift-ui`'s `font()` modifier. Don't bypass this hook. If you do, your screen breaks at Larger Text.
161
+ Every label scales with the user's Larger Text accessibility setting via `src/lib/dynamic-font.ts`. The `useDynamicFont` hook maps the declared point size to a SwiftUI `Font.TextStyle` and passes it to `@expo/ui/swift-ui`'s `font()` modifier (upstream `expo/expo#46007`), so the Geist family rides Apple's Dynamic Type curves natively. SwiftUI rescales the text when the setting changes, no JS re-render. The declared size is the base, so default-size rendering is unchanged. Don't bypass this hook. If you do, your text stops scaling at Larger Text.
162
+
163
+ Native compact chrome does not scale with Dynamic Type, by iOS design, and that is not a bug to chase. Segmented `Picker` titles (Preferences' Appearance and Reduce Motion, the auth Sign in/Sign up and Email/Username toggles), inline navigation-bar titles, and tab-bar labels stay fixed at accessibility text sizes. SwiftUI's `pickerStyle("segmented")` renders titles in `UISegmentedControl`'s own font and ignores any `dfont` on the segments or the picker (verified on device). `react-native-screens` builds nav-title fonts at a fixed size (`scaleMultiplier: 1.0`, no `UIFontMetrics`), matching UIKit's compact inline title. Neither is `@expo/ui` or template code forcing it. iOS exposes this chrome to accessibility through the large-content HUD on long-press, not inline growth, so the behavior is correct and consistent. To make a control grow anyway you must leave the native control: reflow the segmented picker to a vertical option list at accessibility sizes, or use a large title (which does scale) instead of an inline one.
162
164
 
163
165
  Geist's variable axes are not currently exercised at runtime (we use static TTFs for individual weights via `expo-font`). The variable TTF ships in `assets/fonts/` for future use. If runtime needs variable weight interpolation, point `expo-font` at the variable file and use SwiftUI's `.fontWeight()` modifier.
164
166
 
@@ -269,17 +271,19 @@ The system uses `@expo/ui/swift-ui` primitives exclusively for native rendering.
269
271
  | `ProgressView` | Spinner or determinate progress | Loading states |
270
272
  | `ContentUnavailableView` | Empty state with SF Symbol + title + description | Empty home, no results |
271
273
 
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
+
272
276
  ### Custom composition (in `components/ui/`)
273
277
 
274
- | Component | Purpose | Notes |
275
- | :-------------------------- | :---------------------------------------------------------------------------- | :--------------------------------- |
276
- | `Material` | Translucent surface with iOS 26+ Liquid Glass / iOS 16.4-25 BlurView fallback | Reserve for navigation chrome only |
277
- | `OfflineBanner` | Top-of-screen notification banner using `Material` chrome variant | Shows when network unavailable |
278
- | `LoadingScreen` | Brand-icon + spinner, themed by appearance | Suspense fallback |
279
- | `ErrorBoundary` | Top-level crash boundary with brand recovery UI | Wraps each route segment |
280
- | `ConvexError` | Maps Convex errors to user-readable copy | Used in error displays |
281
- | `Skeleton` / `SkeletonLine` | Animated loading placeholders, respect Reduce Motion | Profile loading, list loading |
282
- | `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
+ | `ConvexError` | Maps Convex errors to user-readable copy | Used in error displays |
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 |
283
287
 
284
288
  ### Custom hooks (in `hooks/`)
285
289
 
@@ -2,20 +2,22 @@
2
2
 
3
3
  Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Native SwiftUI via `@expo/ui/swift-ui`, email + password + email OTP + Apple Sign In, APNs push, Universal Links, profile + active sessions with avatar uploads and device-by-device revocation. EAS for the whole build surface: 10 workflows, fingerprint-gated OTA-or-build, TestFlight, rollback, rollout, ASC events, and the Apple Sign In JWT rotation cron.
4
4
 
5
+ A lot of the SwiftUI modifiers the template reaches for, `clipShape("capsule")`, `defaultScrollAnchorForRole`, `scrollTargetBehavior`, `scrollPosition`, `textInputAutocapitalization`, `textContentType`, the `Alert` component, the Dynamic Type pair (`textStyle` scaling on `font`, `dynamicTypeSize` bounds), `accessibilityHidden`, are upstream PRs we wrote and got merged into `expo/expo`. Full ledger in [`../../docs/UPSTREAM.md`](../../docs/UPSTREAM.md).
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
8
- bun install
10
+ npm install
9
11
 
10
- bunx vexpo lite # 60-second path: Convex + Better Auth, simulator-ready
11
- bunx vexpo lite --new # same, plus a Convex signup walkthrough if you don't have one
12
+ npx vexpo lite # 60-second path: Convex + Better Auth, simulator-ready
13
+ npx vexpo lite --new # same, plus a Convex signup walkthrough if you don't have one
12
14
  ```
13
15
 
14
16
  Then in two terminals:
15
17
 
16
18
  ```bash
17
- bun run convex:dev # terminal 1
18
- bun run ios # terminal 2
19
+ npm run convex:dev # terminal 1
20
+ npm run ios # terminal 2
19
21
  ```
20
22
 
21
23
  Lite mode skips Apple / EAS / Resend entirely. `REQUIRE_EMAIL_VERIFICATION` is off on Convex so sign-up auto-verifies, the user lands in the app with one tap, and the UI hides the OTP / password-reset / change-email flows that need Resend to work.
@@ -23,33 +25,38 @@ Lite mode skips Apple / EAS / Resend entirely. `REQUIRE_EMAIL_VERIFICATION` is o
23
25
  When you're ready to ship, swap `lite` for `full`:
24
26
 
25
27
  ```bash
26
- bunx vexpo full # provisions Resend, Apple Sign In, EAS, rebrand wizard
27
- bunx vexpo full --new # same, plus walks Apple / Convex / Expo / Resend signups
28
+ npx vexpo full # provisions Resend, Apple Sign In, EAS, rebrand wizard
29
+ npx vexpo full --new # same, plus walks Apple / Convex / Expo / Resend signups
28
30
  ```
29
31
 
30
- `full` writes `.env.local`, sets Convex env vars (`REQUIRE_EMAIL_VERIFICATION=true` once Resend is wired), validates the ASC API key, signs the SIWA JWT, runs `eas init` and `eas env:push`, prompts the rebrand wizard. Prints the `eas build` command at the end. vexpo doesn't run it for you, you run `bunx eas build -p ios --profile production --auto-submit-with-profile testflight` when you're ready.
32
+ `full` writes `.env.local`, sets Convex env vars (`REQUIRE_EMAIL_VERIFICATION=true` once Resend is wired), validates the ASC API key, signs the SIWA JWT, runs `eas init` and `eas env:push`, prompts the rebrand wizard. Prints the `eas build` command at the end. vexpo doesn't run it for you, you run `npx eas build -p ios --profile production --auto-submit-with-profile testflight` when you're ready.
31
33
 
32
- Run `bunx vexpo doctor` any time to auth-check every credential against the real service and cross-reference IDs across `.env.local`, Convex env, EAS env, and `app.config.ts`. Catches "wrong .p8 from another project" or ".env.prod copied from a different fork" in seconds.
34
+ Run `npx vexpo doctor` any time to auth-check every credential against the real service and cross-reference IDs across `.env.local`, Convex env, EAS env, and `app.config.ts`. Catches "wrong .p8 from another project" or ".env.prod copied from a different fork" in seconds.
33
35
 
34
36
  Long-form walkthrough with every prompt, every env-var alternative, and recovery paths: [`SETUP.md`](./SETUP.md).
35
37
 
36
38
  ## What's wired up
37
39
 
38
- - Convex backend with reactive queries, storage, real-time sync, and rate limiting on every endpoint via `@convex-dev/rate-limiter`
39
- - Better Auth via `@convex-dev/better-auth` (sessions, accounts, devices)
40
+ - Convex backend with reactive queries, storage, real-time sync, and `@convex-dev/rate-limiter` on every application mutation. Auth-route rate limits ship via Better Auth at the HTTP layer.
41
+ - Better Auth via `@convex-dev/better-auth` (sessions, accounts; per-device revocation via `session.userAgent`)
42
+ - App Attest device attestation via `@expo/app-integrity` with server-side verification in Convex
40
43
  - Resend via `@convex-dev/resend` for OTP, password reset, change-email, with webhook delivery events
41
- - Apple Sign In via Apple's official `AppleAuthenticationButton`, HIG-compliant BLACK/WHITE theme-aware, SIWA Services ID + ES256 JWT signing (180-day expiry, auto-rotated every 90 days)
44
+ - Apple Sign In via Apple's official `AppleAuthenticationButton`, HIG-compliant (BLACK in dark mode, WHITE in light; `WHITE_OUTLINE` isn't used), SIWA Services ID + ES256 JWT signing (180-day expiry, auto-rotated every 90 days)
42
45
  - APNs push via `expo-notifications` with token registration on sign-in
43
46
  - Apple Universal Links from Convex's HTTP router (AASA at `/.well-known/apple-app-site-association`)
44
47
  - Profile editing with avatar uploads to Convex storage
45
48
  - Active sessions screen with device-by-device revocation
46
- - Theme switching, haptics toggle, reduced motion, dynamic type, VoiceOver labels everywhere
49
+ - Account soft-delete with a 30-day grace window and a restore-or-confirm screen on next sign-in
50
+ - Pull-to-refresh on home and sessions, plus an interactive update banner on iOS 26
51
+ - Theme switching, haptics toggle, reduced motion, VoiceOver labels everywhere (decorative views hidden from the rotor)
52
+ - Native Dynamic Type end to end: every label scales with the Larger Text setting via `textStyle`, bounded with `dynamicTypeSize` ceilings on the seven fixed-geometry controls that would clip instead of wrap
47
53
  - Spotlight-style search tab (debounced, scored, keyword-aware)
48
54
  - Skeleton placeholders during initial query loads
49
55
  - Debug screen at `/debug` gated by toggle, off in production by default
50
56
  - Liquid Glass on iOS 26+ via `expo-glass-effect`, UIVisualEffectView blur fallback on iOS 16.4-25 via `expo-blur`, both behind a `<Material>` primitive
51
- - EAS Build / Update / Submit / Metadata. `runtimeVersion: { policy: "fingerprint" }`, branch/channel model, `appVersionSource: "remote"`. ASC API key managed by EAS (`eas credentials -p ios`), no `eas.json` patches
52
- - 10 EAS Workflows under `.eas/workflows/`: dev builds, PR previews with `github-comment` + QR + fingerprint-gated OTA-or-build, deploy on `main`, TestFlight on `beta/*`, manual rollback / rollout, ASC event triggers to Slack, the SIWA JWT rotation cron, Maestro E2E
57
+ - OTA updates code-signed end-to-end (`expo-updates` code signing; generate the cert with `npm run updates:gen-cert`), so only signed bundles install
58
+ - EAS Build / Update / Submit / Metadata. `runtimeVersion: { policy: "fingerprint" }` (auto-bumps on native code changes), branch/channel model, `appVersionSource: "remote"`. ASC API key managed by EAS (`eas credentials -p ios`), no `eas.json` patches. `@expo/fingerprint >= 0.19.3` makes the policy deterministic across machines and CI out of the box, so the earlier `fingerprint.config.js` + `.fingerprintignore` jsi knobs were dropped.
59
+ - 10 EAS Workflows under `.eas/workflows/`: dev builds, PR previews with `github-comment` + QR + fingerprint-gated OTA-or-build, production deploy, TestFlight on `beta/*`, manual rollback / rollout, ASC event triggers to Slack, the SIWA JWT rotation cron, Maestro E2E. PR previews, Maestro E2E, and the production deploy are manual-only (`workflow_dispatch`) by default: the first two to conserve EAS build credits (restore their `pull_request` triggers to run on every PR), the deploy so a merge to `main` can't build, submit, and ship an OTA by surprise (add a `push: main` trigger if you want that)
53
60
  - GitHub Actions for general-purpose checks: typecheck, lint, format, tests, fingerprint diff on PR + push to `main`
54
61
 
55
62
  ## Pre-reqs
@@ -62,92 +69,90 @@ Long-form walkthrough with every prompt, every env-var alternative, and recovery
62
69
  ## Scripts
63
70
 
64
71
  ```
65
- bun run dev Metro + dev client
66
- bun run start Metro with cleared cache
67
- bun run ios Clean prebuild + compile + run on simulator
68
- bun run ios:dev Run on simulator (skip prebuild, fast)
69
- bun run ios:device Clean prebuild + compile + run on physical device
70
- bun run prebuild Generate iOS native project from config
71
-
72
- bun run convex:dev Convex dev server (watch mode)
73
- bun run convex:deploy Deploy Convex functions to production
74
- bun run convex:logs Tail dev deployment logs
75
- bun run convex:logs:prod Tail prod deployment logs
76
- bun run convex:env List dev env vars
77
- bun run convex:env:prod List prod env vars
78
- bun run convex:insights OCC conflicts + resource limits (dev)
79
- bun run convex:insights:prod Same for prod
80
- bun run convex:dashboard Open the Convex dashboard
81
- bun run convex:codegen Regenerate convex/_generated/
82
-
83
- bun run eas:dev eas build -p ios --profile development:simulator
84
- bun run eas:dev:device eas build -p ios --profile development:device
85
- bun run eas:tf eas build -p ios --profile production --auto-submit-with-profile testflight
86
- bun run eas:prod eas build -p ios --profile production
87
- bun run metadata:lint eas metadata:lint
88
- bun run metadata:push eas metadata:lint && eas metadata:push
89
- bun run metadata:pull eas metadata:pull
90
- bun run env:pull eas env:pull --environment development
91
- bun run env:pull:prod eas env:pull --environment production
92
-
93
- bun run clean Trash node_modules, ios, caches, then reinstall
94
- bun run clean:metro Trash Metro/Babel/Haste caches only
95
- bun run clean:state Wipe .setup-state.json + standard clean
96
- bun run typecheck tsc --noEmit
97
- bun run lint oxlint
98
- bun run format oxfmt
99
- bun run format:check oxfmt --check
100
- bun run test vitest run
101
- bun run test:watch vitest
102
- bun run fp Print Expo fingerprint hash
103
- bun run fp:diff Diff fingerprint vs base ref
104
- bun run upgrade expo install expo@canary && expo install --fix
105
- bun run upgrade:stable expo install expo@latest && expo install --fix
72
+ npm run dev Metro + dev client
73
+ npm run start Metro with cleared cache
74
+ npm run ios Clean prebuild + compile + run on simulator
75
+ npm run ios:dev Run on simulator (skip prebuild, fast)
76
+ npm run ios:device Clean prebuild + compile + run on physical device
77
+ npm run prebuild Generate iOS native project from config
78
+
79
+ npm run convex:dev Convex dev server (watch mode)
80
+ npm run convex:deploy Deploy Convex functions to production
81
+ npm run convex:logs Tail dev deployment logs
82
+ npm run convex:logs:prod Tail prod deployment logs
83
+ npm run convex:env List dev env vars
84
+ npm run convex:env:prod List prod env vars
85
+ npm run convex:insights OCC conflicts + resource limits (dev)
86
+ npm run convex:insights:prod Same for prod
87
+ npm run convex:dashboard Open the Convex dashboard
88
+ npm run convex:codegen Regenerate convex/_generated/
89
+
90
+ npm run eas:dev eas build -p ios --profile development:simulator
91
+ npm run eas:dev:device eas build -p ios --profile development:device
92
+ npm run eas:tf eas build -p ios --profile production --auto-submit-with-profile testflight
93
+ npm run eas:prod eas build -p ios --profile production
94
+ npm run metadata:lint eas metadata:lint
95
+ npm run metadata:push eas metadata:lint && eas metadata:push
96
+ npm run metadata:pull eas metadata:pull
97
+ npm run env:pull eas env:pull --environment development
98
+ npm run env:pull:prod eas env:pull --environment production
99
+
100
+ npm run clean Trash node_modules, ios, caches, then reinstall
101
+ npm run clean:metro Trash Metro/Babel/Haste caches only
102
+ npm run clean:state Wipe .setup-state.json + standard clean
103
+ npm run typecheck tsc --noEmit
104
+ npm run lint oxlint
105
+ npm run format oxfmt
106
+ npm run format:check oxfmt --check
107
+ npm run test vitest run
108
+ npm run test:watch vitest
109
+ npm run fp Print Expo fingerprint hash
110
+ npm run fp:diff Diff fingerprint vs base ref
111
+ npm run upgrade expo install expo@next && expo install --fix
112
+ npm run upgrade:stable expo install expo@latest && expo install --fix
106
113
  ```
107
114
 
108
- Setup is one-shot, not a `package.json` script. Run `bunx vexpo lite` / `bunx vexpo full` / `bunx vexpo doctor` directly. All deletions go through `trash` (macOS Trash, recoverable).
115
+ Setup is one-shot, not a `package.json` script. Run `npx vexpo lite` / `npx vexpo full` / `npx vexpo doctor` directly. All deletions go through `trash` (macOS Trash, recoverable).
109
116
 
110
117
  ## Project structure
111
118
 
112
119
  ```
113
- app/ Expo Router screens
114
- (auth)/ Sign in, sign up, forgot/reset password
115
- (app)/ Authenticated screens
116
- (tabs)/ Tab navigation
117
- welcome.tsx, profile.tsx, sessions.tsx, debug.tsx, ...
118
- +native-intent.tsx Deep link validation
119
- +not-found.tsx 404 fallback
120
- components/ Reusable UI
121
- constants/ Theme, layout, UI tokens
120
+ src/
121
+ app/ Expo Router screens
122
+ (app)/ Authenticated stack (auth modal, tabs, profile, ...)
123
+ (tabs)/ Home, search, settings
124
+ auth/ Sign in, sign up, forgot/reset password (modal)
125
+ profile/ index.tsx + change-password.tsx
126
+ welcome.tsx, sessions.tsx, restore-account.tsx, debug.tsx, ...
127
+ +native-intent.tsx Deep link validation
128
+ +not-found.tsx 404 fallback
129
+ components/ Reusable UI (auth/, ui/)
130
+ constants/ Theme, layout, UI tokens
131
+ hooks/ useNetwork, useTheme, useUpdates, etc.
132
+ lib/ Auth client, haptics, env, deep links, native state
122
133
  convex/ Convex backend
123
- hooks/ useNetwork, useTheme, useUpdates, etc.
124
- lib/ Auth client, haptics, env, deep links
125
134
  plugins/
126
- with-auto-signing.js Sets CODE_SIGN_STYLE=Automatic + DEVELOPMENT_TEAM
135
+ with-auto-signing.js CODE_SIGN_STYLE=Automatic + DEVELOPMENT_TEAM
127
136
  with-pod-deployment-target.js Forces every pod to iOS 16.4
128
137
  .eas/workflows/ 10 EAS Workflow YAML files
129
138
  .github/workflows/check.yml Typecheck, lint, format, tests, fingerprint diff
130
139
  scripts/
131
140
  clean.ts Trash + reinstall
132
141
  rotate-apple-jwt.mjs CI: re-sign JWT from env vars
133
- __tests__/ Convex constants + validators + HMAC verification + deep-link
142
+ __tests__/ Convex + lib unit tests (validators, HMAC, deep link, schemas)
134
143
  ```
135
144
 
136
145
  ## Long-form docs
137
146
 
138
147
  - [`SETUP.md`](./SETUP.md). Every setup phase with full prompts, env-var alternatives for non-interactive runs, recovery paths.
139
148
  - [`DESIGN.md`](./DESIGN.md). Color palette, typography, spacing, radius ladder, materials, the SwiftUI primitives + custom composition surface.
149
+ - [`../../docs/UPSTREAM.md`](../../docs/UPSTREAM.md). Every upstream PR powering the template: `@expo/ui/swift-ui` modifiers, `expo-modules-core` fixes, `expo-tools` resolution, CI workflow guards.
140
150
  - [`AGENTS.md`](./AGENTS.md). Guidance for AI coding agents working in this codebase.
141
151
 
142
152
  ## Version pinning
143
153
 
144
- Every `expo-*` package uses the same canary tag. Mismatched tags cause subtle runtime crashes.
145
-
146
- `package.json` overrides:
147
-
148
- - `@better-auth/passkey: 1.6.9` prevents better-auth from pulling SolidJS deps that break Metro.
149
- - `@expo/ui: 56.0.0-canary-20260506-03817f5` unifies the version across transitive deps.
154
+ Every `expo-*` package tracks the same SDK 56 release. Mismatched versions cause subtle runtime crashes. `npm run upgrade:stable` runs `expo install expo@latest && expo install --fix` to roll all of them forward together; `npm run upgrade` (`expo@next`) tracks the next SDK preview.
150
155
 
151
- `react-native-reanimated 4.3.0` is intentionally ahead of the canary's expected 4.2.1 because Bun resolves `react-native-worklets@0.8.x` which needs Reanimated 4.3+. Listed in `expo.install.exclude` to silence the doctor warning.
156
+ `@convex-dev/better-auth@0.12.0` is the minimum compatible with `better-auth@1.6.x` (peer-dep range is `>=1.6.9 <1.7.0`). Earlier versions peer-dep `better-auth <1.6.0` and reject the `mode` field newer better-auth adds to adapter queries, breaking signup. The template pins `better-auth@1.6.16` + `@convex-dev/better-auth@0.12.3`.
152
157
 
153
- `@convex-dev/better-auth@0.12.0` is the minimum compatible with `better-auth@1.6.9`. Earlier versions peer-dep `better-auth <1.6.0` and reject the `mode` field newer better-auth adds to adapter queries, breaking signup. Currently the template installs from `patches/convex-dev-better-auth-0.12.2.tgz` until [PR #368](https://github.com/get-convex/better-auth/pull/368) merges.
158
+ `convex` is pinned `~1.40.0` for now: 1.41.0 adds a `transactionLimits` options param to `runMutation` that `@convex-dev/resend@0.2.4`'s ctx types reject, which breaks the `convex/http.ts` typecheck. Widen the range back to `^1.40.0` once resend's types accept 1.41.