From f435a287dab16e533ed1c866e1e580bc24e79c4a Mon Sep 17 00:00:00 2001 From: Filip Cichorek Date: Thu, 26 Mar 2026 09:38:07 +0100 Subject: [PATCH 1/9] Add store policies pages and consent checkboxes - Add policy detail pages fetched from Spree API (/policies/[slug]) - Add policy consent checkbox on registration (Privacy Policy & Terms of Service) - Add policy consent checkbox on checkout (all policies for guests, excluding registration policies for authenticated users) - Add policies section in footer with dynamic links - Fix footer links to use localized basePath Closes spree/storefront#60 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 3 + .../(checkout)/checkout/[id]/page.tsx | 47 +++++++++++++--- .../(storefront)/account/register/page.tsx | 28 ++++++++++ .../[locale]/(storefront)/layout.tsx | 18 +++--- .../(storefront)/policies/[slug]/page.tsx | 56 +++++++++++++++++++ src/components/layout/Footer.tsx | 44 ++++++++++++--- src/components/policy/PolicyConsent.tsx | 50 +++++++++++++++++ src/lib/constants/policies.ts | 5 ++ src/lib/data/policies.ts | 17 ++++++ 9 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 src/app/[country]/[locale]/(storefront)/policies/[slug]/page.tsx create mode 100644 src/components/policy/PolicyConsent.tsx create mode 100644 src/lib/constants/policies.ts create mode 100644 src/lib/data/policies.ts diff --git a/README.md b/README.md index 3b88789a..091a0459 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A modern, headless e-commerce storefront built with Next.js 16, React 19, and [S - **Multi-Region Support** - Country, currency, and language switching via URL segments, powered by [Spree Markets](https://spreecommerce.org/docs/developer/core-concepts/markets) - **Responsive Design** - Mobile-first Tailwind CSS styling - **Google Tag Mananager** and **Google Analytics 4 Ecommerce events** tracking supported natively +- **Store Policies** - Dynamic policy pages (Privacy Policy, Terms of Service, etc.) fetched from Spree API, with consent checkboxes on registration and checkout - **SEO-ready** - meta tags, JSON-LD, OpenGraph - all built in! - **Error Tracking** - Sentry integration for both server-side and client-side error monitoring with source maps @@ -122,6 +123,7 @@ src/ │ │ ├── register/ # Registration │ │ └── profile/ # Profile settings │ ├── cart/ # Shopping cart +│ ├── policies/ # Store policy pages │ ├── products/ # Product listing │ │ └── [slug]/ # Product details │ ├── t/[...permalink]/ # Taxon/category pages @@ -143,6 +145,7 @@ src/ ├── credit-cards.ts # Payment methods ├── customer.ts # Auth & profile ├── orders.ts # Order history + ├── policies.ts # Store policies ├── products.ts # Product queries ├── store.ts # Store configuration └── taxonomies.ts # Categories/taxons diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx index 68d1bda8..0578320a 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import type { Address, AddressParams, Cart, Country } from "@spree/sdk"; +import type { Address, AddressParams, Cart, Country, Policy } from "@spree/sdk"; import { CircleAlert, Loader2 } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -13,6 +13,7 @@ import { type PaymentSectionHandle, } from "@/components/checkout/PaymentSection"; import { Summary } from "@/components/checkout/Summary"; +import { PolicyConsent } from "@/components/policy/PolicyConsent"; import { useAuth } from "@/contexts/AuthContext"; import { useCheckout } from "@/contexts/CheckoutContext"; import { @@ -20,6 +21,7 @@ import { trackAddShippingInfo, trackBeginCheckout, } from "@/lib/analytics/gtm"; +import { REGISTRATION_POLICY_SLUGS } from "@/lib/constants/policies"; import { getAddresses, updateAddress } from "@/lib/data/addresses"; import { applyCode, @@ -36,6 +38,7 @@ import { completeCheckoutOrder, completeCheckoutPaymentSession, } from "@/lib/data/payment"; +import { getPolicies } from "@/lib/data/policies"; import { extractBasePath } from "@/lib/utils/path"; interface CheckoutPageProps { @@ -100,6 +103,8 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { const [sectionErrors, setSectionErrors] = useState>( {}, ); + const [policies, setPolicies] = useState([]); + const [policyConsent, setPolicyConsent] = useState(false); const fulfillments = cart?.fulfillments ?? []; @@ -184,12 +189,14 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { if (!paymentError) setError(null); try { - const [cartData, market, addressesData, authStatus] = await Promise.all([ - getCheckoutOrder(cartId), - resolveMarket(urlCountry).catch(() => null), - getAddresses(), - checkAuth(), - ]); + const [cartData, market, addressesData, authStatus, allPolicies] = + await Promise.all([ + getCheckoutOrder(cartId), + resolveMarket(urlCountry).catch(() => null), + getAddresses(), + checkAuth(), + getPolicies(), + ]); const countriesData = market ? await getMarketCountries(market.id).catch(() => ({ @@ -212,6 +219,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { setCountries(countriesData.data); setSavedAddresses(addressesData.data); setIsAuthenticated(authStatus); + setPolicies( + authStatus + ? allPolicies.filter( + (p) => !REGISTRATION_POLICY_SLUGS.includes(p.slug), + ) + : allPolicies, + ); if (!beginCheckoutFiredRef.current) { try { @@ -460,6 +474,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { setSectionErrors({}); setError(null); + if (policies.length > 0 && !policyConsent) { + setError( + "You must agree to the store policies before placing your order", + ); + return; + } + // Refresh cart to get latest requirements const freshOrder = await getCheckoutOrder(cart.id); if (!freshOrder) { @@ -628,6 +649,18 @@ export default function CheckoutPage({ params }: CheckoutPageProps) { /> + {/* Policy consent */} + {policies.length > 0 && ( +
+ +
+ )} + {/* Pay now button — Shopify: black, tall, minimal radius, bold */}