Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .changeset/calm-commerce-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@godaddy/commerce-server': minor
---

Add an Express server package for GoDaddy Commerce catalog, cart, and hosted checkout routes, with production API defaults and host-owned configuration and attribution.

Require host-approved checkout return URLs and keep non-catalog pricing in trusted server helpers. Recover saved carts when the Orders API reports a missing or completed draft.

Read completed orders through the authorized Orders REST API with the `commerce.order:read` scope.
5 changes: 5 additions & 0 deletions .changeset/quiet-storefront-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/commerce-storefront": minor
---

Add an opinionated React storefront package with catalog, verified variant selection, a shared cart drawer, and optional hosted checkout handoff. Ship scoped, layer-free CSS compatible with Tailwind v3 host builds, TypeScript response contracts, and an independent consumer example. Keep host application content mounted when commerce configuration is unavailable.
34 changes: 33 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ biome-config-godaddy (packages/biome-config-godaddy)
- typecheck: tsc --noEmit
- test: vitest run

@godaddy/commerce-server (packages/commerce-server)
- build: tsdown
- typecheck: tsc --noEmit
- lint: biome check src
- lint:fix: biome check --write src
- test: vitest run
- prepublishOnly: pnpm build

@godaddy/localizations (packages/localizations)
- dev: tsdown --watch
- build: tsdown
Expand Down Expand Up @@ -129,6 +137,11 @@ Packages (top-level purpose)
- React component library for checkout flows; integrates with commerce APIs
- Uses tsdown for TS build and Tailwind CLI v4 for CSS build; Vitest for tests; Vite preview
- Depends on @godaddy/localizations
- @godaddy/commerce-storefront
- React catalog, product details, and cart components using a same-origin Commerce API
- @godaddy/commerce-server
- Express 5 routers for the storefront API, hosted checkout, and order lookup
- Uses tsdown, Vitest, and Biome; usable independently of the storefront package
- @godaddy/localizations
- Localized strings for checkout components; TS build via tsdown

Expand Down Expand Up @@ -275,7 +288,26 @@ D. @godaddy/react
- Uses path alias "@/*" for src
- If adding components, follow existing patterns in src/components/checkout/** and src/components/ui/**

E. @godaddy/localizations
E. @godaddy/commerce-storefront
- Opinionated React storefront; fixed same-origin `/api/commerce` server contract documented in packages/commerce-storefront/docs/server-api.md.
- Peers: React/React DOM 18 or 19, React Router 7 or 8.3+, TanStack Query 5. Host owns router/query providers; CommerceStorefront owns the cart provider and drawer.
- Build: tsdown plus local Tailwind CLI, CSS scoping, and layer removal in declared order; styles exported as ./styles.css. Artifact tests process the output through Tailwind v3 to verify host compatibility. No host Tailwind setup or global reset.
- Test: build first, then Vitest (behavior plus compiled artifact checks). Commands: build, typecheck, lint, test.
- Example: examples/commerce-storefront, port 5184, development-only in-memory server; production build needs real API routes.
- Keep credentials, merchant provisioning and platform configuration out of this client package. No dependency on the separate commerce web-component runtime.

F. @godaddy/commerce-server
- Mount `createCommerceRouter()` at `/api/commerce` after `express.json()`. `createCommerceCatalogRouter()` installs config/catalog/cart routes; `createGoDaddyPaymentsRouter()` installs checkout/order-status routes.
- Hosts can supply `CommerceConfiguration`. The default runtime reader uses server environment variables for credentials, store/channel IDs, currency, and checkout flags; it does not read files. The API defaults to `https://api.godaddy.com`; an explicit server-controlled `apiBaseUrl` option supports alternate origins without embedding environment-specific hosts. Keep this package server-only.
- Hosts own deployment-specific configuration loading, provisioning readiness, retries, and optional `sourceApp`/`owner` attribution. Shipping options use the hosted checkout API shape; omit them to use the store configuration.
- HTTP checkout accepts only cart/SKU inputs; server-owned non-catalog prices use the trusted helper. Configure `checkoutReturnUrls` with exact HTTPS cancel/success destinations; without it, HTTP checkout is disabled. Only success URLs may add an `orderId` query parameter.
- Exported helpers `createCheckoutSession()` and `getOrderStatus()` support in-process server callers. Order lookup uses the store-scoped Orders REST API with OAuth scope `commerce.order:read`, verifies the order/store/channel binding, and returns the API's payment status. A checkout redirect alone is not proof of payment; hosts must authorize caller access to each order.
- Source alias `@/*` maps to `src/*` in TypeScript and Vitest. tsdown resolves it when bundling JavaScript and declarations; consumers need no alias configuration. When changing module resolution, verify a packed consumer outside the workspace.
- Validate supplied `X-Commerce-Scope` headers before catalog/cart/checkout calls. It guards against stale bindings and is not authorization. Hosts own authentication and authorization.
- Cart mutations return a refreshed cart. Keep URL cart/item IDs authoritative, allowlist PATCH fields, and only clear carts for explicit missing/expired-order errors; upstream authentication or transport failures must preserve saved carts.
- Commands: `pnpm --filter @godaddy/commerce-server build`, `typecheck`, `lint`, and `test`. See packages/commerce-server/README.md and packages/commerce-storefront/docs/server-api.md for integration details.

G. @godaddy/localizations
- Purpose: Localization bundles for checkout UI
- Structure: src/<locale>.ts with a shared object shape; exported via src/index.ts
- Scripts: dev/build/typecheck
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ This monorepo contains the following packages:
| [`biome-config-godaddy`](/packages/biome-config-godaddy) | Fast Rust-based alternative to ESLint and Prettier using Biome | [![npm](https://img.shields.io/npm/v/biome-config-godaddy.svg)](https://www.npmjs.com/package/biome-config-godaddy) |
| [`@godaddy/app-connect`](/packages/app-connect) | Platform integration tools for GoDaddy apps | [![npm](https://img.shields.io/npm/v/@godaddy/app-connect.svg)](https://www.npmjs.com/package/@godaddy/app-connect) |
| [`@godaddy/react`](/packages/react) | React components and commerce API integration | [![npm](https://img.shields.io/npm/v/@godaddy/react.svg)](https://www.npmjs.com/package/@godaddy/react) |
| [@godaddy/commerce-storefront](packages/commerce-storefront) | Opinionated React catalog, product, and cart templates; [consumer example](examples/commerce-storefront) | Unreleased |
| [@godaddy/commerce-server](packages/commerce-server) | Express APIs for Commerce catalog, carts, hosted checkout, and order lookup | Unreleased |

`@godaddy/commerce-server` implements the same-origin API used by `@godaddy/commerce-storefront`. The packages can also be used independently with a custom client or server; see the [server integration guide](packages/commerce-server/README.md) and [storefront API contract](packages/commerce-storefront/docs/server-api.md).

## Why GoDaddy JavaScript?

Expand Down
19 changes: 19 additions & 0 deletions examples/commerce-storefront/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Independent commerce storefront example

This React Router application consumes the compiled `@godaddy/commerce-storefront` package and its shipped CSS. It runs as a standalone application without Tailwind configuration.

Run from the repository root with Node 24:

```sh
pnpm install
pnpm --filter @godaddy/commerce-storefront build
pnpm --filter commerce-storefront-example dev
```

Open <http://127.0.0.1:5184/shop>. The Vite development server serves a demonstration catalog and in-memory cart at `/api/commerce`. Try adding the mug, changing quantities, removing items, and selecting the blue or sold-out clay tote. Reloading the browser restores a saved cart while the demo server remains running. Restarting the server expires it.

This mock is for local UI demonstration only. It creates no Commerce orders or payments and is not a production server implementation. Checkout is intentionally disabled. The production build verifies bundling; it does not include the mock API, so a deployed build needs an implementation of the [server contract](../../packages/commerce-storefront/docs/server-api.md).

```sh
pnpm --filter commerce-storefront-example build
```
2 changes: 2 additions & 0 deletions examples/commerce-storefront/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Commerce storefront example</title></head><body><div id="root"></div><script type="module" src="/main.tsx"></script></body></html>
27 changes: 27 additions & 0 deletions examples/commerce-storefront/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Link, Navigate, Route, Routes } from 'react-router';
import { Catalog, CartButton, CommerceStorefront, ProductDetails } from '@godaddy/commerce-storefront';
import '@godaddy/commerce-storefront/styles.css';
import './styles.css';

const client = new QueryClient();
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={client}>
<BrowserRouter>
<CommerceStorefront theme={{ '--commerce-accent': '#174c3c', '--commerce-accent-hover': '#10362b' }}>
<a className='skip-link' href='#main'>Skip to products</a>
<header className='site-header'><Link to='/shop'>Field & Form</Link><CartButton /></header>
<main id='main'>
<p className='demo-note'>Demo catalog and in-memory cart. No orders or payments are created.</p>
<Routes>
<Route path='/shop' element={<Catalog title='Everyday essentials' description='Useful objects, thoughtfully made.' />} />
<Route path='/products/:productId' element={<ProductDetails />} />
<Route path='*' element={<Navigate to='/shop' replace />} />
</Routes>
</main>
<footer>Independent React app · No Tailwind configuration</footer>
</CommerceStorefront>
</BrowserRouter>
</QueryClientProvider>
);
24 changes: 24 additions & 0 deletions examples/commerce-storefront/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "commerce-storefront-example",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5184 --strictPort",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@godaddy/commerce-storefront": "workspace:*",
"@tanstack/react-query": "^5.66.0",
"react": "^19",
"react-dom": "^19",
"react-router": "^7.0.0"
},
"devDependencies": {
"@types/node": "^22.13.1",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"typescript": "~5.7.3",
"vite": "^6.4.1"
}
}
8 changes: 8 additions & 0 deletions examples/commerce-storefront/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
body { margin: 0; background: #faf9f6; color: #242c28; font-family: system-ui, sans-serif; }
.site-header { display:flex; align-items:center; justify-content:space-between; padding:24px max(16px, 5vw); border-bottom:1px solid #deded8; }
.site-header > a { font-family: Georgia, serif; font-size: 24px; color: inherit; text-decoration: none; }
main { max-width:1200px; padding:24px 16px 64px; margin:auto; }
.demo-note { padding:12px; background:#e9efe9; border-radius:8px; font-size:14px; }
footer { padding:24px; border-top:1px solid #deded8; font-size:14px; }
.skip-link { position:absolute; top:-100px; padding:16px; background:white; z-index:100; }
.skip-link:focus { top:0; }
20 changes: 20 additions & 0 deletions examples/commerce-storefront/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"ES2022",
"DOM"
],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true
},
"include": [
"*.ts",
"*.tsx"
]
}
67 changes: 67 additions & 0 deletions examples/commerce-storefront/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { randomUUID } from 'node:crypto';
import { defineConfig, type Plugin } from 'vite';
import type { CartOrder, SKU, SKUGroup } from '@godaddy/commerce-storefront';

const money = (value: number) => ({ value, currencyCode: 'USD' });
const sku = (id: string, price: number, quantity = 20): SKU => ({
id, prices: { edges: [{ node: { value: money(price) } }] },
inventoryCounts: { edges: [{ node: { type: 'AVAILABLE', quantity } }] },
});
const mug = sku('mug', 2400);
const blue = sku('tote-blue', 3600);
const clay = sku('tote-clay', 3800, 0);
const products: SKUGroup[] = [
{ id: 'mug', label: 'Studio mug', description: 'A sturdy ceramic mug for slow mornings.', priceRange: { min: 2400, max: 2400 }, skus: { edges: [{ node: mug }], totalCount: 1 } },
{ id: 'tote', label: 'Market tote', description: 'Choose a color for your everyday carry.', priceRange: { min: 3600, max: 3800 }, attributes: { edges: [{ node: { id: 'color', name: 'color', label: 'Color', values: { edges: [{ node: { name: 'blue', label: 'Blue' } }, { node: { name: 'clay', label: 'Clay' } }] } } }] }, skus: { edges: [{ node: blue }, { node: clay }], totalCount: 2 } },
];

// Demonstration only. This is neither a production server nor a Commerce API emulator.
function demoApi(): Plugin {
const carts = new Map<string, CartOrder>();
return { name: 'storefront-demo-api', configureServer(server) {
server.middlewares.use('/api/commerce', async (req, res, next) => {
try {
const url = new URL(req.url ?? '/', 'http://localhost');
const send = (value: unknown, status = 200) => { res.statusCode = status; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(value)); };
if (url.pathname === '/config') return send({ cartScope: 'demo-v1', currencyCode: 'USD' });
if (url.pathname === '/products') return send({ skuGroups: { edges: products.map(node => ({ node })), pageInfo: { hasNextPage: false } } });
if (url.pathname.startsWith('/products/')) {
const product = products.find(item => item.id === url.pathname.split('/')[2]);
const color = url.searchParams.get('attributeValues');
return send({ skuGroup: product && color ? { ...product, skus: { edges: color === 'blue' ? [{ node: blue }] : color === 'clay' ? [{ node: clay }] : [], totalCount: 1 } } : product ?? null });
}
if (!url.pathname.startsWith('/cart')) return next();
if (req.headers['x-commerce-scope'] !== 'demo-v1') return send({ error: 'Store changed. Reload the page.' }, 409);
let body = '';
for await (const chunk of req) body += chunk;
const input = body ? JSON.parse(body) : {};
const [, , cartId, , itemId] = url.pathname.split('/');
let cart = carts.get(cartId);
if (req.method === 'GET') return send(cart ? { cart } : { error: 'Cart expired.' }, cart ? 200 : 404);
if (!cart && url.pathname !== '/cart') return send({ error: 'Cart expired.' }, 404);
cart ??= { id: randomUUID(), lineItems: [] };
if (req.method === 'POST') {
for (const item of input.lineItems ?? [input]) {
const selected = [mug, blue, clay].find(s => s.id === item.skuId);
if (!selected || selected === clay || !Number.isInteger(item.quantity) || item.quantity < 1) return send({ error: 'This variant is unavailable.' }, 400);
const price = selected.prices!.edges![0]!.node!.value!.value!;
const existing = cart.lineItems!.find(line => line.skuId === item.skuId);
if (existing) { existing.quantity = (existing.quantity ?? 0) + item.quantity; existing.totals = { subTotal: money(existing.quantity! * price) }; }
else cart.lineItems!.push({ id: randomUUID(), skuId: item.skuId, name: item.name, quantity: item.quantity, totals: { subTotal: money(price * item.quantity) } });
}
}
if (req.method === 'PATCH') {
const item = cart.lineItems!.find(line => line.id === itemId);
if (!item || !Number.isInteger(input.quantity) || input.quantity < 1) return send({ error: 'Invalid quantity.' }, 400);
const unit = item.totals!.subTotal!.value! / item.quantity!;
item.quantity = input.quantity; item.totals = { subTotal: money(unit * input.quantity) };
}
if (req.method === 'DELETE') cart.lineItems = cart.lineItems!.filter(line => line.id !== itemId);
const total = cart.lineItems!.reduce((sum, item) => sum + (item.totals?.subTotal?.value ?? 0), 0);
cart.totals = { subTotal: money(total), total: money(total) };
carts.set(cart.id!, cart); send({ cart });
} catch { res.statusCode = 500; res.end(JSON.stringify({ error: 'Demo server failed.' })); }
});
} };
}
export default defineConfig({ plugins: [demoApi()] });
3 changes: 3 additions & 0 deletions packages/commerce-server/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Copyright 2026 GoDaddy.com Operating Company, LLC

Licensed under the MIT License.
Loading
Loading