Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
89d5daf
docs(specs): spec 0017 legal pages and cookie consent
kris1027 Oct 20, 2026
9cc580f
feat(legal): legal pages from Markdown with live shipping values
kris1027 Oct 20, 2026
8176421
feat(checkout): require and record terms acceptance on each order
kris1027 Oct 20, 2026
4646174
feat(consent): consent cookie, registry, banner and settings dialog
kris1027 Oct 20, 2026
b4fd720
docs(specs): verify steps for spec 0017
kris1027 Oct 20, 2026
03300f6
test(legal): value sourcing for versions, canonical, locale and email…
kris1027 Oct 20, 2026
824cbc7
docs(scope): legal pages & cookie consent verified, tested and done
kris1027 Oct 20, 2026
127c351
chore(sync): reconcile docs
kris1027 Oct 20, 2026
2376195
refactor(legal): move footer links to src/lib/legal
kris1027 Oct 20, 2026
88b36a0
refactor(legal): make the renderer's allowed elements readonly
kris1027 Oct 20, 2026
db796be
docs(legal): correct the loader and footer order comments
kris1027 Oct 20, 2026
49d32a2
fix(consent): link the dialog to the cookie policy through legalPaths
kris1027 Oct 20, 2026
c6bf6e9
refactor(dates): share one YYYY-MM-DD schema for legal and checkout
kris1027 Oct 20, 2026
68677a1
refactor(ui): share the inline link class
kris1027 Oct 20, 2026
2ed3425
refactor(consent): one set of category labels for the dialog and cook…
kris1027 Oct 20, 2026
03eea7a
refactor(email): build the confirmation's legal links through one helper
kris1027 Oct 20, 2026
651bc18
refactor(legal): share the TermsAcceptance type between checkout and …
kris1027 Oct 20, 2026
5a19276
refactor(consent): derive per category choices from optionalCategories
kris1027 Oct 20, 2026
1adb6ad
refactor(dates): brand checked YYYY-MM-DD text as CalendarDayText
kris1027 Oct 20, 2026
37de060
refactor(checkout): drop the repeated acceptTerms check in startCheckout
kris1027 Oct 20, 2026
981eaef
docs(specs): align spec 0017 with what was built
kris1027 Oct 20, 2026
083e9e2
test(legal): prove the legal text guard fails a real next build
kris1027 Oct 20, 2026
abf9d7a
refactor(legal): run the one raw text loader and path pattern in Vitest
kris1027 Oct 21, 2026
2989274
test(consent): wait for hydration instead of network idle
kris1027 Oct 21, 2026
75a2fd7
test(dashboard): scope figure checks to their card
kris1027 Oct 21, 2026
ac58a64
test(consent): wait for the empty cart checkout to land on /cart
kris1027 Oct 21, 2026
ae818f2
test(catalog): wait for the home grid before reading its order
kris1027 Oct 21, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
- run: pnpm format:check
- run: pnpm typecheck
- run: pnpm test
# spec 0017, AC-4: a blank or unknown placeholder in a legal text fails `next build`.
- run: pnpm test:build

db:
runs-on: ubuntu-latest
Expand Down
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pnpm lint && pnpm typecheck
pnpm format # Prettier; format:check in CI
pnpm test # Vitest unit + integration (*.test.ts beside the source)
pnpm test:db # Vitest db project against TEST_DATABASE_URL (needs pnpm db:start)
pnpm test:build # Vitest build project: next build on a copy of the app (tests/build/)
pnpm test:e2e # Playwright e2e (tests/e2e/*.spec.ts)
```

Expand All @@ -57,6 +58,7 @@ Stored in `docs/specs/`. Format: `docs/specs/NNNN-title/index.md`. Scope lives i
- **Money and time**: integer minor units (cents) in `Int` columns, never floats; timestamps in UTC `timestamptz`, shown in `STORE_TIMEZONE`.
- **Caching**: `cacheComponents` is on. Catalog reads are `'use cache'` functions tagged from `src/lib/cache-tags.ts`; whatever changes what they return must expire the tag (`updateTag` in a server action, `revalidateTag(tag, { expire: 0 })` in a route handler). Request data (cookies, the cart) sits behind Suspense.
- **Status codes**: under Cache Components a page's `notFound()` / `redirect()` answer 200 (the shell streams first). Where the real status matters, decide it in the proxy before rendering (the admin gate does this).
- **Cookies and storage**: every new cookie or web storage key goes in the consent registry (`src/lib/consent/registry.ts`; its test catches unlisted cookies, not storage keys); anything not strictly necessary runs only behind `ConsentGate` / `useConsent()`. Rendering a page never sets a cookie.
- **Cron**: handlers under `app/api/cron/` refuse anything but `Authorization: Bearer ${CRON_SECRET}` (constant time compare); schedules live in `vercel.json`.
- **Env**: every variable the app reads is added to the Zod schema in `src/lib/env.ts` and to `.env.example`. A variable only a CLI reads (e.g. `SEND_EMAIL_HOOK_URI`, read by the Supabase CLI through `env()` in `supabase/config.toml`) goes in `.env.example` alone, marked as such.
- **UI**: WCAG AA baseline on storefront and admin (labels, keyboard use, visible focus, contrast).
Expand All @@ -69,7 +71,7 @@ Stored in `docs/specs/`. Format: `docs/specs/NNNN-title/index.md`. Scope lives i
Installed:
- ESLint (Next.js config, `@typescript-eslint/no-explicit-any: error`) + Prettier with `prettier-plugin-tailwindcss`; scripts `format`, `format:check`, `test`, `test:e2e`. Prettier skips `*.md` on purpose (skills and `next dev` own those files).
- Pre commit (simple-git-hooks + lint-staged): lint and format staged files, then `pnpm typecheck`. `SKIP_SIMPLE_GIT_HOOKS=1` bypasses it in an emergency.
- CI: [.github/workflows/ci.yml](.github/workflows/ci.yml), on pushes to `main` and on every PR, as parallel jobs: `static` (lint, format check, typecheck, unit), `db` (`test:db`), `e2e` (4 Playwright shards, each with its own Supabase stack and build; shard 1 also runs `test:stack`), and a `check` gate that needs them all (the ruleset's required check, so it keeps that name).
- CI: [.github/workflows/ci.yml](.github/workflows/ci.yml), on pushes to `main` and on every PR, as parallel jobs: `static` (lint, format check, typecheck, unit, build guard), `db` (`test:db`), `e2e` (4 Playwright shards, each with its own Supabase stack and build; shard 1 also runs `test:stack`), and a `check` gate that needs them all (the ruleset's required check, so it keeps that name).

## Git

Expand Down Expand Up @@ -105,5 +107,8 @@ MCP servers: Supabase (connected), Stripe (connected), Vercel (connected), Playw
- [src/features/settings/AGENTS.md](src/features/settings/AGENTS.md): the admin settings page, the shipping fee and free delivery threshold
- [src/features/account/AGENTS.md](src/features/account/AGENTS.md): customer accounts, sign up and sign in, the customer session and lock, order history, the address book, settings
- [src/lib/shipping/AGENTS.md](src/lib/shipping/AGENTS.md): the shared delivery fee rule, address schema and settings reads
- [src/lib/legal/AGENTS.md](src/lib/legal/AGENTS.md): the five legal texts as bundled Markdown, their versions, placeholders and the production blank guard
- [src/lib/consent/AGENTS.md](src/lib/consent/AGENTS.md): the consent cookie, cookie registry, `ConsentProvider` / `useConsent` / `ConsentGate`, banner and settings dialog
- [src/features/legal/AGENTS.md](src/features/legal/AGENTS.md): the storefront legal pages, their Markdown renderer and the footer links

_Drafted by /audit from the repo, worth a quick human pass. Edit freely: once a line stops matching this draft, later runs treat it as curated and will flag rather than overwrite it._
3 changes: 2 additions & 1 deletion app/(store)/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CheckoutSummary,
CheckoutSummarySkeleton,
} from "@/features/checkout/components/checkout-summary";
import { currentTermsVersion } from "@/lib/legal/documents";
import { cn } from "@/lib/utils";

export const metadata: Metadata = {
Expand All @@ -18,7 +19,7 @@ export default function CheckoutPage({ searchParams }: PageProps<"/checkout">) {
<div className={cn(storeContainer, "flex flex-col gap-8 py-12 md:py-16")}>
<h1 className="font-heading text-4xl md:text-5xl">Checkout</h1>
<Suspense fallback={<CheckoutSummarySkeleton />}>
<CheckoutSummary searchParams={searchParams} />
<CheckoutSummary searchParams={searchParams} termsVersion={currentTermsVersion} />
</Suspense>
</div>
);
Expand Down
16 changes: 16 additions & 0 deletions app/(store)/cookies/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CookieTable } from "@/features/legal/components/cookie-table";
import { LegalPage, legalMetadata } from "@/features/legal/components/legal-page";
import { cookieRegistry } from "@/lib/consent/registry";
import { CookieSettingsButton } from "@/lib/consent/cookie-settings-control";

export const metadata = legalMetadata("cookies");

// spec 0017, AC-16 and AC-17: the policy, the registry table, and the settings button.
export default function CookiesPage() {
return (
<LegalPage slug="cookies">
<CookieTable registry={cookieRegistry} />
<CookieSettingsButton />
</LegalPage>
);
}
42 changes: 25 additions & 17 deletions app/(store)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,35 @@ import { CartLink, CartLinkView } from "@/features/cart/components/cart-link";
import { HeaderSearch } from "@/features/catalog/listing/components/search-box";
import { categoryPath } from "@/features/categories/paths";
import { getNavCategories } from "@/features/categories/queries";
import { ConsentBanner } from "@/lib/consent/consent-banner";
import { ConsentProvider } from "@/lib/consent/consent-provider";
import { cookieRegistry } from "@/lib/consent/registry";
import { cookiePolicyVersion } from "@/lib/legal/documents";

// The nav is a cached read tagged `categories`, so the shell stays static (spec 0013, AC-3).
// Consent is read in the browser only (spec 0017, AC-22), which keeps it static too.
export default async function StoreLayout({ children }: LayoutProps<"/">) {
const categories = await getNavCategories();
return (
<StoreShell
navItems={categories.map((category) => ({
href: categoryPath(category.slug),
label: category.name,
}))}
actions={
<>
<HeaderSearch />
<AccountLink />
<Suspense fallback={<CartLinkView count={0} />}>
<CartLink />
</Suspense>
</>
}
>
{children}
</StoreShell>
<ConsentProvider policyVersion={cookiePolicyVersion} registry={cookieRegistry}>
<StoreShell
banner={<ConsentBanner />}
navItems={categories.map((category) => ({
href: categoryPath(category.slug),
label: category.name,
}))}
actions={
<>
<HeaderSearch />
<AccountLink />
<Suspense fallback={<CartLinkView count={0} />}>
<CartLink />
</Suspense>
</>
}
>
{children}
</StoreShell>
</ConsentProvider>
);
}
7 changes: 7 additions & 0 deletions app/(store)/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LegalPage, legalMetadata } from "@/features/legal/components/legal-page";

export const metadata = legalMetadata("privacy");

export default function PrivacyPage() {
return <LegalPage slug="privacy" />;
}
7 changes: 7 additions & 0 deletions app/(store)/returns/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LegalPage, legalMetadata } from "@/features/legal/components/legal-page";

export const metadata = legalMetadata("returns");

export default function ReturnsPage() {
return <LegalPage slug="returns" />;
}
20 changes: 20 additions & 0 deletions app/(store)/shipping/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LegalPage, legalMetadata } from "@/features/legal/components/legal-page";
import { shippingValues } from "@/features/legal/shipping-values";
import { env } from "@/lib/env";
import { getShippingSettings } from "@/lib/shipping/settings";

export const metadata = legalMetadata("shipping");

// spec 0017, AC-5: static plus the cached settings read, which an admin save expires.
export default async function ShippingPage() {
const settings = await getShippingSettings();
return (
<LegalPage
slug="shipping"
values={shippingValues(settings, {
currency: env.STORE_CURRENCY,
locale: env.STORE_LOCALE,
})}
/>
);
}
7 changes: 7 additions & 0 deletions app/(store)/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LegalPage, legalMetadata } from "@/features/legal/components/legal-page";

export const metadata = legalMetadata("terms");

export default function TermsPage() {
return <LegalPage slug="terms" />;
}
14 changes: 11 additions & 3 deletions docs/scope/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ _These are recommendations to keep your build orderly, not requirements. Skip an
| 14 | Discounts & promo codes | Slice 8 | done |
| 15 | Admin sales dashboard | Slice 9 | done |
| 23 | Native controls in GET forms | Slice 9 follow up | done |
| 16 | Legal pages & cookie consent | Launch | planned |
| 16 | Legal pages & cookie consent | Launch | done |
| 17 | SEO for storefront pages | Launch | planned |
| 18 | Analytics & error tracking | Launch | planned |
| 19 | Production deploy | Launch | planned |
Expand Down Expand Up @@ -311,10 +311,18 @@ spec [0016](../specs/0016-native-controls-get-forms.md) · code in [src/componen

## Launch readiness

### 16. Legal pages & cookie consent · needs a decision
### 16. Legal pages & cookie consent · done
Terms, privacy policy, returns and shipping policy, and a cookie banner that blocks non essential cookies until the customer agrees.
**Done when:** the legal pages are linked from every page footer and from checkout; no analytics or marketing cookies are set before consent; the customer can change their choice later.
- [ ] Design it (spec): `/architect legal pages & cookie consent`
spec [0017](../specs/0017-legal-pages-cookie-consent/index.md) · code in [src/lib/legal/](../../src/lib/legal/), [src/lib/consent/](../../src/lib/consent/), [src/features/legal/](../../src/features/legal/)
- [x] Design it (spec): `/architect legal pages & cookie consent`
- [x] Build it: `/develop legal pages & cookie consent`
- [x] Thin thread: the terms page from Markdown, the footer link, the checkout checkbox recording the version on the order, the admin line and the confirmation email (AC-1, AC-2, AC-4, AC-7 to AC-11, AC-13, AC-14)
- [x] The other four pages with live shipping values, the model withdrawal form, print styles and the sign up notice (AC-1 to AC-3, AC-5 to AC-7, AC-12)
- [x] Consent core: the cookie, the registry, `ConsentProvider`, `useConsent` and `ConsentGate`, and the cookie table (AC-15, AC-17, AC-22)
- [x] Banner, settings dialog, withdrawal, GPC, the style guide demo and the first visit privacy e2e (AC-16, AC-18 to AC-21, AC-23, AC-24)
- [x] Verify it: `/check verify legal pages & cookie consent`
- [x] Test it: `/test legal pages & cookie consent`

### 17. SEO for storefront pages
Make products findable: titles, descriptions, canonical links, sitemap, product rich results, and social share cards on every public page.
Expand Down
Loading
Loading