Skip to content

feat(legal): legal pages, terms acceptance at checkout and cookie consent - #25

Merged
kris1027 merged 27 commits into
mainfrom
feat/legal-pages-cookie-consent
Oct 21, 2026
Merged

kris1027 merged 27 commits into
mainfrom
feat/legal-pages-cookie-consent

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

Adds five legal pages (terms, privacy, returns, shipping, cookies) written as Markdown in the repo and linked from every store footer and from checkout. Checkout now requires an "I accept the terms" box and records the accepted terms version on each order. A small first party consent system (cookie, registry, banner, settings dialog, ConsentGate) is ready for analytics to plug into later; the banner stays hidden until an optional cookie exists.

Why

Implements spec 0017 (scope feature 16, Launch readiness). The store needs its legal texts in place before launch, a record of which terms a customer accepted for any later dispute, and a consent API so feature 18 (analytics) never loads a tracking script without a yes. Options and reasoning are in rationale.md.

Changes

Legal pages (src/lib/legal/, src/features/legal/, app/(store)/{terms,privacy,returns,shipping,cookies})

  • Five Markdown drafts with front matter title, description, updated. The updated date doubles as the version.
  • Texts are bundled at build time as strings through a small Turbopack loader (scripts/raw-text-loader.cjs, scoped to src/lib/legal/content/*.md); Vitest gets a matching plugin. Nothing reads them with fs at runtime.
  • {{placeholders}} are filled from the new brand.legal block and, on /shipping, from the cached shipping settings, so a fee change in /admin/settings shows up with no deploy. An unknown placeholder throws at load. Under VERCEL_ENV=production, any [[TO FILL marker or null brand.legal field also throws, so blank drafts cannot ship.
  • Each page sets title, description and canonical URL, and stays static. Footer links on every StoreShell page; header, footer and banner are hidden in print.
  • /returns ends with the EU model withdrawal form.

Terms acceptance at checkout

  • Required checkbox above Pay with new tab links to terms and privacy, plus a Returns / Shipping line under Pay. Unticked Pay shows an inline error and focuses the box; Pay is never disabled.
  • startCheckout takes acceptTerms and termsVersion. A stale version returns a new terms_changed error with the current version; the form unticks the box, shows an alert linking the new terms, and keeps every other field. Both checks run before any database work.
  • New nullable orders.terms_version (DATE) and orders.terms_accepted_at (timestamptz), written in the order creating transaction, with a CHECK that they are both set or both null. The PII purge leaves them alone.
  • Shown on /admin/orders/[id] ("Terms accepted: …" or "not recorded") and in the order confirmation email (version line plus absolute links to Terms, Returns, Privacy).
  • Sign up page gets a one line terms and privacy notice. Nothing new is stored.

Cookie consent (src/lib/consent/)

  • bestore_consent cookie written by the browser only, validated with Zod; a version or age mismatch counts as no choice.
  • registry.ts lists every cookie the store sets (today only necessary ones: cart, consent, Supabase auth, Stripe's own). Its test fails on any unlisted cookie. /cookies renders it as a table.
  • ConsentProvider, useConsent() and ConsentGate for feature 18. Banner and settings dialog (Save choices, Accept all, Reject all). Withdrawing a category deletes its cookies and reloads. Global Privacy Control counts as a refusal until the customer opts in.
  • The footer "Cookie settings" is a plain link to /cookies in server HTML and becomes a dialog button after hydration.
  • /style-guide gets a Consent demo with one demo_analytics cookie so the banner path can be exercised end to end (most of that file's diff is reindentation from the new provider wrapper).

Small supporting changes

  • CART_COOKIE moved to src/lib/cart/cookie-name.ts so the client side registry can name it without importing server only code.
  • FormNotice accepts a ReactNode message (for the alert with a link). New NewTabLink component.
  • AGENTS.md: new rule that every cookie or storage key goes in the consent registry, plus three nested context files.

How to test / verify

  • pnpm install && pnpm db:migrate (one new migration).
  • pnpm test for the loader, placeholders, front matter, drafts, consent cookie, registry, checkout schema and email render.
  • pnpm test:db for terms recording, terms_changed and unticked refusals creating no order, the CHECK, the purge and admin queries.
  • pnpm test:e2e tests/e2e/legal tests/e2e/admin/settings.spec.ts for pages, footer, print, checkout box, terms changed flow, sign up notice, cookie settings, the style guide banner demo, GPC, and the first visit check (only necessary cookies, empty web storage on /, a product, /cart, /checkout).
  • VERCEL_ENV=production pnpm build should fail while any [[TO FILL remains. That is expected today.
  • By hand: open /checkout, press Pay unticked, then tick and pay; the order in /admin/orders shows the terms line. On /style-guide, Accept all sets demo_analytics; untick Analytics in settings and Save, the page reloads and the cookie is gone.

The full checklist with results is in verify.md.

Risk & rollout

  • Migration: adds two nullable columns and one CHECK to orders. Existing rows keep both null and pass the CHECK. No backfill, no index.
  • Checkout contract: startCheckout now requires acceptTerms: true and a matching termsVersion. Any caller that does not send them gets a validation error. The only caller is the checkout form, updated here.
  • Production build guard: a production build fails until a lawyer reviewed text replaces every [[TO FILL and all brand.legal fields are set. This is on purpose, and it will block feature 19 (production deploy) until done.
  • Drafts are not legal advice: each file opens with a draft comment saying it needs legal review before launch.
  • Five value sourcing steps in verify.md are still unticked as manual checks (terms updated flowing everywhere, NEXT_PUBLIC_SITE_URL, filling one brand.legal field, STORE_LOCALE=pl-PL, a late evening acceptance time). Commit 03300f6 adds unit tests for versions, canonical URLs, locale formatting and email links.
  • No feature flags. Rollback is reverting the branch; the two columns can stay harmlessly.

Notes for reviewers

  • Worth extra eyes: the consent cookie parsing and withdrawal deletion in src/lib/consent/consent.ts and consent-provider.tsx, and the before any DB work ordering in start-checkout.ts.
  • Terms versions are calendar dates formatted with timeZone: "UTC" so they never shift by a day; acceptance times use STORE_TIMEZONE.
  • No /check review record exists yet for this branch in docs/reviews/.

🤖 Generated with Claude Code

kris1027 and others added 8 commits October 20, 2026 19:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… links

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added accessibility Keyboard access, screen reader support, contrast, and accessibility feature New user-facing functionality ui Layouts, styling, and shared interface components database Schemas, migrations, queries, and row-level security labels Oct 20, 2026
@kris1027 kris1027 self-assigned this Oct 20, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStore Oct 20, 2026
@kris1027 kris1027 moved this from Backlog to In review in BeStore Oct 20, 2026
kris1027 and others added 14 commits October 20, 2026 21:11
The shared store shell imported a feature module. The links only map
over src/lib/legal/paths.ts, so they belong beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The banner and the settings control already did; the dialog hard-coded
/cookies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
front-matter.ts and the checkout schema each wrote the same calendar
day refine. calendarDayTextSchema in src/lib/dates.ts serves both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New tab links, legal Markdown links and the checkout sign in hint each
spelled out the same class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ie table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…admin orders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
acceptAllChoices, grantedChoices, refusedAll and the dialog listed
analytics and marketing by hand, so a new category needed an edit in
each. choicesFrom builds them from the one list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legal versions travelled as plain strings, and calendarDate took any
string. Only text that passed calendarDayTextSchema now reaches it: a
legal text's updated, the terms version checkout accepts and the
current version a terms_changed refusal hands back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
checkoutSchema's z.literal(true) already refuses anything else on the
server, so the second check could never fire. ESLint now allows a key
destructured only to keep it off the rest object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AC-4 lists the store placeholders, AC-9 says a missing or malformed
termsVersion answers terms_changed, AC-17 lists sidebar_state, the raw
Markdown rule names the custom loader, and the CHECK test is an update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 0017 asked for the guard test to run next build; only the loader's
unit tests covered it. A new Vitest build project copies the app under
.next/, adds a fixture line to terms.md and builds it: a [[TO FILL]]
marker with VERCEL_ENV=production and an unknown placeholder must each
fail the build with the loader's message. CI runs it in the static job.

The Turbopack rule's path condition now accepts a folder prefix, since
the copy's Turbopack root is the repo, not the copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kris1027 and others added 5 commits October 21, 2026 16:05
The Vitest transform copied the loader's body and kept its own path
pattern, which had already drifted from next.config.ts. Both now use
scripts/raw-text-loader.cjs and src/lib/legal/content-path.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page that keeps fetching never reaches networkidle, so the first visit
check timed out in CI. The footer's Cookie settings turning from a link
into a button is a deterministic ready signal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare getByText also matched the hidden copy of a streamed segment,
a strict mode violation on mobile that failed main as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The streamed redirect navigated after hydration and destroyed the page
the storage read ran in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
allTextContents does not wait, so it could read the grid before it
streamed in and find neither product.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit 3b2f359 into main Oct 21, 2026
7 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStore Oct 21, 2026
@kris1027
kris1027 deleted the feat/legal-pages-cookie-consent branch October 21, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accessibility Keyboard access, screen reader support, contrast, and accessibility database Schemas, migrations, queries, and row-level security feature New user-facing functionality ui Layouts, styling, and shared interface components

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant