Repository navigation
feat(legal): legal pages, terms acceptance at checkout and cookie consent - #25
Merged
Merged
Conversation
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>
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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})title,description,updated. Theupdateddate doubles as the version.scripts/raw-text-loader.cjs, scoped tosrc/lib/legal/content/*.md); Vitest gets a matching plugin. Nothing reads them withfsat runtime.{{placeholders}}are filled from the newbrand.legalblock and, on/shipping, from the cached shipping settings, so a fee change in/admin/settingsshows up with no deploy. An unknown placeholder throws at load. UnderVERCEL_ENV=production, any[[TO FILLmarker ornullbrand.legalfield also throws, so blank drafts cannot ship.StoreShellpage; header, footer and banner are hidden in print./returnsends with the EU model withdrawal form.Terms acceptance at checkout
startCheckouttakesacceptTermsandtermsVersion. A stale version returns a newterms_changederror 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.orders.terms_version(DATE) andorders.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./admin/orders/[id]("Terms accepted: …" or "not recorded") and in the order confirmation email (version line plus absolute links to Terms, Returns, Privacy).Cookie consent (
src/lib/consent/)bestore_consentcookie written by the browser only, validated with Zod; a version or age mismatch counts as no choice.registry.tslists every cookie the store sets (today only necessary ones: cart, consent, Supabase auth, Stripe's own). Its test fails on any unlisted cookie./cookiesrenders it as a table.ConsentProvider,useConsent()andConsentGatefor 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./cookiesin server HTML and becomes a dialog button after hydration./style-guidegets a Consent demo with onedemo_analyticscookie 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_COOKIEmoved tosrc/lib/cart/cookie-name.tsso the client side registry can name it without importing server only code.FormNoticeaccepts aReactNodemessage (for the alert with a link). NewNewTabLinkcomponent.How to test / verify
pnpm install && pnpm db:migrate(one new migration).pnpm testfor the loader, placeholders, front matter, drafts, consent cookie, registry, checkout schema and email render.pnpm test:dbfor terms recording,terms_changedand unticked refusals creating no order, the CHECK, the purge and admin queries.pnpm test:e2e tests/e2e/legal tests/e2e/admin/settings.spec.tsfor 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 buildshould fail while any[[TO FILLremains. That is expected today./checkout, press Pay unticked, then tick and pay; the order in/admin/ordersshows the terms line. On/style-guide, Accept all setsdemo_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
orders. Existing rows keep both null and pass the CHECK. No backfill, no index.startCheckoutnow requiresacceptTerms: trueand a matchingtermsVersion. Any caller that does not send them gets a validation error. The only caller is the checkout form, updated here.[[TO FILLand allbrand.legalfields are set. This is on purpose, and it will block feature 19 (production deploy) until done.updatedflowing everywhere,NEXT_PUBLIC_SITE_URL, filling onebrand.legalfield,STORE_LOCALE=pl-PL, a late evening acceptance time). Commit03300f6adds unit tests for versions, canonical URLs, locale formatting and email links.Notes for reviewers
src/lib/consent/consent.tsandconsent-provider.tsx, and the before any DB work ordering instart-checkout.ts.timeZone: "UTC"so they never shift by a day; acceptance times useSTORE_TIMEZONE./check reviewrecord exists yet for this branch indocs/reviews/.🤖 Generated with Claude Code