@ramonclaudio/create-vexpo 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +10 -10
  2. package/dist/index.js +8 -7
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
  5. package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
  6. package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
  7. package/dist/templates/default/.eas/workflows/release.yml +3 -7
  8. package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
  9. package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
  10. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
  11. package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
  12. package/dist/templates/default/.github/workflows/check.yml +20 -12
  13. package/dist/templates/default/.maestro/launch.yaml +19 -10
  14. package/dist/templates/default/AGENTS.md +25 -8
  15. package/dist/templates/default/DESIGN.md +14 -10
  16. package/dist/templates/default/README.md +83 -78
  17. package/dist/templates/default/SETUP.md +159 -152
  18. package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
  19. package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
  20. package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
  21. package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
  22. package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
  23. package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
  24. package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
  25. package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
  26. package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
  27. package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
  28. package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
  29. package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
  30. package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
  31. package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
  32. package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
  33. package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
  34. package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
  35. package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
  36. package/dist/templates/default/_env.example +7 -7
  37. package/dist/templates/default/_gitattributes +1 -1
  38. package/dist/templates/default/_gitignore +17 -2
  39. package/dist/templates/default/_npmrc +7 -0
  40. package/dist/templates/default/_oxlintrc.json +1 -1
  41. package/dist/templates/default/app-store/accessibility.config.json +20 -0
  42. package/dist/templates/default/app-store/privacy.config.json +27 -0
  43. package/dist/templates/default/app.config.ts +105 -33
  44. package/dist/templates/default/app.json +1 -9
  45. package/dist/templates/default/convex/_generated/api.d.ts +12 -0
  46. package/dist/templates/default/convex/admin.ts +0 -13
  47. package/dist/templates/default/convex/appAttest.ts +467 -0
  48. package/dist/templates/default/convex/appAttestStore.ts +141 -0
  49. package/dist/templates/default/convex/apple.ts +53 -0
  50. package/dist/templates/default/convex/auth.ts +6 -45
  51. package/dist/templates/default/convex/constants.ts +2 -7
  52. package/dist/templates/default/convex/crons.ts +12 -5
  53. package/dist/templates/default/convex/email.ts +4 -24
  54. package/dist/templates/default/convex/env.ts +0 -4
  55. package/dist/templates/default/convex/errors.ts +0 -7
  56. package/dist/templates/default/convex/functions.ts +0 -26
  57. package/dist/templates/default/convex/http.ts +3 -5
  58. package/dist/templates/default/convex/log.ts +2 -25
  59. package/dist/templates/default/convex/pushSender.ts +145 -0
  60. package/dist/templates/default/convex/pushTokens.ts +110 -13
  61. package/dist/templates/default/convex/rateLimit.ts +8 -39
  62. package/dist/templates/default/convex/schema.ts +48 -5
  63. package/dist/templates/default/convex/tsconfig.json +1 -0
  64. package/dist/templates/default/convex/users.ts +143 -61
  65. package/dist/templates/default/convex/validators.ts +1 -38
  66. package/dist/templates/default/convex/webhook.ts +1 -31
  67. package/dist/templates/default/convex.json +1 -2
  68. package/dist/templates/default/metro.config.js +9 -1
  69. package/dist/templates/default/package.json +67 -70
  70. package/dist/templates/default/plugins/README.md +5 -1
  71. package/dist/templates/default/scripts/README.md +9 -9
  72. package/dist/templates/default/scripts/_run.mjs +3 -20
  73. package/dist/templates/default/scripts/clean.ts +81 -69
  74. package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
  75. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
  76. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
  77. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
  78. package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
  79. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
  80. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
  81. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
  82. package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
  83. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
  84. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
  85. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
  86. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
  87. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
  88. package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
  89. package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
  90. package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
  91. package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
  92. package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
  93. package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
  94. package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
  95. package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
  96. package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
  97. package/dist/templates/default/src/app/+native-intent.tsx +25 -0
  98. package/dist/templates/default/src/app/+not-found.tsx +43 -0
  99. package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
  100. package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
  101. package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
  102. package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
  103. package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
  104. package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
  105. package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
  106. package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
  107. package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
  108. package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
  109. package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
  110. package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
  111. package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
  112. package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
  113. package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
  114. package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
  115. package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
  116. package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
  117. package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
  118. package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
  119. package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
  120. package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
  121. package/dist/templates/default/src/hooks/use-network.ts +34 -0
  122. package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
  123. package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
  124. package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
  125. package/dist/templates/default/src/lib/appAttest.ts +78 -0
  126. package/dist/templates/default/src/lib/assets.ts +9 -0
  127. package/dist/templates/default/src/lib/deep-link.ts +82 -0
  128. package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
  129. package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
  130. package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
  131. package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
  132. package/dist/templates/default/src/lib/masks.ts +21 -0
  133. package/dist/templates/default/src/lib/native-state.ts +20 -0
  134. package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
  135. package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
  136. package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
  137. package/dist/templates/default/src/lib/text-style.ts +20 -0
  138. package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
  139. package/dist/templates/default/store.config.json +1 -1
  140. package/dist/templates/default/tsconfig.json +3 -1
  141. package/dist/templates/default/vitest.config.ts +8 -1
  142. package/package.json +5 -5
  143. package/dist/templates/default/app/(app)/_layout.tsx +0 -73
  144. package/dist/templates/default/app/(app)/debug.tsx +0 -389
  145. package/dist/templates/default/app/(app)/sessions.tsx +0 -191
  146. package/dist/templates/default/app/(app)/welcome.tsx +0 -140
  147. package/dist/templates/default/app/+native-intent.tsx +0 -14
  148. package/dist/templates/default/app/+not-found.tsx +0 -51
  149. package/dist/templates/default/bun.lock +0 -1860
  150. package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
  151. package/dist/templates/default/components/ui/convex-error.tsx +0 -32
  152. package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
  153. package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
  154. package/dist/templates/default/components/ui/status-text.tsx +0 -49
  155. package/dist/templates/default/components/ui/update-banner.tsx +0 -82
  156. package/dist/templates/default/fingerprint.config.js +0 -9
  157. package/dist/templates/default/hooks/use-debounce.ts +0 -20
  158. package/dist/templates/default/hooks/use-deep-link.ts +0 -43
  159. package/dist/templates/default/hooks/use-network.ts +0 -11
  160. package/dist/templates/default/lib/assets.ts +0 -17
  161. package/dist/templates/default/lib/deep-link.ts +0 -71
  162. package/dist/templates/default/patches/PR-368.patch +0 -91
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
  165. /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
  166. /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
  167. /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
  168. /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
  169. /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
  170. /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
  171. /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
  172. /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
  173. /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
  174. /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "vitest";
2
+
3
+ import { textStyleForSize } from "@/lib/text-style";
4
+
5
+ describe("textStyleForSize", () => {
6
+ test("maps every point size in the template's type scale to a Font.TextStyle", () => {
7
+ expect(textStyleForSize(34)).toBe("largeTitle");
8
+ expect(textStyleForSize(32)).toBe("largeTitle");
9
+ expect(textStyleForSize(30)).toBe("title");
10
+ expect(textStyleForSize(28)).toBe("title");
11
+ expect(textStyleForSize(24)).toBe("title2");
12
+ expect(textStyleForSize(22)).toBe("title2");
13
+ expect(textStyleForSize(20)).toBe("title3");
14
+ expect(textStyleForSize(18)).toBe("title3");
15
+ expect(textStyleForSize(17)).toBe("body");
16
+ expect(textStyleForSize(16)).toBe("callout");
17
+ expect(textStyleForSize(15)).toBe("subheadline");
18
+ expect(textStyleForSize(14)).toBe("footnote");
19
+ expect(textStyleForSize(13)).toBe("footnote");
20
+ expect(textStyleForSize(12)).toBe("caption");
21
+ expect(textStyleForSize(11)).toBe("caption2");
22
+ });
23
+
24
+ test("clamps sizes below the scale to caption2", () => {
25
+ expect(textStyleForSize(8)).toBe("caption2");
26
+ });
27
+
28
+ test("clamps sizes above the scale to largeTitle", () => {
29
+ expect(textStyleForSize(64)).toBe("largeTitle");
30
+ });
31
+ });
@@ -1,10 +1,10 @@
1
- # .env.local template. Copy to .env.local then run `bun run setup`.
2
- # `bun run setup` writes most of these for you. The two identity vars
1
+ # .env.local template. Copy to .env.local then run `npm run setup`.
2
+ # `npm run setup` writes most of these for you. The two identity vars
3
3
  # (EXPO_PUBLIC_APP_BUNDLE_ID and EXPO_PUBLIC_APPLE_TEAM_ID) are prompted by
4
- # `bun run setup:convex`. Edit by hand if you prefer.
4
+ # `npm run setup:convex`. Edit by hand if you prefer.
5
5
 
6
6
  # ---------------------------------------------------------------------------
7
- # Convex deployment (written by `bun run setup:convex`)
7
+ # Convex deployment (written by `npm run setup:convex`)
8
8
  # ---------------------------------------------------------------------------
9
9
  CONVEX_DEPLOYMENT=
10
10
  EXPO_PUBLIC_CONVEX_URL=
@@ -12,7 +12,7 @@ EXPO_PUBLIC_CONVEX_SITE_URL=
12
12
  EXPO_PUBLIC_SITE_URL=
13
13
 
14
14
  # ---------------------------------------------------------------------------
15
- # iOS identity (prompted by `bun run setup:convex`, also pushed to Convex env)
15
+ # iOS identity (prompted by `npm run setup:convex`, also pushed to Convex env)
16
16
  # ---------------------------------------------------------------------------
17
17
  # Reverse-DNS bundle id, e.g. com.you.vexpo.
18
18
  EXPO_PUBLIC_APP_BUNDLE_ID=
@@ -26,9 +26,9 @@ EXPO_PUBLIC_APPLE_TEAM_ID=
26
26
  # EXPO_PUBLIC_EXPO_OWNER=
27
27
  # Toggles `name` to "Vexpo (Dev)" so dev and prod can install side-by-side.
28
28
  # APP_VARIANT=development
29
- # Pre-fills the full-access Resend key for `bun run setup:resend` (else prompts).
29
+ # Pre-fills the full-access Resend key for `npm run setup:resend` (else prompts).
30
30
  # RESEND_FULL_ACCESS_KEY=
31
- # Skips prompts in `bun run setup:apple` / `setup:asc`.
31
+ # Skips prompts in `npm run setup:apple` / `setup:asc`.
32
32
  # APPLE_P8_PATH=
33
33
  # APPLE_AUTH_KEY_PATH=
34
34
  # APPLE_ASC_ISSUER_ID=
@@ -2,6 +2,6 @@
2
2
  README.md merge=ours
3
3
  .env.example merge=ours
4
4
  package.json merge=ours
5
- bun.lock merge=ours
5
+ package-lock.json merge=ours
6
6
  app.json merge=ours
7
7
  eas.json merge=ours
@@ -5,6 +5,7 @@ node_modules/
5
5
  .env
6
6
  .env.*
7
7
  !.env.example
8
+ .envrc
8
9
 
9
10
  # Expo
10
11
  .expo/
@@ -22,12 +23,26 @@ ios/
22
23
  AuthKey_*
23
24
  SubscriptionKey_*
24
25
 
26
+ # EAS Update code-signing private key. The matching public cert at
27
+ # `certs/certificate.pem` IS committed (it's bundled in the .ipa and used
28
+ # on-device to verify OTA bundles). The private key lives only on the
29
+ # author's machine + as an EAS file-type secret (`EAS_UPDATE_PRIVATE_KEY`).
30
+ keys/
31
+ private-key.pem
32
+ *.private-key.pem
33
+
25
34
  # EAS (CLI state; keep workflows tracked)
26
35
  .eas/*
27
36
  !.eas/workflows/
28
37
 
29
- # Agents / local AI tooling
30
- .claude/
38
+ # Agents / local AI tooling.
39
+ # The monorepo .gitignore drops .claude/ repo-wide; the template re-includes
40
+ # its own .claude/settings.json so the scaffolded SDK 56 agent config ships
41
+ # with the template. Everything else inside .claude/ (transcripts, hooks,
42
+ # local state) stays untracked.
43
+ !.claude/
44
+ .claude/*
45
+ !.claude/settings.json
31
46
  .agents/
32
47
  .cursor/
33
48
  .aider*
@@ -0,0 +1,7 @@
1
+ # Expo's preview tags advance React + React DOM independently of the rest of
2
+ # the dep tree, so transitive peerOptional `react-dom` resolves to a newer
3
+ # version than the root `react` pin. npm 7+ rejects this by default (ERESOLVE);
4
+ # bun installs it loose. Setting legacy-peer-deps mirrors bun's behavior and
5
+ # matches Expo's own template defaults. Drop this once the Expo SDK ships with
6
+ # react + react-dom aligned and EAS Build's npm version is happy with it.
7
+ legacy-peer-deps=true
@@ -19,7 +19,7 @@
19
19
  "import/no-unassigned-import": "off",
20
20
  "no-await-in-loop": "off",
21
21
  "unicorn/consistent-function-scoping": "off",
22
- "no-underscore-dangle": ["warn", { "allow": ["_id", "_creationTime"] }],
22
+ "no-underscore-dangle": ["warn", { "allow": ["_id", "_creationTime", "_contentAvailable"] }],
23
23
  "no-shadow": ["warn", { "allow": ["resolve", "reject"] }]
24
24
  },
25
25
  "ignorePatterns": [
@@ -0,0 +1,20 @@
1
+ {
2
+ "_comment": "Drives `vexpo asc:accessibility lint`. The starter template ships VoiceOver labels, DynamicType, reduced-motion respect, and DynamicColorIOS for high-contrast support. Audit each feature against your actual user flows before declaring FULLY_SUPPORTS in production.",
3
+ "entries": [
4
+ {
5
+ "deviceFamily": "IPHONE",
6
+ "features": {
7
+ "VOICE_OVER": "FULLY_SUPPORTS",
8
+ "VOICE_CONTROL": "FULLY_SUPPORTS",
9
+ "LARGER_TEXT": "FULLY_SUPPORTS",
10
+ "DARK_INTERFACE": "FULLY_SUPPORTS",
11
+ "SUFFICIENT_CONTRAST": "FULLY_SUPPORTS",
12
+ "DIFFERENTIATION_WITHOUT_COLOR_ALONE": "FULLY_SUPPORTS",
13
+ "REDUCED_MOTION": "FULLY_SUPPORTS",
14
+ "CAPTIONS": "NOT_APPLICABLE",
15
+ "AUDIO_DESCRIPTIONS": "NOT_APPLICABLE"
16
+ },
17
+ "notes": "Verify against the actual screens in your app before submission. The defaults reflect what the vexpo template ships, not what your app delivers."
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "_comment": "Drives `vexpo asc:privacy lint`. The dashboard form at App Store Connect is the source of truth for submission; this file is for keeping a versioned, reviewable copy alongside the code. Default state: the starter collects no data beyond what Better Auth needs for sign-in (email + opaque user id), which `IDENTIFIERS` covers. Tighten or expand to match your app.",
3
+ "collectsData": true,
4
+ "entries": [
5
+ {
6
+ "category": "CONTACT_INFO",
7
+ "collected": true,
8
+ "usedForTracking": false,
9
+ "linkedToUser": true,
10
+ "purposes": ["APP_FUNCTIONALITY"]
11
+ },
12
+ {
13
+ "category": "IDENTIFIERS",
14
+ "collected": true,
15
+ "usedForTracking": false,
16
+ "linkedToUser": true,
17
+ "purposes": ["APP_FUNCTIONALITY"]
18
+ },
19
+ {
20
+ "category": "DIAGNOSTICS",
21
+ "collected": true,
22
+ "usedForTracking": false,
23
+ "linkedToUser": false,
24
+ "purposes": ["ANALYTICS", "APP_FUNCTIONALITY"]
25
+ }
26
+ ]
27
+ }
@@ -1,10 +1,22 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
1
4
  import type { ConfigContext, ExpoConfig } from "expo/config";
2
5
 
3
- import pkg from "./package.json";
6
+ // Read JSON via fs, not a static import or `require`. Node 24 strips TS types
7
+ // natively and loads this file as ESM (it has `export default`), where bare
8
+ // JSON imports need an attribute and `require`/`__dirname` don't exist. The
9
+ // expo CLI path transpiles to CJS via sucrase. `fs` + `process.cwd()` is the
10
+ // only shape that reads identically under both, and cwd is the project root in
11
+ // every context that evaluates this config (expo, eas, prebuild).
12
+ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")) as {
13
+ name: string;
14
+ version: string;
15
+ };
4
16
 
5
17
  const IS_DEV = process.env.APP_VARIANT === "development";
6
18
 
7
- // Identity comes from .env.local (written by `bun run setup:convex`). Fallbacks
19
+ // Identity comes from .env.local (written by `npm run setup:convex`). Fallbacks
8
20
  // keep `expo prebuild` from crashing on a fresh checkout, but a real build
9
21
  // requires real values.
10
22
  const BUNDLE_ID = process.env.EXPO_PUBLIC_APP_BUNDLE_ID ?? `com.example.${pkg.name}`;
@@ -24,7 +36,9 @@ type StoreConfig = {
24
36
  };
25
37
  const storeConfig: StoreConfig | undefined = (() => {
26
38
  try {
27
- return require("./store.config.json") as StoreConfig;
39
+ return JSON.parse(
40
+ readFileSync(resolve(process.cwd(), "store.config.json"), "utf8"),
41
+ ) as StoreConfig;
28
42
  } catch {
29
43
  return undefined;
30
44
  }
@@ -38,11 +52,24 @@ const SUPPORT = {
38
52
  };
39
53
 
40
54
  export default ({ config }: ConfigContext): ExpoConfig => {
41
- // `eas init` writes `extra.eas.projectId` into app.json. The static config
42
- // is merged into our return value via `...config`, so reading from
43
- // `config.extra?.eas?.projectId` here picks up the EAS project id without
44
- // hand-editing this file every time `eas init` runs.
45
- const projectId = (config.extra as { eas?: { projectId?: string } } | undefined)?.eas?.projectId;
55
+ // Primary source: `eas init` writes `extra.eas.projectId` into app.json, and
56
+ // the static config is merged into our return value via `...config`. Reading
57
+ // from `config.extra?.eas?.projectId` picks that up without hand-editing
58
+ // this file.
59
+ //
60
+ // Fallback: `process.env.EAS_PROJECT_ID`. Useful when the var is in the
61
+ // parent process env at eas-cli invocation time — e.g. shell export, direnv
62
+ // auto-load, or an eas.json profile `env` entry. Note that eas-cli itself
63
+ // does NOT auto-load `.env.local` for projectId resolution (it spawns
64
+ // `expo config` with `EXPO_NO_DOTENV=1` to keep config eval deterministic),
65
+ // so dropping `EAS_PROJECT_ID` into `.env.local` alone won't skip the
66
+ // `Configure this project?` prompt on a fresh checkout. The fallback DOES
67
+ // help vexpo's own CLI commands (which load `.env.local` themselves) and
68
+ // any tooling that pre-populates process.env before invoking expo/eas.
69
+ // See `README.md` for the recommended direnv-based local setup.
70
+ const projectId =
71
+ (config.extra as { eas?: { projectId?: string } } | undefined)?.eas?.projectId ??
72
+ process.env.EAS_PROJECT_ID;
46
73
 
47
74
  return {
48
75
  ...config,
@@ -61,41 +88,60 @@ export default ({ config }: ConfigContext): ExpoConfig => {
61
88
  scheme: "vexpo",
62
89
  icon: "./assets/icon.png",
63
90
  ...(EXPO_OWNER ? { owner: EXPO_OWNER } : {}),
91
+ // Fingerprint policy: a native change auto-bumps the runtime hash so OTA
92
+ // updates never load against an incompatible binary. The local-vs-EAS
93
+ // drift that broke earlier SDK 56 builds no longer reproduces on current
94
+ // deps (precompiled modules + codegen land in `ios/`, which fingerprint
95
+ // ignores; jsi artifacts covered by @expo/fingerprint >=0.19.3 defaults),
96
+ // so no autolinking workaround is needed. Verified by unpatched EAS builds
97
+ // clearing CONFIGURE_EXPO_UPDATES.
64
98
  runtimeVersion: { policy: "fingerprint" },
65
99
  developmentClient: {
66
100
  silentLaunch: true,
67
101
  },
68
102
  updates: {
103
+ // Derived from projectId, which resolves identically locally and on EAS
104
+ // workers because both read the plaintext `EAS_PROJECT_ID` env var. That
105
+ // symmetry keeps `extra.eas`/`updates.url`/`enabled` in fingerprint parity
106
+ // with no `sourceSkips`. If projectId is unset (fresh fork before setup),
107
+ // this is false on both sides: still consistent, OTA just stays inactive
108
+ // until configured.
69
109
  enabled: !!projectId,
70
110
  checkAutomatically: "ON_LOAD",
71
- fallbackToCacheTimeout: 0,
72
- enableBsdiffPatchSupport: true,
111
+ // Brief patience for the update server before falling back to the
112
+ // bundled JS. Zero blocks every cold launch indefinitely on flaky
113
+ // networks (hotel WiFi, captive portals). 2 seconds is enough for
114
+ // a fast check on good networks and a graceful timeout otherwise.
115
+ fallbackToCacheTimeout: 2000,
73
116
  ...(projectId ? { url: `https://u.expo.dev/${projectId}` } : {}),
74
117
  // `expo-channel-name` request header is required for runtime channel
75
- // surfing via `Updates.setUpdateRequestHeadersOverride`. Baseline
76
- // value is overwritten per-build by EAS Build (it reads the channel
77
- // from the eas.json build profile). Without this baseline declared
78
- // here, override calls reject with "unknown header".
79
- requestHeaders: { "expo-channel-name": "production" },
118
+ // surfing via `Updates.setUpdateRequestHeadersOverride`. EAS Build
119
+ // overwrites the baseline per-build from the eas.json build profile,
120
+ // so the value here only matters for local dev/prebuild without an
121
+ // EAS profile. `development` is the safe default; a missed profile
122
+ // override won't accidentally pull production OTA into a dev build.
123
+ requestHeaders: { "expo-channel-name": "development" },
80
124
  // Only ship icon + splash with each OTA. Fonts, sounds, and other
81
125
  // build-baked assets stay in the .ipa and never download. Shrinks
82
126
  // bundle by ~95% on diff-able updates.
83
127
  assetPatternsToBeBundled: ["assets/icon.png", "assets/splash-image-*.png"],
84
- // Production/Enterprise plan opt-in. Sign updates with a private key
85
- // checked against the bundled certificate so a compromised CDN can't
86
- // ship arbitrary JS. Generate keypair + cert via:
87
- // bunx expo-updates codesigning:generate \
88
- // --certificate-output-directory certs \
89
- // --key-output-directory ../keys \
90
- // --certificate-validity-duration-years 10 \
91
- // --certificate-common-name "Your Organization Name"
92
- // Store `../keys/private-key.pem` as an EAS env file-type secret
93
- // (`EAS_UPDATE_PRIVATE_KEY`) and reference it from
94
- // `.eas/workflows/deploy-production.yml`'s update job via
95
- // `params.private_key_path: "$EAS_UPDATE_PRIVATE_KEY"`. Then
96
- // uncomment the block below.
97
- // codeSigningCertificate: "./certs/certificate.pem",
98
- // codeSigningMetadata: { keyid: "main", alg: "rsa-v1_5-sha256" },
128
+ // End-to-end OTA code signing. The cert is committed at
129
+ // `./certs/certificate.pem` and bundled with each .ipa; the device
130
+ // verifies every update against it before applying. The matching
131
+ // private key lives ONLY as an EAS file-type secret
132
+ // (`EAS_UPDATE_PRIVATE_KEY`) and never lands in the repo.
133
+ // Generate the keypair once with `npm run updates:gen-cert`, upload
134
+ // the key to EAS, and every subsequent `eas update` signs locally
135
+ // before publishing. A compromised CDN or EAS account cannot ship
136
+ // arbitrary JS. Until the cert exists the block stays off and
137
+ // updates ship unsigned (fine for prototyping; not for production).
138
+ // https://docs.expo.dev/eas-update/code-signing/
139
+ ...(existsSync(resolve(process.cwd(), "certs", "certificate.pem"))
140
+ ? {
141
+ codeSigningCertificate: "./certs/certificate.pem",
142
+ codeSigningMetadata: { keyid: "main", alg: "rsa-v1_5-sha256" },
143
+ }
144
+ : {}),
99
145
  },
100
146
  ios: {
101
147
  supportsTablet: false,
@@ -108,6 +154,17 @@ export default ({ config }: ConfigContext): ExpoConfig => {
108
154
  ITSAppUsesNonExemptEncryption: false,
109
155
  LSApplicationQueriesSchemes: ["mailto", "tel", "sms", "itms-apps"],
110
156
  },
157
+ // App Attest entitlement. `@expo/app-integrity` calls the
158
+ // DCAppAttestService API; iOS rejects unentitled access. The value
159
+ // `production` keeps every signed/distributed build (TestFlight,
160
+ // App Store) in the production AAGUID. Local debug builds with the
161
+ // Xcode debugger attached attest against the development AAGUID
162
+ // automatically without changing this value, so the same entry
163
+ // works across simulator-impossible and real-device paths.
164
+ // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.devicecheck.appattest-environment
165
+ entitlements: {
166
+ "com.apple.developer.devicecheck.appattest-environment": "production",
167
+ },
111
168
  associatedDomains: [
112
169
  `applinks:${process.env.EXPO_PUBLIC_CONVEX_SITE_URL?.replace(/^https?:\/\//, "") ?? "example.convex.site"}`,
113
170
  ],
@@ -138,9 +195,8 @@ export default ({ config }: ConfigContext): ExpoConfig => {
138
195
  [
139
196
  "expo-router",
140
197
  {
141
- asyncRoutes: {
142
- default: "development",
143
- },
198
+ sitemap: false,
199
+ headOrigin: process.env.EXPO_PUBLIC_HEAD_ORIGIN,
144
200
  },
145
201
  ],
146
202
  [
@@ -179,6 +235,8 @@ export default ({ config }: ConfigContext): ExpoConfig => {
179
235
  faceIDPermission: "Allow $(PRODUCT_NAME) to use Face ID to confirm sensitive actions.",
180
236
  },
181
237
  ],
238
+ "expo-image",
239
+ "expo-sqlite",
182
240
  "expo-system-ui",
183
241
  "expo-secure-store",
184
242
  "expo-web-browser",
@@ -191,6 +249,20 @@ export default ({ config }: ConfigContext): ExpoConfig => {
191
249
  extra: {
192
250
  ...config.extra,
193
251
  support: SUPPORT,
252
+ // Inject the resolved projectId so eas-cli commands that read
253
+ // `extra.eas.projectId` directly (e.g. `eas project:info`) see the
254
+ // env-var fallback too, not just `updates.url`. Existing
255
+ // `config.extra.eas` keys (if any) survive the merge. projectId stays
256
+ // in fingerprint parity because local and EAS both resolve it from the
257
+ // plaintext `EAS_PROJECT_ID` env var, so no `sourceSkips` are needed.
258
+ ...(projectId
259
+ ? {
260
+ eas: {
261
+ ...(config.extra as { eas?: Record<string, unknown> } | undefined)?.eas,
262
+ projectId,
263
+ },
264
+ }
265
+ : {}),
194
266
  },
195
267
  experiments: {
196
268
  typedRoutes: true,
@@ -1,11 +1,3 @@
1
1
  {
2
- "expo": {
3
- "extra": {
4
- "router": {
5
- "asyncRoutes": {
6
- "default": "development"
7
- }
8
- }
9
- }
10
- }
2
+ "expo": {}
11
3
  }
@@ -9,6 +9,9 @@
9
9
  */
10
10
 
11
11
  import type * as admin from "../admin.js";
12
+ import type * as appAttest from "../appAttest.js";
13
+ import type * as appAttestStore from "../appAttestStore.js";
14
+ import type * as apple from "../apple.js";
12
15
  import type * as auth from "../auth.js";
13
16
  import type * as constants from "../constants.js";
14
17
  import type * as crons from "../crons.js";
@@ -17,10 +20,13 @@ import type * as env from "../env.js";
17
20
  import type * as errors from "../errors.js";
18
21
  import type * as functions from "../functions.js";
19
22
  import type * as http from "../http.js";
23
+ import type * as log from "../log.js";
24
+ import type * as pushSender from "../pushSender.js";
20
25
  import type * as pushTokens from "../pushTokens.js";
21
26
  import type * as rateLimit from "../rateLimit.js";
22
27
  import type * as users from "../users.js";
23
28
  import type * as validators from "../validators.js";
29
+ import type * as webhook from "../webhook.js";
24
30
 
25
31
  import type {
26
32
  ApiFromModules,
@@ -30,6 +36,9 @@ import type {
30
36
 
31
37
  declare const fullApi: ApiFromModules<{
32
38
  admin: typeof admin;
39
+ appAttest: typeof appAttest;
40
+ appAttestStore: typeof appAttestStore;
41
+ apple: typeof apple;
33
42
  auth: typeof auth;
34
43
  constants: typeof constants;
35
44
  crons: typeof crons;
@@ -38,10 +47,13 @@ declare const fullApi: ApiFromModules<{
38
47
  errors: typeof errors;
39
48
  functions: typeof functions;
40
49
  http: typeof http;
50
+ log: typeof log;
51
+ pushSender: typeof pushSender;
41
52
  pushTokens: typeof pushTokens;
42
53
  rateLimit: typeof rateLimit;
43
54
  users: typeof users;
44
55
  validators: typeof validators;
56
+ webhook: typeof webhook;
45
57
  }>;
46
58
 
47
59
  /**
@@ -1,8 +1,3 @@
1
- /**
2
- * Admin actions for fixture / review accounts. Internal-only, never exposed
3
- * to client code. Run via `bunx convex run admin:<fn>` (or your PM's dlx).
4
- */
5
-
6
1
  import { v } from "convex/values";
7
2
 
8
3
  import { components } from "./_generated/api";
@@ -11,9 +6,6 @@ import { createAuth } from "./auth";
11
6
  import { rateLimiter, type RateLimitName } from "./rateLimit";
12
7
 
13
8
  /**
14
- * Create a fully verified review account. Used by `setup:review-account`
15
- * to seed Apple's App Review with a working sign-in.
16
- *
17
9
  * Idempotent: if the user already exists, just re-asserts emailVerified=true.
18
10
  * Does NOT rotate the password on re-run, delete the user from the dashboard
19
11
  * first if you need a fresh password.
@@ -83,11 +75,6 @@ export const createReviewAccount = internalAction({
83
75
  },
84
76
  });
85
77
 
86
- /**
87
- * Reset a rate-limit bucket. Run from the dashboard:
88
- * `bunx convex run admin:resetRateLimit '{"name":"avatarUpload","key":"<userId>"}'`
89
- * Omit `key` to reset the shared bucket.
90
- */
91
78
  export const resetRateLimit = internalMutation({
92
79
  args: { name: v.string(), key: v.optional(v.string()) },
93
80
  returns: v.object({