Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,23 @@ bun lint # turbo lint (Biome)
bun run fix # biome check --write .
```

To run the app, use the dev-client workflow (not Expo Go). Restart Metro after any `EXPO_PUBLIC_*` change:
To run the app, use the dev-client workflow (not Expo Go). `apps/mobile` is the Expo project root:

```sh
cd apps/mobile && bun ios
cd apps/mobile
bun expo start # existing development client
bun ios # rebuild and run iOS
bun android # rebuild and run Android
bunx expo-doctor
```

Restart Metro after any `EXPO_PUBLIC_*` change. Rebuild the development client after adding a native dependency or changing a config plugin. Run all EAS commands from `apps/mobile`; `eas.json` and `.eas/workflows/` live there.

### Things to know before touching dependencies

- **`tooling/pins.json` is the single source of truth** for native-coupled versions (Expo SDK, React Native, React, and related native modules). Update pins there first, then mirror them in package manifests. Do not bump `react-native-worklets` past exactly `0.10.0` or downgrade `react-native-reanimated` below `^4.5.1` — both pins are deliberate.
- **`bunfig.toml` forces the hoisted linker.** Metro requires it; don't change the linker setting.
- Install Expo/native dependencies from `apps/mobile` with `bun expo install <package>` so Expo can select and check SDK-compatible versions.
- Use `bun` for everything — installing, running scripts, and creating branches of work. Don't introduce npm/yarn/pnpm lockfiles.

## Pull Request Guidelines
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ios/
android/
dist
.env*.local
credentials.json
.DS_Store
*.log
.turbo
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Production-grade template for Expo apps. Bun workspaces + Turborepo monorepo. On
- `bun install` — always bun, never npm/yarn/pnpm. `bunfig.toml` forces the hoisted linker (Metro cannot resolve through bun's isolated store — do not remove).
- `bunx turbo lint typecheck test` — the full gate; CI runs exactly this. All three must be green before any commit.
- Per package: `bunx vitest run` (83+ tests), `bunx tsc --noEmit`, `bunx biome check --write <paths>`.
- App: `cd apps/mobile && bun ios` / `bun android` — development builds only; this template does not run in Expo Go (native deps: Unistyles/Nitro, ClerkKit, Sentry, RevenueCat).
- Expo app root: `apps/mobile`. Run `bun expo start`, `bun ios`, `bun android`, `bunx expo-doctor`, and all `eas` commands from there. Development builds only; this template does not run in Expo Go (native deps: Unistyles/Nitro, ClerkKit, Sentry, RevenueCat).
- CLI: `bunx tsup` builds `scripts/index.ts` → `dist/index.js`. Test locally: `node dist/index.js init test-app --template <this-repo-path> --yes ...` (see `--help` for non-interactive flags).

## Using this CLI as an agent
Expand All @@ -30,8 +30,10 @@ For agents inside this template repo exercising the scaffold flow end-to-end (th

## Architecture rules

- **Vendor quarantine**: every third-party service lives behind `@repo/<vendor>` with `keys.ts` (zod schema), a provider/client entry, and inert-when-unset behavior (no key → no-op + at most ONE `console.info`; never throw, never warn-spam). App code never imports vendor SDKs directly (e.g. `useSignIn` comes from `@repo/auth`, not `@clerk/expo`).
- **Vendor quarantine**: every third-party service lives behind `@repo/<vendor>` with `keys.ts` (zod schema), a provider/client entry, and inert-when-unset behavior (no key → no-op + at most ONE `console.info`; never throw, never warn-spam). App code never imports vendor SDKs directly (e.g. `useSignIn` comes from `@repo/auth`, not `@clerk/expo`). Native/runtime SDK versions are declared in `apps/mobile/package.json` for Expo tooling and consumed as peer dependencies by wrappers.
- **Env**: **only** `apps/mobile/.env.local` is loaded (Expo project root = `apps/mobile/`, next to `app.json`). Repo-root `.env*` is a trap — Metro ignores it. Template: `apps/mobile/.env.example` → copy to `apps/mobile/.env.local`. `EXPO_PUBLIC_*` only in client code; validated once at boot via `@repo/env` `composeEnv` in `apps/mobile/src/env.ts`. Metro statically inlines `process.env.EXPO_PUBLIC_X` member expressions — always read env via static member access; restart Metro after changing any `EXPO_PUBLIC_*` value. Real secrets live only in Supabase secrets / EAS env, never client files. Never commit `.env.local`; never put real values in `.env.example`.
- **Native lifecycle**: JavaScript changes need a Metro reload; `EXPO_PUBLIC_*` changes need a Metro restart; native dependency or config-plugin changes need a rebuilt development client (`cd apps/mobile && bun ios` / `bun android`). Metro cannot add a native module to an existing binary.
- **EAS**: `apps/mobile/eas.json`, `apps/mobile/.eas/workflows/`, and future `credentials.json` live in the Expo app root. Run EAS commands from `apps/mobile`; configure the Expo GitHub App base directory as `apps/mobile`.
- **Auth**: Clerk Core 3 result-object API (`signIn.emailCode.sendCode/verifyCode`, `finalize()` — methods return `{ error }`, they don't throw). Supabase runs in third-party-auth mode: the Clerk JWT rides every request via the `accessToken` getter; RLS policies key on `auth.jwt()->>'sub'`. Route gating is declarative `Stack.Protected` in `apps/mobile/src/app/_layout.tsx` — every unauthenticated screen MUST be declared inside the `!isSignedIn` guard or users get stranded on it after sign-in (this bug shipped once; don't reintroduce it).
- **Styling**: Unistyles v3 tokens only (`packages/design-system/src/tokens.ts`) — no hardcoded hex in app code (theme has `danger`, `accent`, etc.). `index.ts` imports unistyles config BEFORE `expo-router/entry`; that load order is correctness, not style. UI is deliberately grayscale; accent tokens exist but stay unused in the demo.
- **Glass**: all glass uses system APIs (`expo-glass-effect` GlassView, `@expo/ui` SwiftUI `glassEffect`) gated behind `isLiquidGlassAvailable()` with fill/hairline fallbacks. Never fake glass with translucent washes.
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,21 +175,33 @@ Native-coupled dependency versions live in `tooling/pins.json` as the single sou
- `react-native-worklets` is pinned **exactly** to `0.10.0` (Expo's own pin — 0.10.1 crashes at launch)
- `react-native-reanimated` is a deliberate override at `^4.5.1`

Native/runtime SDKs are declared directly in `apps/mobile/package.json` so `expo install --check`, Expo Doctor, and autolinking see the complete native surface. `@repo/*` wrappers consume those SDKs as peer dependencies; application code still imports only the wrapper APIs.

### Known gotchas

- **Hoisted linker required** — `bunfig.toml` forces Bun's hoisted linker because Metro cannot resolve modules through Bun's default isolated layout. Don't remove it.
- **iOS deployment target 17.0** — required by `@clerk/expo`, set via `expo-build-properties`.
- **Dev client, not Expo Go** — this template does not run in Expo Go, by design. Production dependencies ship native code that Go's sandbox doesn't include: react-native-unistyles v3 (Nitro Modules), @clerk/expo (ClerkKit / native Google Sign-In), @sentry/react-native, react-native-purchases — plus config Go can't honor (iOS 17 deployment target, config plugins). Run a development build instead: `bun ios` / `bun android` compiles the dev client once, and everything after is the familiar Metro hot-reload workflow.
- **Dev client, not Expo Go** — this template declares `expo-dev-client` and does not run in Expo Go, by design. Production dependencies ship native code that Go's sandbox doesn't include: react-native-unistyles v3 (Nitro Modules), @clerk/expo (ClerkKit / native Google Sign-In), @sentry/react-native, react-native-purchases — plus config Go can't honor (iOS 17 deployment target, config plugins). From `apps/mobile`, `bun ios` / `bun android` rebuilds the native client; `bun expo start` starts Metro for an existing client.

## Scripts

Run from the repository root:
Repository tasks run from the repository root:

```sh
bun install
bunx turbo lint typecheck test
bunx tsup # build create-expo-forge CLI
```

Expo and EAS tasks run from the Expo app root:

```sh
bun dev # turbo dev
bun typecheck # turbo typecheck
bun lint # turbo lint
bun run fix # biome check --write .
cd apps/mobile
bun expo start # Metro for the existing dev client
bun ios # rebuild and run iOS
bun android # rebuild and run Android
bunx expo-doctor
eas init # all EAS commands run here
```

Linting and formatting are handled by [Biome](https://biomejs.dev) 2.
Expand All @@ -198,12 +210,12 @@ Linting and formatting are handled by [Biome](https://biomejs.dev) 2.

Every push to `main` and every pull request runs `.github/workflows/ci.yml`: Bun (version pinned by the `packageManager` field) installs with a frozen lockfile, then `bunx turbo lint typecheck test` fans out across all workspaces — Biome lint, `tsc --noEmit`, and Vitest suites — with the Turborepo cache persisted between runs.

Two [EAS Workflows](https://docs.expo.dev/eas/workflows/) live in `.eas/workflows/`:
Two [EAS Workflows](https://docs.expo.dev/eas/workflows/) live in `apps/mobile/.eas/workflows/`, beside the app's `eas.json`:

- `create-production-builds.yml` — manual trigger (`eas workflow:run create-production-builds.yml`), builds iOS + Android with the `production` profile
- `create-production-builds.yml` — manual trigger from `apps/mobile` (`eas workflow:run create-production-builds.yml`), builds iOS + Android with the `production` profile
- `publish-preview-update.yml` — on push to `main`, publishes an OTA update to the `preview` branch

They are syntax-complete but dormant until the project is linked to EAS. One-time activation: run `eas init` (keep the committed root `eas.json` profiles — or merge if `eas init` writes a new one), connect the GitHub repo to the EAS project on [expo.dev](https://expo.dev) so push triggers fire, and add an `EXPO_TOKEN` [access token](https://expo.dev/settings/access-tokens) as a repo secret for any CI-driven `eas` CLI calls. The `.eas/` workflows directory must sit next to `eas.json` at the repo root.
They are syntax-complete but dormant until the project is linked to EAS. Expo's monorepo project root is `apps/mobile`, so run `eas init`, `eas build`, `eas update`, and workflow commands from there. Keep `eas.json`, future `credentials.json`, and `.eas/workflows/` in that app root. Configure the Expo GitHub App base directory as `apps/mobile`, then add an `EXPO_TOKEN` [access token](https://expo.dev/settings/access-tokens) as a repository secret for CI-driven EAS commands.

## Roadmap

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Manually triggered production builds for both platforms.
# Run with: eas workflow:run create-production-builds.yml
# Run from apps/mobile: eas workflow:run create-production-builds.yml
# Requires the project to be linked to EAS (`eas init`) — see README "CI".
name: Create production builds

Expand Down
File renamed without changes.
11 changes: 11 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"main": "index.ts",
"scripts": {
"start": "expo start",
"prebuild": "expo prebuild",
"doctor": "bunx expo-doctor",
"ios": "expo run:ios",
"android": "expo run:android",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"test": "vitest run"
},
"dependencies": {
"@clerk/expo": "^3.7.0",
"@expo/ui": "~57.0.3",
"@repo/analytics": "workspace:*",
"@repo/auth": "workspace:*",
Expand All @@ -22,21 +25,29 @@
"@repo/observability": "workspace:*",
"@repo/payments": "workspace:*",
"@repo/updates": "workspace:*",
"@sentry/react-native": "~7.11.0",
"@shopify/flash-list": "2.0.2",
"expo": "~57.0.2",
"expo-auth-session": "~57.0.1",
"expo-build-properties": "~57.0.0",
"expo-constants": "~57.0.3",
"expo-dev-client": "~57.0.5",
"expo-device": "~57.0.0",
"expo-glass-effect": "~57.0.0",
"expo-notifications": "~57.0.3",
"expo-router": "~57.0.3",
"expo-secure-store": "~57.0.0",
"expo-status-bar": "~57.0.0",
"expo-symbols": "~57.0.0",
"expo-system-ui": "~57.0.0",
"expo-updates": "~57.0.6",
"expo-web-browser": "~57.0.0",
"posthog-react-native": "^4.54.4",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-gesture-handler": "~2.32.0",
"react-native-nitro-modules": "^0.36.1",
"react-native-purchases": "^10.4.1",
"react-native-reanimated": "^4.5.1",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
Expand Down
10 changes: 7 additions & 3 deletions apps/mobile/src/app/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,10 @@ export default function WelcomeScreen() {
pressed ? styles.buttonPressed : null,
]}
>
<Text style={styles.primaryIcon}>{""}</Text>
<Image
source={require("../../assets/images/icons/apple.png")}
style={styles.primaryIcon}
/>
<Text style={styles.primaryLabel}>Continue with Apple</Text>
</Pressable>
<Pressable
Expand Down Expand Up @@ -356,9 +359,10 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 48,
},
primaryIcon: {
height: 24,
height: 18,
resizeMode: "contain",
tintColor: theme.colors.onInk,
width: 24,
width: 18,
},
primaryLabel: {
...theme.type.body,
Expand Down
Loading
Loading