diff --git a/.changeset/config.json b/.changeset/config.json index e4764606..2619e13d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -13,6 +13,7 @@ "ignore": [ "web", "mobile-app", + "@beakerstack/billing", "@beakerstack/shared", "@beakerstack/shared-tests" ], diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..b17aaea3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,34 @@ +name: Bug report +description: Something broken in the template or published packages +title: "[bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting. Search [existing issues](https://github.com/Artificer-Innovations/BeakerStack/issues) first. + For usage questions, prefer [Discussions](https://github.com/Artificer-Innovations/BeakerStack/discussions). + - type: textarea + id: description + attributes: + label: What happened? + description: What did you expect vs what you saw? + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + placeholder: "1. npm run setup\n2. ..." + validations: + required: true + - type: input + id: version + attributes: + label: Template tag or commit + placeholder: "2026.001 or abc1234" + - type: input + id: environment + attributes: + label: Environment + placeholder: "Node 20, macOS, local Supabase" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..71a9f00c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions (Discussions) + url: https://github.com/Artificer-Innovations/BeakerStack/discussions + about: Usage help and design questions + - name: Security report + url: https://github.com/Artificer-Innovations/BeakerStack/blob/main/SECURITY.md + about: Report vulnerabilities privately — do not file a public issue diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..412bb2ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Propose a change to the upstream template +title: "[feat]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + For large or opinionated changes, open a [Discussion](https://github.com/Artificer-Innovations/BeakerStack/discussions) first. + Stack swaps (e.g. different auth or payment provider) usually belong in forks — see [CONTRIBUTING.md](https://github.com/Artificer-Innovations/BeakerStack/blob/main/CONTRIBUTING.md). + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What are you trying to accomplish? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + validations: + required: false diff --git a/.github/RELEASE_TEMPLATE.md b/.github/RELEASE_TEMPLATE.md new file mode 100644 index 00000000..15a03ab8 --- /dev/null +++ b/.github/RELEASE_TEMPLATE.md @@ -0,0 +1,13 @@ + + +## Summary + +[3–4 sentences: what this release represents, headline capabilities, any breaking fork steps.] + +- **Landing:** https://beakerstack.com +- **Quick start:** https://github.com/Artificer-Innovations/BeakerStack/blob/main/QUICKSTART.md +- **Feedback:** https://github.com/Artificer-Innovations/BeakerStack/discussions + +--- + + diff --git a/.github/workflows/check-merge-strategy.yml b/.github/workflows/check-merge-strategy.yml index 1545d5de..db7e1800 100644 --- a/.github/workflows/check-merge-strategy.yml +++ b/.github/workflows/check-merge-strategy.yml @@ -39,7 +39,7 @@ jobs: Using "Squash and merge" on PRs targeting \`main\` can cause merge conflicts when merging \`develop\` → \`main\` later. - See [ARCHITECTURE.md](../../blob/develop/ARCHITECTURE.md#pull-request-process) for details. + See https://github.com/Artificer-Innovations/BeakerStack/blob/develop/docs/ARCHITECTURE.md#pull-request-process for details. --- diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index fb1af274..75c071d3 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -8,6 +8,7 @@ on: permissions: contents: read id-token: write + deployments: write concurrency: group: production-deploy @@ -146,6 +147,49 @@ jobs: --environment prod \ --region "${AWS_REGION_VALUE}" + - name: Create GitHub Deployment (production) + uses: actions/github-script@v7 + env: + PRODUCTION_URL: ${{ format('https://{0}/', env.DOMAIN) }} + with: + script: | + const environmentUrl = process.env.PRODUCTION_URL; + const logUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.sha, + environment: 'production', + auto_merge: false, + required_contexts: [], + description: 'Production deployment', + }); + if (!deployment.data.id) { + core.warning(`createDeployment returned no id (HTTP ${deployment.status}): ${deployment.data.message ?? JSON.stringify(deployment.data)}`); + return; + } + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'success', + environment_url: environmentUrl, + log_url: logUrl, + description: 'Production deployed', + }); + + - name: Write Actions run summary + shell: bash + env: + PRODUCTION_URL: ${{ format('https://{0}/', env.DOMAIN) }} + run: | + set -euo pipefail + cat >> "${GITHUB_STEP_SUMMARY}" </" + }, + { + "pattern": "^https://deploy\\.yourdomain\\.com" } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41a5e5d5..b03274a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,20 @@ Thank you for helping improve Beaker Stack. +## What belongs upstream + +**Welcome:** bug fixes, documentation, performance improvements to shared logic, opt-in integrations that fit the template’s architecture, and new publishable `@beakerstack/*` packages. + +**Usually not upstream:** swapping core stack choices (e.g. Firebase instead of Supabase, Paddle instead of Stripe, Next.js instead of Vite). Those belong in **downstream forks** where you own the tradeoffs. + +**Larger changes:** open a [Discussion](https://github.com/Artificer-Innovations/BeakerStack/discussions) first — especially features that affect every adopter’s fork or CI secrets. Link the Discussion in your PR. + +Issues: use [GitHub Issues](https://github.com/Artificer-Innovations/BeakerStack/issues) with the appropriate template (bug vs feature). Questions and usage help fit Discussions (Q&A). + ## Workflow +**Package manager:** This repo uses **npm** exclusively — `pnpm` and `yarn` are not supported. The `engines` field in `package.json` requires `npm >= 9.0.0` and all CI runs use `npm ci`. Both `pnpm-lock.yaml` and `yarn.lock` are listed in `.gitignore` to prevent accidental commits. + 1. Create a branch from `develop` (or the branch your team uses for integration). 2. Make focused changes; match existing style, types, and test patterns. 3. Run checks locally: @@ -16,6 +28,25 @@ Thank you for helping improve Beaker Stack. 4. Open a pull request with a clear description of **what** changed and **why**. +## Commits and release notes + +**Conventional commits** are required on the integration branch. [git-cliff](https://github.com/orhun/git-cliff) uses them for **template** CalVer release notes. + +Examples: + +- `feat: add usage summary to billing dashboard` +- `fix: correct PR preview redirect for trailing slash` +- `docs: clarify OAuth redirect URLs in OAUTH guide` +- `chore: bump Supabase CLI in CI` + +**Template-only changes** (anything outside publishable `packages/*`) need a conventional commit only — no Changeset. + +**Publishable package changes** (`@beakerstack/test-utils` today) require a Changeset — see below. + +**Mixed PRs** (template + package): use a conventional commit message and include a Changeset if any publishable package files changed. + +Versioning overview: [docs/VERSIONING.md](docs/VERSIONING.md). + ## Pre-commit hook The repository uses [lint-staged](https://github.com/lint-staged/lint-staged) and [Husky](https://typicode.github.io/husky/) to enforce code quality on every commit. @@ -117,6 +148,10 @@ See `packages/test-utils/` for a minimal working example. - **Topic index:** [docs/README.md](docs/README.md). - Before merging doc-only changes, run **`npm run docs:linkcheck`** (uses [markdown-link-check](https://github.com/tcort/markdown-link-check) with [.markdown-link-check.json](.markdown-link-check.json)). +## Template releases (maintainers) + +CalVer template releases use [.github/workflows/release-template.yml](.github/workflows/release-template.yml). Before publishing, paste the prose block from [.github/RELEASE_TEMPLATE.md](.github/RELEASE_TEMPLATE.md) **above** the git-cliff-generated notes in the GitHub Release body. + ## Questions -Use GitHub Issues or your team's usual channel. For OAuth-specific setup, start with [docs/oauth/README.md](docs/oauth/README.md). +Use [Discussions](https://github.com/Artificer-Innovations/BeakerStack/discussions) for usage questions and [Issues](https://github.com/Artificer-Innovations/BeakerStack/issues) for bugs. OAuth setup: [docs/OAUTH.md](docs/OAUTH.md). diff --git a/QUICKSTART.md b/QUICKSTART.md index 910e7b03..68d65501 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -58,7 +58,7 @@ You should see a banner and a prompt similar to: ```text ======================================================================== + + -+ BeakerStack + ++ Beaker Stack + + + + Choose (1) local setup or (2) full cloud wizard. + + + @@ -164,6 +164,6 @@ Work through these when you are ready; they are intentionally dense. - [ ] Firebase / Google Cloud OAuth clients; `google-services.json` where required. - [ ] Supabase Auth Google provider + redirect URLs: [docs/supabase-preview-setup.md](docs/supabase-preview-setup.md), [docs/supabase-staging-production-setup.md](docs/supabase-staging-production-setup.md). -**Deep dives:** [docs/pr-preview-setup.md](docs/pr-preview-setup.md), [ARCHITECTURE.md](ARCHITECTURE.md), [docs/README.md](docs/README.md). +**Deep dives:** [docs/pr-preview-setup.md](docs/pr-preview-setup.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/README.md](docs/README.md). Do not commit `.env*` files; sensitive paths are listed in [.cursorignore](.cursorignore). diff --git a/README.md b/README.md index 88dda263..9c2ec64c 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,136 @@ # Beaker Stack -Beaker Stack is a **monorepo template** for building **Supabase-backed** applications that ship on **web (React + Vite)** and **mobile (React Native + Expo)** at the same time: **one product, two surfaces, one backend, one deploy pipeline.** +**Ship your SaaS faster.** -Most full-stack templates optimize for getting an app running. Beaker Stack optimizes for the **pipeline around it**—local parity, PR previews, staging from `develop`, production from `main`, and shared business logic so web and mobile do not drift. +[![Test](https://github.com/Artificer-Innovations/BeakerStack/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/Artificer-Innovations/BeakerStack/actions/workflows/test.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](package.json) +[![Release](https://img.shields.io/github/v/release/Artificer-Innovations/BeakerStack?label=template&display_name=release)](https://github.com/Artificer-Innovations/BeakerStack/releases/tag/2026.001) +[![Discussions](https://img.shields.io/github/discussions/Artificer-Innovations/BeakerStack)](https://github.com/Artificer-Innovations/BeakerStack/discussions) -> **What you end up with:** **Production** at `https://yourdomain.com`, **staging** at `https://staging.yourdomain.com`, and **every pull request** getting an **ephemeral web URL** at `https://deploy.yourdomain.com/pr-/` (path-based previews on AWS). When CI secrets are configured, **Expo EAS** update channels align to preview, staging, and production. Env keys and GitHub Actions secrets follow a **single manifest** so pipelines trust the same layout you use locally. It is the small-team version of infrastructure many solo stacks skip—without giving up a sane SDLC. +Beaker Stack is the foundation we wished existed for shipping real B2C SaaS: **one product, two surfaces, one backend, one deploy pipeline**. Web and mobile from a shared codebase, auth and billing past the demo, a three-environment pipeline that catches mistakes before production, and a structure that AI coding agents can modify without breaking things. -## Why this shape - -**1. SDLC from day one, not later.** The usual small-project arc is: move fast, add CI when it hurts, add staging when it really hurts, skip PR previews entirely. That is cheap on day one and expensive on day ninety—manual deploys, no rollback story, and a “staging” environment that is really hope. Beaker Stack assumes the **pipeline is part of the product**: developers test against **local** Supabase; **every PR** gets a **deploy URL** (and optional mobile channel) so the change runs in an environment close to production; **merged work on `develop`** flows to **staging**; **production** is fed by a **deliberate `develop` → `main`** promotion, not ad-hoc pushes. - -**2. Review discipline: the artifact under review is the running change, not only the diff.** Every PR gets a **URL** so reviewers answer whether the behavior works **in situ**, without cloning the branch. That catches integration issues before merge. There is a one-time cost to wire DNS and secrets; **per-PR marginal cost stays low** once the stack exists. - -**3. Shared code as discipline, not a headline percentage.** Most teams end up with a React tree and a React Native tree that **drift**. Putting business logic, validation, and types in **`packages/shared`** is not mainly a reuse metric—it is a rule that **one product on two surfaces** stays true **by construction**, not aspirationally. +React + Vite on the web. React Native + Expo on mobile. Supabase underneath. Stripe when you take money. -## Why the setup wizard exists +![Beaker Stack PR checks, setup wizard, and mobile app](docs/images/readme/hero.png) -Standing up Route 53, ACM, three Supabase tiers, EAS, and GitHub secrets **by hand** would contradict the claim that small teams can afford this shape. **`npm run setup`** (local path: Docker Supabase, `.env.local`, type generation) and **`npm run setup:full`** (remote resources, optional AWS bootstrap, optional `gh` sync) exist **because** the thesis is that ceremony is cheap **when the template does the work on day one**. One command (plus honest prerequisites in [QUICKSTART.md](QUICKSTART.md)) is how “SDLC from day one” stops being aspirational. +[beakerstack.com](https://beakerstack.com) · [Quick start](QUICKSTART.md) · [Releases](https://github.com/Artificer-Innovations/BeakerStack/releases) -**Who it is for:** Teams whose bottleneck is **shipping safely** (reviews, previews, staged promotion) at least as much as raw feature throughput—and who want Supabase plus **paired web and mobile** without two divergent codebases. - -**How this differs from app-first starters (e.g. T3, create-t3-turbo, default Expo templates):** Those optimize for **getting an app running**. Beaker Stack optimizes for the **pipeline around the app**: local parity, PR-in-situ review, staging from `develop`, production from `main`, and secrets/env layout that CI can trust. If you only need one surface and a single deploy button, a simpler template is the right trade. If you want **small-team SDLC without the usual “add it when we need it” regret**, this shape is the bet. +## Why this shape -The specific machinery (tiered Supabase, AWS static hosting with PR paths, EAS channels, and the `setup-manifest` map for Actions secrets) is listed below. **That list is how the opinions are implemented, not why you would adopt them.** +**The wedge.** A credible B2C app needs web, iOS, Android, auth, billing, entitlements, and a marketing site that ranks — not eventually, from day one. Most templates give you one slice. Stitching the rest is a four-to-six-week integration project before you write product code. Beaker Stack is that work already done, in a shape that still holds when you customize it. -**Stack versions (from the repo today):** web uses **React 18.2** and **Vite 5**; mobile uses **Expo SDK ~50** and **React Native 0.73**; CI uses **Node 20** and **Supabase CLI 2.54.11** (see `.github/workflows`). +**One codebase, not three products that drift.** The slow death of cross-platform apps is duplicated business logic: validation on web, different validation on mobile, billing hooks that only exist on one surface. Shared hooks, shared types, shared billing in `packages/shared` mean one bug, one fix, three platforms. That is not a reuse percentage; it is how you keep “one product” true after the fork. -**Out of the box you get:** Email + Google auth flows (with optional Apple per [docs/oauth/OAUTH_SETUP.md](docs/oauth/OAUTH_SETUP.md)), user profiles + RLS patterns, Maestro-oriented E2E layout, and scripts for local Docker Supabase plus full-cloud bootstrap. +**Three environments because “works on my machine” is not a release strategy.** B2C apps that take payments cannot ship migrations that break production, and stakeholders need to click a feature in a PR, not read a diff. Beaker Stack mirrors your branch model in Supabase — local Docker, shared PR preview DB, staging on `develop`, production on `main` — and deploys path-based web previews so the artifact under review is the running change. ---- +The test pyramid (unit, integration, E2E, database) catches regressions across web and mobile in a single PR, before merge. Optional EAS Update channels align mobile to the same preview → staging → production flow. -**[Get started → QUICKSTART.md](QUICKSTART.md)** — **Use this template**, local “hello world” in minutes, full-cloud checklist when you are ready. +**Built for AI coding agents.** Dozens of full-stack templates exist. Few say plainly: we designed this to be modified by AI coding agents, and here is how. Typed landing and billing configs fail loudly instead of silently rendering wrong copy. Schema-generated types tie the database to TypeScript so an agent cannot drift from RLS reality. Tests are colocated with a documented decision matrix so an agent knows where new coverage belongs. Monorepo boundaries scope changes. When an agent still ships something broken, PR previews and CI are the safety net — not hope. -| Need | Doc | -| --------------------- | ------------------------------------------------------------ | -| Full topic index | [docs/README.md](docs/README.md) | -| Stripe billing setup | [docs/stripe-billing-setup.md](docs/stripe-billing-setup.md) | -| Environments & design | [ARCHITECTURE.md](ARCHITECTURE.md) | -| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | +**Fork the template; update the packages.** Every template fork diverges immediately — that is fine. Beaker Stack does two things about it. **CalVer tags** (`2026.001`, …) mark exact snapshot baselines so you know what you forked and can merge upstream deliberately ([docs/UPGRADING.md](docs/UPGRADING.md)). Reusable pieces ship as **`@beakerstack/*` npm packages** (semver, Changesets) so you can bump test helpers or billing without re-merging the whole monorepo. See [docs/VERSIONING.md](docs/VERSIONING.md). -## Features +## Template + npm packages -- **Monorepo** — `apps/web`, `apps/mobile`, `packages/shared`, `supabase/`. -- **Supabase** — Local Docker + shared preview + staging + production remotes ([ARCHITECTURE.md](ARCHITECTURE.md)). -- **Auth** — Email/password and Google (Supabase + native Google on mobile when configured); user-scoped RLS; extend migrations for org/tenant models if needed. -- **Web (AWS)** — PR previews, staging, production static hosting via CloudFormation helpers under `scripts/pr-preview/`. -- **Mobile (Expo)** — EAS dev client workflow; CI can publish OTA updates per environment when secrets are set. -- **CI/CD** — PR workflow, `develop` → staging, `main` → production ([.github/workflows](.github/workflows)). -- **Setup UX** — `npm run setup` menu and `npm run setup:full` phased wizard (`--dry-run`, `--from=PHASE`); secrets only in gitignored files. +| Distribution | What it is | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------- | +| **Template** | Fork or “Use this template”; CalVer tags + [GitHub Releases](https://github.com/Artificer-Innovations/BeakerStack/releases) | +| **npm packages** | Optional `@beakerstack/*` for existing apps; semver via Changesets | -## Prerequisites (summary) +| Package | Status | Install | +| ------------------------- | ------------------------------------------------------------------------------ | ---------------------------------- | +| `@beakerstack/test-utils` | Published on npm (`0.0.1`; workspace may show `0.0.0` until release PR merges) | `npm i -D @beakerstack/test-utils` | +| `@beakerstack/billing` | In template (npm planned) | Workspace in fork today | +| `@beakerstack/shared` | In template (npm planned) | Workspace in fork today | -- **Local dev:** Node **20** recommended (`>=18` in `package.json`), npm `>=9`, Docker Desktop, Supabase CLI; native toolchains if you build iOS/Android; Maestro for some E2E commands. **Typical time:** about **5–15 minutes** after `npm install` if Docker images are warm (see [QUICKSTART.md](QUICKSTART.md)). -- **Full cloud + CI:** accounts, DNS, ACM, IAM, Supabase PAT, `EXPO_TOKEN`, GitHub secret sync — often **1–3 hours** the first time. Details and checklists: [QUICKSTART.md](QUICKSTART.md) and [docs/reference/github-actions-secrets.md](docs/reference/github-actions-secrets.md). +## What's in the repo -### What is not included (costs and vendor bills) +Reference inventory — the argument is above. -This template wires **real** cloud resources. You pay vendors under **their** pricing, not ours. Expect ongoing charges roughly along these lines (order-of-magnitude; check current pricing): +| Area | What you get | Doc | +| --------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| Apps | Web (Vite), mobile (Expo), shared `packages/*` | [ARCHITECTURE](docs/ARCHITECTURE.md) | +| Auth | Email/password, Google OAuth, optional Apple; RLS-first profiles; schema-generated types | [OAUTH](docs/OAUTH.md) | +| Billing | Stripe subscriptions (not one-off checkout-only), plan gates, customer portal, usage metering | [stripe-billing-setup](docs/stripe-billing-setup.md) | +| Marketing | Config-driven landing; prerendered SEO home (canonical + Open Graph) | [landing README](apps/web/src/components/landing/README.md) | +| CI/CD | Path-based PR previews on AWS (S3/CloudFront), staging on `develop`, production on `main`, EAS channels | [pr-preview-setup](docs/pr-preview-setup.md) | +| Setup | `npm run setup` / `setup:full` (local + optional full cloud) | [QUICKSTART](QUICKSTART.md) | +| Tests | Unit, integration, Maestro E2E, pgTAP DB tests; colocated with decision matrix | [TESTING](docs/TESTING.md) | +| Agents | Typed configs, generated DB types, package boundaries, test placement rules — conventions in ARCHITECTURE + TESTING | [ARCHITECTURE](docs/ARCHITECTURE.md), [TESTING](docs/TESTING.md) | -- **Supabase** — Remote projects typically need a **paid** tier for serious staging/production (often on the order of **~$25/month per project** on Pro-class plans; preview can sometimes stay smaller). Local dev stays free in Docker. -- **AWS** — Route 53 hosted zones, ACM (certs are usually free), S3 storage, CloudFront egress, and Lambda@Edge or function charges where the stack uses them—all **usage-based**. -- **Expo / EAS** — Free tier exists; **EAS Update** and **build minutes** can move you to paid plans as usage grows. -- **Google Cloud** — OAuth clients and Firebase-related APIs may incur charges at scale; small teams often stay within free tiers for auth-only usage. +Stack: React 18.2, Vite 5, Expo SDK ~50, React Native 0.73, Node 20 in CI. -If you need a **zero-cloud** path, stay on **local** Supabase and skip `setup:full` until you are ready. +## Screenshots -## Documentation +| Billing UI | Environments pipeline | Web + mobile | +| --------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------- | +| ![Billing plans UI](docs/images/readme/billing.png) | ![Three-environment pipeline](docs/images/readme/environments-pipeline.png) | ![Web and mobile](docs/images/readme/mobile-hero.png) | -- [QUICKSTART.md](QUICKSTART.md) — **Start here** -- [docs/README.md](docs/README.md) — All guides (OAuth, AWS, Supabase, testing) -- [docs/guides/MOBILE.md](docs/guides/MOBILE.md) — Native rebuild and dev-client commands -- [docs/BRANDING.md](docs/BRANDING.md) — Icons and theme -- [docs/TESTING.md](docs/TESTING.md) — Test strategy -- [docs/renaming.md](docs/renaming.md) — Rename the template (also [QUICKSTART.md](QUICKSTART.md) after install) +## Quick start -## Project Structure - -``` -BeakerStack/ -├── apps/ -│ ├── mobile/ # React Native (Expo) -│ └── web/ # React (Vite) web app -├── packages/ -│ └── shared/ # Shared components, hooks, types -├── supabase/ # Database migrations, functions -├── tests/ # Integration & E2E tests -├── scripts/ # Development and setup scripts -└── docs/ # Documentation +```bash +git clone https://github.com//.git +cd +npm install +npm run setup ``` -## Development - -### Useful scripts - -- `npm run setup` — Interactive: local stack (default) or full cloud wizard -- `npm run setup:local` — Local only -- `npm run setup:full` — Cloud provisioning and optional `gh` sync -- `npm run dev:all` — Local Supabase + web + mobile -- `npm run test` / `npm run lint` / `npm run type-check` / `npm run format` +The setup wizard handles Docker Supabase, `.env.local`, and type generation. Full cloud + CI checklist: [QUICKSTART.md](QUICKSTART.md). -### Database +**Node 18+** (`>=18` in `package.json`); **Node 20** matches CI. Docker Desktop + Supabase CLI for local work. -- **`supabase/migrations/` is canonical at the repo root** — run `supabase migration new …`, `supabase start`, and `supabase db reset` from the repository root (mobile developers: see [`apps/mobile/supabase/migrations/README.md`](apps/mobile/supabase/migrations/README.md)). -- `supabase start` / `supabase stop` — Local Supabase -- `npm run gen:types` — TypeScript types from DB -- `supabase db reset` — Reset local DB +### Renaming the template -### Testing - -- `npm run test:unit` — Unit tests (mobile, web, shared, and `scripts/`) -- `npm run test:unit:scripts` — Repo script tests only (`scripts/__tests__/`, Node test runner) -- `npm run test:integration` — Integration tests -- `npm run test:e2e` — E2E (Maestro) -- `npm run test:db` — Database tests - -### Development helper - -- `npm run dev:check` / `npm run dev:clean` / `npm run dev:start` - -### Mobile (from repo root) +```bash +npm run rename -- --from "Beaker Stack" --to "Your Product" --dry-run +``` -- `npm run mobile` / `npm run mobile:ios` / `npm run mobile:android` / `npm run mobile:clean` +[docs/renaming.md](docs/renaming.md) -For **native clean rebuilds**, simulator uninstall, and `prebuild --clean`, see **[docs/guides/MOBILE.md](docs/guides/MOBILE.md)**. +## Project structure -## Deployment (CI/CD) +``` +BeakerStack/ +├── apps/web, apps/mobile +├── packages/shared, packages/billing, packages/test-utils +├── supabase/ # migrations, Edge Functions +├── tests/ # integration & E2E +├── scripts/ # setup, deploy, codegen +└── docs/ +``` -| Trigger | Workflow | -| --------------------- | -------------------------------------------------------------------------- | -| **Pull requests** | [pr-preview-environment.yml](.github/workflows/pr-preview-environment.yml) | -| **Push to `develop`** | [deploy-staging.yml](.github/workflows/deploy-staging.yml) | -| **Push to `main`** | [deploy-production.yml](.github/workflows/deploy-production.yml) | +## Documentation -Details: [docs/pr-preview-setup.md](docs/pr-preview-setup.md). +| Topic | Doc | +| -------------- | ------------------------------------------------------------ | +| First run | [QUICKSTART.md](QUICKSTART.md) | +| All guides | [docs/README.md](docs/README.md) | +| Development | [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | +| Architecture | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | +| Versioning | [docs/VERSIONING.md](docs/VERSIONING.md) | +| OAuth | [docs/OAUTH.md](docs/OAUTH.md) | +| Stripe billing | [docs/stripe-billing-setup.md](docs/stripe-billing-setup.md) | +| Testing | [docs/TESTING.md](docs/TESTING.md) | +| Mobile native | [docs/guides/MOBILE.md](docs/guides/MOBILE.md) | +| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | +| Security | [SECURITY.md](SECURITY.md) | -## Releases and versioning +## Development -Template snapshots are tagged on `main` using **CalVer** (`2026.001`, `2026.002`, …). Each release includes generated notes covering what changed and whether there are any breaking steps. `@beakerstack/*` packages use independent **semver** on npm. +Day-to-day commands: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). Native rebuilds and EAS: [docs/guides/MOBILE.md](docs/guides/MOBILE.md). -| Trigger | Workflow | -| ------------------- | --------------------------------------------------------------------------------------------------------- | -| **Manual dispatch** | [release-template.yml](.github/workflows/release-template.yml) — cuts a new CalVer tag and GitHub Release | +## Releases -See [VERSIONING.md](VERSIONING.md) for what counts as a breaking change and [UPGRADING.md](UPGRADING.md) for how to pull template changes into an existing fork. +Template: CalVer tags (`2026.001`, …), notes from conventional commits — [Releases](https://github.com/Artificer-Innovations/BeakerStack/releases). Packages: independent semver on npm. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). +Bug fixes, docs, agent-friendly structure improvements, and shared packages welcome. For larger changes, open a [Discussion](https://github.com/Artificer-Innovations/BeakerStack/discussions) first. Stack swaps (different auth, payments, or framework) belong in your fork — [CONTRIBUTING.md](CONTRIBUTING.md). ## License [MIT](LICENSE) + +## Built with + +[Supabase](https://supabase.com), [Stripe](https://stripe.com), [React](https://react.dev), [React Native](https://reactnative.dev), [Expo](https://expo.dev), [Vite](https://vitejs.dev). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..80c59ee9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Reporting a vulnerability + +**Do not** open a public GitHub issue for security vulnerabilities. + +Report privately to: + +- **Email:** [security@artificerinnovations.com](mailto:security@artificerinnovations.com) +- **GitHub:** [Private vulnerability reporting](https://github.com/Artificer-Innovations/BeakerStack/security/advisories/new) + +Include a description, steps to reproduce, and impact. We aim to acknowledge reports within a few business days. + +## Scope + +**In scope** + +- Source code in this repository (template apps, `packages/*`, `supabase/` migrations and Edge Functions) +- Published `@beakerstack/*` npm packages maintained from this repo +- RLS policies, auth flows, and billing patterns shipped as part of the template + +**Out of scope** + +- Deployments and infrastructure you operate in your own AWS, Supabase, or Stripe accounts +- Secrets, API keys, or environment configuration you add in forks +- Vulnerabilities in third-party services (Supabase, Stripe, Expo, etc.) — report those vendors directly +- Downstream applications built from forks unless they are clearly attributable to template code as shipped here + +## Template disclaimer + +Beaker Stack is a **starting point**, not a certified secure product. Before production use, review authentication, authorization (especially Row Level Security), billing webhooks, and Edge Function secrets in **your** fork. Customize policies and threat model for your product and compliance needs. + +## Supported versions + +Security fixes are applied on the active development branch and included in subsequent template releases and package semver bumps. Forks are responsible for merging those updates. diff --git a/VERSIONING.md b/VERSIONING.md deleted file mode 100644 index c9a27f19..00000000 --- a/VERSIONING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Versioning - -BeakerStack uses two parallel versioning schemes — one for the monorepo template and one for its published packages. - -## Template releases — `YYYY.NNN` - -Template releases are **monorepo snapshot tags** on `main`. A tag like `2026.003` means: _this is what BeakerStack looked like at that point in time, and it is a good base to fork from._ - -Tags use **CalVer with a zero-padded sequence number** within the year: - -``` -2026.001 first release of 2026 -2026.002 second release of 2026 -2026.013 thirteenth release of 2026 -``` - -`NNN` increments arbitrarily — there is no meaning to how much time passes between releases. - -### What counts as a breaking change - -A template release is **breaking** if adopters who have forked the template need to take manual action before upgrading. That includes: - -- A new required GitHub Actions secret or repository variable -- A renamed or removed secret / variable that CI depends on -- A changed top-level folder structure (e.g. a directory moved or renamed) -- A changed setup script behavior (flags renamed, phases reordered, outputs changed) -- A new migration step needed to align an existing fork with the updated baseline - -Breaking changes are called out explicitly in the release notes generated for that tag. - -### `main` vs. tagged releases - -| Ref | What it is | Recommended for | -| ---------- | ------------------- | ------------------------------------------------------------------- | -| `main` | Current stable HEAD | Following along with active development | -| `2026.NNN` | Snapshot tag | Starting a new fork; upgrading an existing fork in a controlled way | - -If you are forking BeakerStack to build a product, start from a tagged release so your upgrade story is clear from day one. - -## Package releases — semver - -`@beakerstack/*` packages published to npm use standard **semantic versioning** (`MAJOR.MINOR.PATCH`). Each package is versioned independently via changesets. Release notes for package releases live on the package's GitHub Release page. - -Package versions are independent of template CalVer tags. diff --git a/apps/mobile/README.md b/apps/mobile/README.md index f5d11afd..8dd43ff2 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -4,6 +4,6 @@ Most scripts run from the **repository root** (`npm run mobile`, etc.). See [`do ## Local Supabase schema -Database migrations live only under **`supabase/migrations/`** at the repo root. From the BeakerStack checkout root, run `supabase start`, `supabase migration new …`, and `supabase db reset` — do not expect duplicated `.sql` files under [`supabase/migrations/`](./supabase/migrations/) (see the policy note there). +Database migrations live only under **`supabase/migrations/`** at the repo root. From the Beaker Stack checkout root, run `supabase start`, `supabase migration new …`, and `supabase db reset` — do not expect duplicated `.sql` files under [`supabase/migrations/`](./supabase/migrations/) (see the policy note there). Optional [`supabase/config.toml`](./supabase/config.toml) here keeps mobile-oriented auth redirect URLs separate from the root config; it is not the source of migration SQL. diff --git a/apps/mobile/__tests__/screens/HomeScreen.test.tsx b/apps/mobile/__tests__/screens/HomeScreen.test.tsx index 3e07b487..7284a150 100644 --- a/apps/mobile/__tests__/screens/HomeScreen.test.tsx +++ b/apps/mobile/__tests__/screens/HomeScreen.test.tsx @@ -66,7 +66,6 @@ import { AuthProvider } from '@beakerstack/shared/contexts/AuthContext'; import { ProfileProvider } from '@beakerstack/shared/contexts/ProfileContext'; import type { SupabaseClient } from '@supabase/supabase-js'; import { HOME_TITLE, HOME_SUBTITLE } from '@beakerstack/shared/utils/strings'; -import { BRANDING } from '@beakerstack/shared/config/branding'; describe('HomeScreen', () => { let mockSupabaseClient: Partial; @@ -154,151 +153,102 @@ describe('HomeScreen', () => { ); }; - it('renders home screen with title and subtitle', () => { + // Signed-out tests use waitFor because auth.loading starts true (getSession is async) + // and the screen shows a spinner until loading resolves to unauthenticated. + + it('renders home screen with title and subtitle', async () => { const { getAllByText, getByText } = renderWithAuth( , false ); - // Title appears in both header and main content - const titles = getAllByText(HOME_TITLE); - expect(titles.length).toBeGreaterThan(0); - // Check subtitle using the shared string constant + await waitFor(() => { + // Title appears in both header and main content + const titles = getAllByText(HOME_TITLE); + expect(titles.length).toBeGreaterThan(0); + }); expect(getByText(HOME_SUBTITLE)).toBeTruthy(); }); - it('shows sign in button when not authenticated', () => { - const { getAllByText } = renderWithAuth( - , - false - ); - - // Sign In appears in both header and main content - const signInButtons = getAllByText('Sign In'); - expect(signInButtons.length).toBeGreaterThan(0); - }); - - it('shows sign up button when not authenticated', () => { + it('shows sign in button when not authenticated', async () => { const { getAllByText } = renderWithAuth( , false ); - // Sign Up appears in both header and main content - const signUpButtons = getAllByText('Sign Up'); - expect(signUpButtons.length).toBeGreaterThan(0); - }); - - it('shows dashboard link when authenticated', async () => { - const { getByText } = renderWithAuth( - , - true - ); - await waitFor(() => { - expect(getByText('Go To Dashboard')).toBeTruthy(); + // Sign In appears in both header and main content + const signInButtons = getAllByText('Sign In'); + expect(signInButtons.length).toBeGreaterThan(0); }); }); - it('shows profile link when authenticated', async () => { - const { getByText } = renderWithAuth( + it('shows sign up button when not authenticated', async () => { + const { getAllByText } = renderWithAuth( , - true + false ); await waitFor(() => { - expect(getByText('View Profile')).toBeTruthy(); + // Sign Up appears in both header and main content + const signUpButtons = getAllByText('Sign Up'); + expect(signUpButtons.length).toBeGreaterThan(0); }); }); - it('shows navigation header with dashboard and profile links when authenticated', async () => { + it('shows navigation header with sign in and sign up when not authenticated', async () => { const { getAllByText } = renderWithAuth( , - true + false ); - // The header should be visible with "Beaker Stack" text - // Dashboard and Profile are in the user menu dropdown, not directly visible await waitFor(() => { - const beakerStackText = getAllByText(BRANDING.displayName); - expect(beakerStackText.length).toBeGreaterThan(0); + const signInLinks = getAllByText('Sign In'); + const signUpLinks = getAllByText('Sign Up'); + expect(signInLinks.length).toBeGreaterThan(0); + expect(signUpLinks.length).toBeGreaterThan(0); }); }); - it('shows navigation header with sign in and sign up when not authenticated', () => { + it('navigates to Login and Signup from signed-out buttons', async () => { const { getAllByText } = renderWithAuth( , false ); - const signInLinks = getAllByText('Sign In'); - const signUpLinks = getAllByText('Sign Up'); - expect(signInLinks.length).toBeGreaterThan(0); - expect(signUpLinks.length).toBeGreaterThan(0); - }); - - it('has dashboard button that can navigate', async () => { - const { getByText } = renderWithAuth( - , - true - ); - + // Wait for auth to resolve before interacting with buttons await waitFor(() => { - const button = getByText('Go To Dashboard'); - expect(button).toBeTruthy(); + expect(getAllByText('Sign In').length).toBeGreaterThan(0); }); - }); - it('has profile button that can navigate', async () => { - const { getByText } = renderWithAuth( - , - true - ); + const signIns = getAllByText('Sign In'); + fireEvent.press(signIns[signIns.length - 1]); + expect(mockNavigate).toHaveBeenCalledWith('Login'); - await waitFor(() => { - const button = getByText('View Profile'); - expect(button).toBeTruthy(); - }); + const signUps = getAllByText('Sign Up'); + fireEvent.press(signUps[signUps.length - 1]); + expect(mockNavigate).toHaveBeenCalledWith('Signup'); }); - it('navigates to Dashboard when Go To Dashboard is pressed', async () => { - const { getByText } = renderWithAuth( - , - true - ); + it('resets navigation to Dashboard when authenticated', async () => { + renderWithAuth(, true); await waitFor(() => { - expect(getByText('Go To Dashboard')).toBeTruthy(); + expect(mockReset).toHaveBeenCalledWith({ + index: 0, + routes: [{ name: 'Dashboard' }], + }); }); - fireEvent.press(getByText('Go To Dashboard')); - expect(mockNavigate).toHaveBeenCalledWith('Dashboard'); }); - it('navigates to Profile when View Profile is pressed', async () => { + it('shows redirecting indicator when authenticated', async () => { const { getByText } = renderWithAuth( , true ); await waitFor(() => { - expect(getByText('View Profile')).toBeTruthy(); + expect(getByText('Redirecting...')).toBeTruthy(); }); - fireEvent.press(getByText('View Profile')); - expect(mockNavigate).toHaveBeenCalledWith('Profile'); - }); - - it('navigates to Login and Signup from signed-out buttons', () => { - const { getAllByText } = renderWithAuth( - , - false - ); - - const signIns = getAllByText('Sign In'); - fireEvent.press(signIns[signIns.length - 1]); - expect(mockNavigate).toHaveBeenCalledWith('Login'); - - const signUps = getAllByText('Sign Up'); - fireEvent.press(signUps[signUps.length - 1]); - expect(mockNavigate).toHaveBeenCalledWith('Signup'); }); }); diff --git a/apps/mobile/__tests__/screens/billing/BillingScreens.test.tsx b/apps/mobile/__tests__/screens/billing/BillingScreens.test.tsx index a598b9dd..1ec0123b 100644 --- a/apps/mobile/__tests__/screens/billing/BillingScreens.test.tsx +++ b/apps/mobile/__tests__/screens/billing/BillingScreens.test.tsx @@ -118,6 +118,14 @@ jest.mock('@beakerstack/billing/native', () => { }; }); +jest.mock('@beakerstack/shared/components/navigation/AppHeader.native', () => ({ + AppHeader: () => null, +})); + +jest.mock('../../../src/lib/supabase', () => ({ + supabase: {}, +})); + function renderWithNav(ui: React.ReactElement) { return render({ui}); } diff --git a/apps/mobile/src/components/dashboard/BooleanFeatureTiles.tsx b/apps/mobile/src/components/dashboard/BooleanFeatureTiles.tsx index b214559d..7b8d0cce 100644 --- a/apps/mobile/src/components/dashboard/BooleanFeatureTiles.tsx +++ b/apps/mobile/src/components/dashboard/BooleanFeatureTiles.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { useFeature } from '@beakerstack/billing'; +import { colors } from '@beakerstack/shared/theme/colors'; import { beakerstackBillingConfig } from '../../billing/beakerstackBillingConfig'; interface TileProps { @@ -78,7 +79,7 @@ const styles = StyleSheet.create({ sectionTitle: { fontSize: 14, fontWeight: '600', - color: '#111827', + color: colors.textPrimary, marginBottom: 12, }, row: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, @@ -90,12 +91,12 @@ const styles = StyleSheet.create({ padding: 14, }, tileOn: { - borderColor: '#86efac', - backgroundColor: '#f0fdf4', + borderColor: colors.featureOnBorder, + backgroundColor: colors.featureOnBg, }, tileOff: { - borderColor: '#e5e7eb', - backgroundColor: '#f9fafb', + borderColor: colors.border, + backgroundColor: colors.pageBg, }, tileHeader: { flexDirection: 'row', @@ -103,21 +104,21 @@ const styles = StyleSheet.create({ alignItems: 'flex-start', gap: 8, }, - tileLabel: { fontSize: 13, fontWeight: '600', color: '#111827' }, - tilePlan: { marginTop: 2, fontSize: 11, color: '#6b7280' }, + tileLabel: { fontSize: 13, fontWeight: '600', color: colors.textPrimary }, + tilePlan: { marginTop: 2, fontSize: 11, color: colors.textMuted }, iconWrap: { borderRadius: 999, paddingHorizontal: 8, paddingVertical: 4, }, - iconOn: { backgroundColor: '#dcfce7' }, - iconOff: { backgroundColor: '#e5e7eb' }, + iconOn: { backgroundColor: colors.featureOnIconBg }, + iconOff: { backgroundColor: colors.iconOffBg }, iconPlaceholder: { width: 14, height: 14 }, - iconText: { fontSize: 12, fontWeight: '800', color: '#9ca3af' }, - iconTextOn: { color: '#16a34a' }, + iconText: { fontSize: 12, fontWeight: '800', color: colors.textFaint }, + iconTextOn: { color: colors.featureOnIcon }, codeBox: { marginTop: 10, - backgroundColor: '#0f172a', + backgroundColor: colors.codeBg, borderRadius: 6, paddingHorizontal: 10, paddingVertical: 8, @@ -125,8 +126,8 @@ const styles = StyleSheet.create({ codeText: { fontFamily: 'monospace', fontSize: 11, - color: '#cbd5e1', + color: colors.codeText, }, - codeValOn: { color: '#4ade80' }, - codeValOff: { color: '#64748b' }, + codeValOn: { color: colors.codeValTrue }, + codeValOff: { color: colors.codeValFalse }, }); diff --git a/apps/mobile/src/components/dashboard/CollectionDetail.tsx b/apps/mobile/src/components/dashboard/CollectionDetail.tsx index 260b8653..4240b539 100644 --- a/apps/mobile/src/components/dashboard/CollectionDetail.tsx +++ b/apps/mobile/src/components/dashboard/CollectionDetail.tsx @@ -7,6 +7,7 @@ import { useUsage, } from '@beakerstack/billing'; import type { BillingError } from '@beakerstack/billing'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../../lib/supabase'; import { beakerstackBillingConfig, @@ -319,7 +320,7 @@ export function CollectionDetail({ collection, addItem, onActivity }: Props) { const styles = StyleSheet.create({ emptySelect: { paddingVertical: 24, alignItems: 'center' }, - emptySelectText: { fontSize: 13, color: '#6b7280' }, + emptySelectText: { fontSize: 13, color: colors.textMuted }, headerRow: { flexDirection: 'row', flexWrap: 'wrap', @@ -327,36 +328,36 @@ const styles = StyleSheet.create({ gap: 10, marginBottom: 12, }, - sectionTitle: { fontSize: 14, fontWeight: '600', color: '#111827' }, - sub: { marginTop: 2, fontSize: 11, color: '#6b7280' }, + sectionTitle: { fontSize: 14, fontWeight: '600', color: colors.textPrimary }, + sub: { marginTop: 2, fontSize: 11, color: colors.textMuted }, featureBtns: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, featBtn: { borderRadius: 8, paddingHorizontal: 10, paddingVertical: 8, }, - featBtnAOn: { backgroundColor: '#e0e7ff' }, - featBtnBOn: { backgroundColor: '#f3e8ff' }, - featBtnOff: { backgroundColor: '#f3f4f6' }, - featBtnText: { fontSize: 11, fontWeight: '700', color: '#4338ca' }, - featBtnTextOff: { color: '#9ca3af' }, + featBtnAOn: { backgroundColor: colors.brandLight }, + featBtnBOn: { backgroundColor: colors.featureBOnBg }, + featBtnOff: { backgroundColor: colors.featureOffBg }, + featBtnText: { fontSize: 11, fontWeight: '700', color: colors.brandDark }, + featBtnTextOff: { color: colors.textFaint }, toast: { marginBottom: 10, borderRadius: 8, borderWidth: 1, - borderColor: '#c7d2fe', - backgroundColor: '#eef2ff', + borderColor: colors.indigo[200], + backgroundColor: colors.indigo[50], padding: 10, }, - toastText: { fontSize: 13, color: '#3730a3' }, - err: { marginBottom: 10, fontSize: 13, color: '#dc2626' }, - emptyItems: { fontSize: 13, color: '#6b7280', paddingVertical: 8 }, + toastText: { fontSize: 13, color: colors.indigo[800] }, + err: { marginBottom: 10, fontSize: 13, color: colors.errorIcon }, + emptyItems: { fontSize: 13, color: colors.textMuted, paddingVertical: 8 }, itemList: { gap: 8, marginBottom: 10 }, itemCard: { borderRadius: 10, borderWidth: 1, - borderColor: '#e5e7eb', - backgroundColor: '#f9fafb', + borderColor: colors.border, + backgroundColor: colors.pageBg, padding: 12, }, itemRow: { @@ -366,22 +367,22 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: 8, }, - itemLabel: { fontSize: 13, fontWeight: '600', color: '#1f2937' }, + itemLabel: { fontSize: 13, fontWeight: '600', color: colors.textSecondary }, summarizeBtn: { borderWidth: 1, - borderColor: '#d1d5db', - backgroundColor: '#fff', + borderColor: colors.gray[300], + backgroundColor: colors.cardBg, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8, }, summarizeBtnDisabled: { opacity: 0.5 }, - summarizeBtnText: { fontSize: 11, fontWeight: '600', color: '#374151' }, - itemErr: { marginTop: 6, fontSize: 11, color: '#dc2626' }, + summarizeBtnText: { fontSize: 11, fontWeight: '600', color: colors.textBody }, + itemErr: { marginTop: 6, fontSize: 11, color: colors.errorIcon }, summaryText: { marginTop: 8, fontSize: 11, - color: '#4b5563', + color: colors.textSubtle, lineHeight: 16, }, addItemBtn: { @@ -390,11 +391,11 @@ const styles = StyleSheet.create({ justifyContent: 'center', borderWidth: 2, borderStyle: 'dashed', - borderColor: '#d1d5db', + borderColor: colors.gray[300], borderRadius: 10, paddingVertical: 12, paddingHorizontal: 14, }, - addItemBtnText: { fontSize: 13, fontWeight: '500', color: '#6b7280' }, + addItemBtnText: { fontSize: 13, fontWeight: '500', color: colors.textMuted }, btnDisabled: { opacity: 0.5 }, }); diff --git a/apps/mobile/src/components/dashboard/UsageStrip.tsx b/apps/mobile/src/components/dashboard/UsageStrip.tsx index 9c9cd400..8e39fcb3 100644 --- a/apps/mobile/src/components/dashboard/UsageStrip.tsx +++ b/apps/mobile/src/components/dashboard/UsageStrip.tsx @@ -6,6 +6,7 @@ import { useUsage, } from '@beakerstack/billing'; import type { BillingError } from '@beakerstack/billing'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../../lib/supabase'; import { beakerstackBillingConfig, @@ -201,10 +202,10 @@ const styles = StyleSheet.create({ sectionTitle: { fontSize: 14, fontWeight: '600', - color: '#111827', + color: colors.textPrimary, }, limitBadge: { - backgroundColor: '#fee2e2', + backgroundColor: colors.errorBadgeBg, paddingHorizontal: 8, paddingVertical: 3, borderRadius: 999, @@ -212,42 +213,42 @@ const styles = StyleSheet.create({ limitBadgeText: { fontSize: 11, fontWeight: '600', - color: '#b91c1c', + color: colors.errorTextAlt, }, barTrack: { height: 8, width: '100%', borderRadius: 4, - backgroundColor: '#e5e7eb', + backgroundColor: colors.border, overflow: 'hidden', }, barFill: { height: 8, borderRadius: 4, - backgroundColor: '#4f46e5', + backgroundColor: colors.brand, }, capLine: { marginTop: 6, fontSize: 13, - color: '#6b7280', + color: colors.textMuted, }, actions: { marginTop: 16, flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, btnPrimary: { - backgroundColor: '#4f46e5', + backgroundColor: colors.brand, paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8, }, - btnPrimaryText: { color: '#fff', fontWeight: '600', fontSize: 14 }, + btnPrimaryText: { color: colors.white, fontWeight: '600', fontSize: 14 }, btnDisabled: { opacity: 0.5 }, - limitNote: { fontSize: 14, color: '#6b7280' }, - err: { marginTop: 8, fontSize: 13, color: '#dc2626' }, + limitNote: { fontSize: 14, color: colors.textMuted }, + err: { marginTop: 8, fontSize: 13, color: colors.errorIcon }, results: { marginTop: 16, gap: 8 }, resultItem: { borderRadius: 8, - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, padding: 12, }, - resultTs: { fontSize: 11, color: '#9ca3af', marginBottom: 4 }, - resultBody: { fontSize: 13, color: '#374151' }, + resultTs: { fontSize: 11, color: colors.textFaint, marginBottom: 4 }, + resultBody: { fontSize: 13, color: colors.textBody }, }); diff --git a/apps/mobile/src/screens/DashboardScreen.tsx b/apps/mobile/src/screens/DashboardScreen.tsx index af849d8c..cf93ec29 100644 --- a/apps/mobile/src/screens/DashboardScreen.tsx +++ b/apps/mobile/src/screens/DashboardScreen.tsx @@ -17,6 +17,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useBillingContext, usePlan, useUsage } from '@beakerstack/billing'; import { useAuthContext } from '@beakerstack/shared/contexts/AuthContext'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../lib/supabase'; import { beakerstackBillingConfig, @@ -281,7 +282,7 @@ export default function DashboardScreen({ navigation }: Props) { if (auth.loading) { return ( - + Loading... ); @@ -290,7 +291,7 @@ export default function DashboardScreen({ navigation }: Props) { if (!auth.user) { return ( - + Redirecting... ); @@ -305,7 +306,7 @@ export default function DashboardScreen({ navigation }: Props) { } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#f9fafb' }, + container: { flex: 1, backgroundColor: colors.pageBg }, scrollView: { flex: 1 }, scrollContent: { paddingHorizontal: 16, @@ -319,63 +320,63 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#F9FAFB', + backgroundColor: colors.pageBg, }, - loadingText: { marginTop: 16, fontSize: 16, color: '#6B7280' }, + loadingText: { marginTop: 16, fontSize: 16, color: colors.textMuted }, demoCard: { - backgroundColor: '#fff', + backgroundColor: colors.cardBg, borderRadius: 12, borderWidth: 1, - borderColor: '#e5e7eb', + borderColor: colors.border, padding: 16, marginBottom: 8, }, - cardDemo: { borderStyle: 'dashed', borderColor: '#d1d5db' }, + cardDemo: { borderStyle: 'dashed', borderColor: colors.gray[300] }, badge: { position: 'absolute', right: 12, top: 10, - backgroundColor: '#fef3c7', + backgroundColor: colors.warnBg, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, }, - badgeText: { fontSize: 10, fontWeight: '600', color: '#92400e' }, + badgeText: { fontSize: 10, fontWeight: '600', color: colors.warnText }, demoTitle: { fontSize: 18, fontWeight: '600', - color: '#111827', + color: colors.textPrimary, paddingRight: 100, }, demonstratesLabel: { marginTop: 4, fontSize: 10, fontWeight: '600', - color: '#6b7280', + color: colors.textMuted, textTransform: 'uppercase', letterSpacing: 0.5, }, - demonstratesMono: { fontFamily: 'monospace', fontSize: 12, color: '#374151' }, - demoDesc: { marginTop: 6, fontSize: 14, color: '#4b5563' }, + demonstratesMono: { fontFamily: 'monospace', fontSize: 12, color: colors.textBody }, + demoDesc: { marginTop: 6, fontSize: 14, color: colors.textSubtle }, demoBody: { marginTop: 12 }, - bodyText: { fontSize: 14, color: '#374151' }, + bodyText: { fontSize: 14, color: colors.textBody }, btnRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 8 }, btnSecondary: { borderWidth: 1, - borderColor: '#d1d5db', + borderColor: colors.gray[300], paddingHorizontal: 10, paddingVertical: 6, borderRadius: 6, - backgroundColor: '#fff', + backgroundColor: colors.cardBg, }, - btnSecondaryText: { color: '#374151', fontSize: 12, fontWeight: '500' }, + btnSecondaryText: { color: colors.textBody, fontSize: 12, fontWeight: '500' }, btnDisabled: { opacity: 0.5 }, - muted: { color: '#6b7280', fontSize: 13, marginTop: 4 }, - tinyLegal: { fontSize: 10, color: '#6b7280', marginTop: 8 }, + muted: { color: colors.textMuted, fontSize: 13, marginTop: 4 }, + tinyLegal: { fontSize: 10, color: colors.textMuted, marginTop: 8 }, codeLine: { marginTop: 12, fontSize: 11, fontFamily: 'monospace', - color: '#6b7280', + color: colors.textMuted, }, }); diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 5d25087c..1fe5a304 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -1,15 +1,17 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, SafeAreaView, + ActivityIndicator, } from 'react-native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { HOME_TITLE, HOME_SUBTITLE } from '@beakerstack/shared/utils/strings'; import { useAuthContext } from '@beakerstack/shared/contexts/AuthContext'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../lib/supabase'; type RootStackParamList = { @@ -32,6 +34,26 @@ interface Props { export default function HomeScreen({ navigation }: Props) { const auth = useAuthContext(); + useEffect(() => { + if (!auth.loading && auth.user) { + navigation.reset({ + index: 0, + routes: [{ name: 'Dashboard' }], + }); + } + }, [auth.loading, auth.user, navigation]); + + if (auth.loading || auth.user) { + return ( + + + + {auth.loading ? 'Loading...' : 'Redirecting...'} + + + ); + } + return ( @@ -44,46 +66,19 @@ export default function HomeScreen({ navigation }: Props) { - {auth.user ? ( - // Signed in state - <> - - Logged in as - {auth.user.email} - - - navigation.navigate('Dashboard')} - > - Go To Dashboard - + navigation.navigate('Login')} + > + Sign In + - navigation.navigate('Profile')} - > - View Profile - - - ) : ( - // Signed out state - <> - navigation.navigate('Login')} - > - Sign In - - - navigation.navigate('Signup')} - > - Sign Up - - - )} + navigation.navigate('Signup')} + > + Sign Up + @@ -93,7 +88,7 @@ export default function HomeScreen({ navigation }: Props) { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, }, content: { flex: 1, @@ -108,13 +103,13 @@ const styles = StyleSheet.create({ title: { fontSize: 28, fontWeight: 'bold', - color: '#1f2937', + color: colors.textSecondary, textAlign: 'center', marginBottom: 8, }, subtitle: { fontSize: 16, - color: '#6b7280', + color: colors.textMuted, textAlign: 'center', lineHeight: 24, maxWidth: 300, @@ -123,28 +118,15 @@ const styles = StyleSheet.create({ width: '100%', maxWidth: 300, }, - signedInContainer: { - backgroundColor: '#d1fae5', - borderColor: '#6ee7b7', - borderWidth: 1, - borderRadius: 8, - padding: 12, + loadingContainer: { + flex: 1, + justifyContent: 'center', alignItems: 'center', - marginBottom: 8, - }, - signedInText: { - color: '#065f46', - fontSize: 14, - fontWeight: '500', - }, - signedInEmail: { - color: '#065f46', - fontSize: 16, - fontWeight: '600', - marginTop: 4, + backgroundColor: colors.pageBg, }, + loadingText: { marginTop: 16, fontSize: 16, color: colors.textMuted }, primaryButton: { - backgroundColor: '#3b82f6', + backgroundColor: colors.brand, paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, @@ -157,17 +139,17 @@ const styles = StyleSheet.create({ fontWeight: '600', }, secondaryButton: { - backgroundColor: '#ffffff', + backgroundColor: colors.cardBg, paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, borderWidth: 1, - borderColor: '#3b82f6', + borderColor: colors.brand, alignItems: 'center', marginBottom: 12, }, secondaryButtonText: { - color: '#3b82f6', + color: colors.brand, fontSize: 16, fontWeight: '600', }, diff --git a/apps/mobile/src/screens/LoginScreen.tsx b/apps/mobile/src/screens/LoginScreen.tsx index 8df47782..cdfaebbb 100644 --- a/apps/mobile/src/screens/LoginScreen.tsx +++ b/apps/mobile/src/screens/LoginScreen.tsx @@ -13,6 +13,7 @@ import { import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useAuthContext } from '@beakerstack/shared/contexts/AuthContext'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../lib/supabase'; import { SocialLoginButton } from '../components/SocialLoginButton'; import { useFeatureFlags } from '../config/featureFlags'; @@ -156,7 +157,7 @@ export default function LoginScreen({ navigation }: Props) { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, }, scrollView: { flex: 1, @@ -174,7 +175,7 @@ const styles = StyleSheet.create({ title: { fontSize: 24, fontWeight: 'bold', - color: '#111827', + color: colors.textPrimary, textAlign: 'center', marginBottom: 24, }, @@ -189,17 +190,17 @@ const styles = StyleSheet.create({ dividerLine: { flex: 1, height: 1, - backgroundColor: '#d1d5db', + backgroundColor: colors.gray[300], }, dividerText: { marginHorizontal: 12, - color: '#6b7280', + color: colors.textMuted, fontSize: 14, }, input: { - backgroundColor: '#ffffff', + backgroundColor: colors.cardBg, borderWidth: 1, - borderColor: '#d1d5db', + borderColor: colors.gray[300], borderRadius: 8, paddingVertical: 12, paddingHorizontal: 16, @@ -207,7 +208,7 @@ const styles = StyleSheet.create({ marginBottom: 16, }, loginButton: { - backgroundColor: '#3b82f6', + backgroundColor: colors.brand, paddingVertical: 12, borderRadius: 8, alignItems: 'center', @@ -225,7 +226,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, linkText: { - color: '#3b82f6', + color: colors.brand, fontSize: 14, }, }); diff --git a/apps/mobile/src/screens/ProfileScreen.tsx b/apps/mobile/src/screens/ProfileScreen.tsx index 3dff49aa..ff7c4b0c 100644 --- a/apps/mobile/src/screens/ProfileScreen.tsx +++ b/apps/mobile/src/screens/ProfileScreen.tsx @@ -12,6 +12,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useAuthContext } from '@beakerstack/shared/contexts/AuthContext'; import { useProfileContext } from '@beakerstack/shared/contexts/ProfileContext'; import { Logger } from '@beakerstack/shared/utils/logger'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../lib/supabase'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; // Import Profile Display Components - Metro will automatically resolve .native.tsx files @@ -59,7 +60,7 @@ export default function ProfileScreen({ navigation }: Props) { if (auth.loading) { return ( - + Loading... ); @@ -69,7 +70,7 @@ export default function ProfileScreen({ navigation }: Props) { if (!auth.user) { return ( - + Redirecting... ); @@ -114,7 +115,7 @@ function ProfileScreenContent({ navigation: _navigation }: Props) { {/* Loading State */} {profile.loading && ( - + Loading profile... )} @@ -172,7 +173,7 @@ function ProfileScreenContent({ navigation: _navigation }: Props) { /> ) : ( - + Loading editor... )} @@ -188,18 +189,18 @@ function ProfileScreenContent({ navigation: _navigation }: Props) { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, }, loadingText: { marginTop: 12, fontSize: 16, - color: '#6b7280', + color: colors.textMuted, }, scrollView: { flex: 1, @@ -214,8 +215,8 @@ const styles = StyleSheet.create({ paddingVertical: 48, }, errorCard: { - backgroundColor: '#fef2f2', - borderColor: '#fecaca', + backgroundColor: colors.errorBg, + borderColor: colors.errorBorder, borderWidth: 1, borderRadius: 8, padding: 16, @@ -224,18 +225,18 @@ const styles = StyleSheet.create({ errorTitle: { fontSize: 16, fontWeight: '600', - color: '#991b1b', + color: colors.errorText, marginBottom: 8, }, errorMessage: { fontSize: 14, - color: '#b91c1c', + color: colors.errorTextAlt, }, profileContent: { gap: 16, }, card: { - backgroundColor: '#ffffff', + backgroundColor: colors.cardBg, borderRadius: 8, padding: 16, shadowColor: '#000', @@ -245,7 +246,7 @@ const styles = StyleSheet.create({ elevation: 1, }, editButton: { - backgroundColor: '#4F46E5', + backgroundColor: colors.brand, paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, diff --git a/apps/mobile/src/screens/SignupScreen.tsx b/apps/mobile/src/screens/SignupScreen.tsx index 31f88948..0371d1e3 100644 --- a/apps/mobile/src/screens/SignupScreen.tsx +++ b/apps/mobile/src/screens/SignupScreen.tsx @@ -13,6 +13,7 @@ import { import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useAuthContext } from '@beakerstack/shared/contexts/AuthContext'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; +import { colors } from '@beakerstack/shared/theme/colors'; import { supabase } from '../lib/supabase'; import { SocialLoginButton } from '../components/SocialLoginButton'; import { useFeatureFlags } from '../config/featureFlags'; @@ -174,7 +175,7 @@ export default function SignupScreen({ navigation }: Props) { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f9fafb', + backgroundColor: colors.pageBg, }, scrollView: { flex: 1, @@ -192,7 +193,7 @@ const styles = StyleSheet.create({ title: { fontSize: 24, fontWeight: 'bold', - color: '#111827', + color: colors.textPrimary, textAlign: 'center', marginBottom: 24, }, @@ -207,17 +208,17 @@ const styles = StyleSheet.create({ dividerLine: { flex: 1, height: 1, - backgroundColor: '#d1d5db', + backgroundColor: colors.gray[300], }, dividerText: { marginHorizontal: 12, - color: '#6b7280', + color: colors.textMuted, fontSize: 14, }, input: { - backgroundColor: '#ffffff', + backgroundColor: colors.cardBg, borderWidth: 1, - borderColor: '#d1d5db', + borderColor: colors.gray[300], borderRadius: 8, paddingVertical: 12, paddingHorizontal: 16, @@ -225,7 +226,7 @@ const styles = StyleSheet.create({ marginBottom: 16, }, signupButton: { - backgroundColor: '#3b82f6', + backgroundColor: colors.brand, paddingVertical: 12, borderRadius: 8, alignItems: 'center', @@ -243,7 +244,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, linkText: { - color: '#3b82f6', + color: colors.brand, fontSize: 14, }, }); diff --git a/apps/mobile/src/screens/billing/BillingLayout.tsx b/apps/mobile/src/screens/billing/BillingLayout.tsx index 5d3ac89b..6adef1c9 100644 --- a/apps/mobile/src/screens/billing/BillingLayout.tsx +++ b/apps/mobile/src/screens/billing/BillingLayout.tsx @@ -1,7 +1,7 @@ import React, { type ReactNode } from 'react'; -import { Pressable, ScrollView, Text } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useNavigation } from '@react-navigation/native'; +import { SafeAreaView, ScrollView } from 'react-native'; +import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.native'; +import { supabase } from '../../lib/supabase'; import { BillingTabBar } from './BillingTabBar'; import { billingStyles } from './styles'; @@ -10,25 +10,10 @@ export function BillingLayout({ }: { children: ReactNode; }): React.ReactElement { - const navigation = useNavigation(); return ( - + + - { - const parent = navigation.getParent(); - if (parent?.canGoBack()) { - parent.goBack(); - } else { - navigation.goBack(); - } - }} - > - ← Back - - Billing {children} diff --git a/apps/mobile/src/screens/billing/styles.ts b/apps/mobile/src/screens/billing/styles.ts index 43b42b1b..9d08ae52 100644 --- a/apps/mobile/src/screens/billing/styles.ts +++ b/apps/mobile/src/screens/billing/styles.ts @@ -1,34 +1,29 @@ import { StyleSheet } from 'react-native'; +import { colors } from '@beakerstack/shared/theme/colors'; export const billingColors = { - pageBg: '#f9fafb', - cardBg: '#ffffff', - border: '#e5e7eb', - textPrimary: '#111827', - textMuted: '#6b7280', - indigo: '#4f46e5', - indigoDark: '#4338ca', - errorBg: '#fef2f2', - errorBorder: '#fecaca', - errorText: '#991b1b', - infoBg: '#eff6ff', - infoBorder: '#bfdbfe', - infoText: '#1e3a8a', - warnBg: '#fffbeb', - warnBorder: '#fde68a', - warnText: '#92400e', + pageBg: colors.pageBg, + cardBg: colors.cardBg, + border: colors.border, + rowDivider: colors.rowDivider, + textPrimary: colors.textPrimary, + textMuted: colors.textMuted, + indigo: colors.brand, + indigoDark: colors.brandDark, + errorBg: colors.errorBg, + errorBorder: colors.errorBorder, + errorText: colors.errorText, + infoBg: colors.infoBg, + infoBorder: colors.infoBorder, + infoText: colors.infoText, + warnBg: colors.warnBg, + warnBorder: colors.warnBorder, + warnText: colors.warnText, }; export const billingStyles = StyleSheet.create({ safe: { flex: 1, backgroundColor: billingColors.pageBg }, scrollContent: { padding: 16, paddingBottom: 40 }, - h1: { - fontSize: 22, - fontWeight: '700', - color: billingColors.textPrimary, - marginBottom: 4, - }, - backLink: { color: billingColors.indigo, fontSize: 15, marginBottom: 8 }, card: { backgroundColor: billingColors.cardBg, borderRadius: 12, @@ -77,7 +72,7 @@ export const billingStyles = StyleSheet.create({ alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, - borderBottomColor: '#f3f4f6', + borderBottomColor: billingColors.rowDivider, }, banner: { borderRadius: 8, diff --git a/apps/mobile/supabase/migrations/README.md b/apps/mobile/supabase/migrations/README.md index 228cf591..53014303 100644 --- a/apps/mobile/supabase/migrations/README.md +++ b/apps/mobile/supabase/migrations/README.md @@ -1,6 +1,6 @@ # Schema migrations live at the repository root -SQL migrations are **not** duplicated under this app. Author and apply them only from the BeakerStack repository root: +SQL migrations are **not** duplicated under this app. Author and apply them only from the Beaker Stack repository root: - **Directory:** `supabase/migrations/` (relative to repo root) - **CLI:** Run `supabase start`, `supabase migration new …`, `supabase db reset`, and linked `supabase db push` from the **repository root**, using the root `supabase/config.toml`. diff --git a/apps/mobile/supabase/seed.sql b/apps/mobile/supabase/seed.sql index 804b853e..4728ef02 100644 --- a/apps/mobile/supabase/seed.sql +++ b/apps/mobile/supabase/seed.sql @@ -58,7 +58,7 @@ VALUES NULL, NULL, NULL, - '{"containers_per_account_max": -1, "items_per_container_max": -1, "feature_a": true, "feature_b": true, "feature_c": true}'::jsonb, + '{"containers_per_account_max": -1, "items_per_container_max": -1, "feature_a": true, "feature_b": true}'::jsonb, '{"ai_summarize": -1}'::jsonb, 5, true, diff --git a/apps/web/src/components/billing/PlanCard.web.tsx b/apps/web/src/components/billing/PlanCard.web.tsx index d420f60b..e9bbdd83 100644 --- a/apps/web/src/components/billing/PlanCard.web.tsx +++ b/apps/web/src/components/billing/PlanCard.web.tsx @@ -126,7 +126,12 @@ export function PlanCard({ /** Display price: monthly uses DB cents; annual uses yearly amount from billing-sync (Stripe). */ export function listPriceForPlan(plan: Plan, cadence: 'monthly' | 'annual') { - if (plan.price_cents === 0) return 'US$0'; + if (plan.price_cents === 0) + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, + }).format(0); if (cadence === 'monthly') { return ( new Intl.NumberFormat('en-US', { diff --git a/apps/web/src/components/billing/__tests__/PlanCard.web.test.tsx b/apps/web/src/components/billing/__tests__/PlanCard.web.test.tsx index 1ea3a821..d73296bb 100644 --- a/apps/web/src/components/billing/__tests__/PlanCard.web.test.tsx +++ b/apps/web/src/components/billing/__tests__/PlanCard.web.test.tsx @@ -37,7 +37,7 @@ const proPlan: Plan = { describe('listPriceForPlan', () => { it('formats free and paid cadence copy', () => { const free: Plan = { ...proPlan, id: 'beakerstack_free', price_cents: 0 }; - expect(listPriceForPlan(free, 'monthly')).toBe('US$0'); + expect(listPriceForPlan(free, 'monthly')).toBe('$0'); expect(listPriceForPlan(proPlan, 'monthly')).toMatch(/19/); expect(listPriceForPlan(proPlan, 'annual')).toMatch(/year/); }); diff --git a/apps/web/src/components/landing/README.md b/apps/web/src/components/landing/README.md index e2188b1a..a7347ced 100644 --- a/apps/web/src/components/landing/README.md +++ b/apps/web/src/components/landing/README.md @@ -1,6 +1,6 @@ # Landing page -Config-driven B2C marketing landing page for BeakerStack. +Config-driven B2C marketing landing page for Beaker Stack. ## Rebrand in one file diff --git a/apps/web/src/components/landing/sections/PricingSection.tsx b/apps/web/src/components/landing/sections/PricingSection.tsx index 48f666bb..152a0be1 100644 --- a/apps/web/src/components/landing/sections/PricingSection.tsx +++ b/apps/web/src/components/landing/sections/PricingSection.tsx @@ -46,11 +46,9 @@ function StaticPricingTable() { }).format(cents / 100); const priceHeadline = - plan.price_cents === 0 - ? 'US$0' - : isAnnual && annualCents != null - ? fmt(annualCents) - : fmt(plan.price_cents); + isAnnual && annualCents != null + ? fmt(annualCents) + : fmt(plan.price_cents); const priceSubline = plan.price_cents === 0 diff --git a/apps/web/src/components/landing/sections/__tests__/FeatureRows.test.tsx b/apps/web/src/components/landing/sections/__tests__/FeatureRows.test.tsx index a6b9a4e8..6c431fe9 100644 --- a/apps/web/src/components/landing/sections/__tests__/FeatureRows.test.tsx +++ b/apps/web/src/components/landing/sections/__tests__/FeatureRows.test.tsx @@ -18,7 +18,7 @@ const rows = [ body: 'A single npm run setup provisions your local environment.', ctaLabel: 'Read the architecture', ctaHref: - 'https://github.com/Artificer-Innovations/BeakerStack/blob/main/ARCHITECTURE.md', + 'https://github.com/Artificer-Innovations/BeakerStack/blob/main/docs/ARCHITECTURE.md', mediaSrc: 'https://placehold.co/560x315?text=Setup', mediaAlt: 'Setup screenshot', mediaSide: 'left' as const, @@ -54,7 +54,7 @@ describe('FeatureRows', () => { const link = screen.getByRole('link', { name: /Read the architecture/ }); expect(link).toHaveAttribute( 'href', - 'https://github.com/Artificer-Innovations/BeakerStack/blob/main/ARCHITECTURE.md' + 'https://github.com/Artificer-Innovations/BeakerStack/blob/main/docs/ARCHITECTURE.md' ); expect(link).toHaveAttribute('target', '_blank'); expect(link).toHaveAttribute('rel', 'noopener noreferrer'); diff --git a/apps/web/src/components/landing/sections/__tests__/PricingSection.test.tsx b/apps/web/src/components/landing/sections/__tests__/PricingSection.test.tsx index 716abb89..4931146d 100644 --- a/apps/web/src/components/landing/sections/__tests__/PricingSection.test.tsx +++ b/apps/web/src/components/landing/sections/__tests__/PricingSection.test.tsx @@ -36,14 +36,17 @@ vi.mock('../../../../billing/staticPlanAdapter', () => ({ vi.mock('../../../billing/PlanCard.web', () => ({ PlanCard: ({ plan, + priceHeadline, primary, }: { plan: { id: string; display_name: string }; + priceHeadline: string; primary: { label: string; onClick: () => void }; }) => (