Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a468b8d
feat: add managed Commerce carts and checkout elements
wcole1-godaddy Sep 8, 2026
739d328
feat: refine managed cart and redirect to hosted checkout
wcole1-godaddy Sep 8, 2026
ead011f
fix: share host React and preserve typed cart quantities
wcole1-godaddy Sep 8, 2026
1eaddb7
chore: use patch bumps for experimental Commerce
wcole1-godaddy Sep 8, 2026
ec5f677
Restructure commerce cart client, elements, and React bindings
wcole1-godaddy Sep 9, 2026
7455ac9
Add commerce-elements example for end-to-end checkout runs
wcole1-godaddy Sep 9, 2026
59d40f5
Make commerce elements token-driven and add server-side session creation
wcole1-godaddy Sep 10, 2026
c6d490f
Fix quantity stepper hover radius and use brand teal for the drawer logo
wcole1-godaddy Sep 10, 2026
92d4f9f
Remove the drawer promo code form for now
wcole1-godaddy Sep 10, 2026
46a238a
Tighten drawer line items without variants
wcole1-godaddy Sep 10, 2026
8e5a758
Use the SKU label as the cart line name
wcole1-godaddy Sep 10, 2026
352cb60
Align drawer line items to the content grid
wcole1-godaddy Sep 10, 2026
c05e99c
Keep drawer thumbnails square and center line content against them
wcole1-godaddy Sep 10, 2026
ac314c5
Clamp drawer line titles to two lines and top-align line content
wcole1-godaddy Sep 10, 2026
6162025
Show a loading indicator on commerce buttons while their action runs
wcole1-godaddy Sep 10, 2026
7526417
Drop the divider under the cart drawer header
wcole1-godaddy Sep 10, 2026
4780ac0
Remove the separators between cart drawer line items
wcole1-godaddy Sep 10, 2026
f388b7f
Put the cart summary and footer on an 8px rhythm
wcole1-godaddy Sep 10, 2026
54dc0ce
Create checkout sessions on the server and gate browser tokens
wcole1-godaddy Sep 10, 2026
46ed286
Add the managed CDN runtime build for @godaddy/commerce
wcole1-godaddy Sep 10, 2026
63ab378
Give the drawer gutter a 16px default
wcole1-godaddy Sep 10, 2026
d98fb13
Keep the managed CDN runtime out of the public package
wcole1-godaddy Sep 10, 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
6 changes: 6 additions & 0 deletions .changeset/managed-commerce-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@godaddy/commerce': patch
'@godaddy/react': patch
---

Add managed Commerce carts and one-time checkout through gddy web components, optional React bindings, and a public integration contract for a separately managed CDN. Expose browser Commerce API primitives and support embedded checkout completion, session isolation, and scoped UI containers. Checkout sessions are created through a server-side `createSession` hook that receives the payment reference; browser OAuth tokens require an explicit `dangerouslyAllowBrowserToken` opt-in.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ This monorepo contains the following packages:
| [`eslint-config-godaddy-react-typescript`](/packages/eslint-config-godaddy-react-typescript) | ESLint configuration for React + TypeScript applications | [![npm](https://img.shields.io/npm/v/eslint-config-godaddy-react-typescript.svg)](https://www.npmjs.com/package/eslint-config-godaddy-react-typescript) |
| [`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/commerce`](/packages/commerce) | Managed cart and checkout web components | In development |
| [`@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) |

## Why GoDaddy JavaScript?
Expand Down
36 changes: 36 additions & 0 deletions examples/commerce-elements/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Commerce elements example

A plain Vite page that exercises `@godaddy/commerce` end to end against a real
Commerce environment: `gddy-add-to-cart`, `gddy-cart-button` with the shared
drawer, `gddy-buy-now`, and hosted checkout handoff.

Cart calls need only the public storefront identifiers. Checkout needs the
OAuth client, so `vite.config.ts` adds a dev-only `POST /api/commerce/checkout`
route that performs the client-credentials grant and creates the hosted session
on the server side of Vite, returning only the session id and URL. Neither the
client secret nor an access token ever reaches the browser. A real site owns the
equivalent route on its own server and prices standalone payment references
there.

## Run

```bash
cp examples/commerce-elements/env.sample examples/commerce-elements/.env.local
# fill in the values, then
pnpm --filter commerce-elements dev
```

Open http://localhost:5180. The `dev` script builds `@godaddy/localizations`,
`@godaddy/react`, and `@godaddy/commerce` first; use `dev:only` to skip that.

`window.gddy` is the configured client, so the flow can also be driven from the
browser console, for example `await gddy.addItem('<sku-id>')`.

## CDN page

`/cdn.html` is the same storefront built the way a merchant would build it: a
config object, a `<script defer>` tag, and `gddy-*` tags in plain HTML. Nothing
is bundled here. `VITE_COMMERCE_CDN_URL` in `.env.local` selects the runtime:
a deployed `/v1/commerce.js`, or one served locally from the protected
checkout repository that builds it. The page shows the runtime version and
commit from the `gddy:ready` event.
85 changes: 85 additions & 0 deletions examples/commerce-elements/cdn.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Commerce elements from the CDN</title>
<link rel="stylesheet" href="/src/styles.css" />
<script>
// Everything a storefront needs: one config object and one script tag.
// Vite fills the VITE_ placeholders below from .env.local.
window.gddyCommerceConfig = {
clientId: '%VITE_GODADDY_CLIENT_ID%',
storeId: '%VITE_GODADDY_STORE_ID%',
channelId: '%VITE_GODADDY_CHANNEL_ID%',
apiHost: '%VITE_GODADDY_API_HOST%'.replace(/^https?:\/\//, ''),
locale: 'en-US',
createSession: async input => {
const response = await fetch('/api/commerce/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error('Checkout is unavailable');
return response.json();
},
};
window.addEventListener('gddy:ready', event => {
const { version, commit } = event.detail.release;
document.getElementById('release').textContent =
`runtime ${version} @ ${commit.slice(0, 7)}`;
});
window.addEventListener('gddy:error', event => {
const item = document.createElement('li');
const { error } = event.detail ?? {};
item.textContent = String(error?.message ?? error ?? 'Unknown error');
document.getElementById('errors').append(item);
});
</script>
<script defer src="%VITE_COMMERCE_CDN_URL%"></script>
</head>
<body>
<main class="page">
<header class="bar">
<div>
<h1>Commerce elements from the CDN</h1>
<p class="muted" id="release">Waiting for the runtime…</p>
</div>
<gddy-cart-button>Basket</gddy-cart-button>
</header>
<section class="grid" id="products" aria-label="Products"></section>
<ul class="errors" id="errors" aria-label="Runtime errors"></ul>
<p class="muted">
The runtime comes from <code>VITE_COMMERCE_CDN_URL</code>. The
<a href="/">bundled page</a> uses the npm entry points instead.
</p>
</main>
<script>
// Product cards from VITE_COMMERCE_DEMO_SKUS (`id|label,...`). The elements
// stay disabled until the runtime registers them and configuration completes.
const grid = document.getElementById('products');
for (const entry of '%VITE_COMMERCE_DEMO_SKUS%'.split(',')) {
const [id, label] = entry.trim().split('|');
if (!id) continue;
const card = document.createElement('article');
card.className = 'card';
const title = document.createElement('h2');
title.textContent = label || id;
const code = document.createElement('p');
code.className = 'muted mono';
code.textContent = id;
const actions = document.createElement('div');
actions.className = 'actions';
const add = document.createElement('gddy-add-to-cart');
add.setAttribute('sku-id', id);
add.textContent = 'Add to cart';
const buy = document.createElement('gddy-buy-now');
buy.setAttribute('sku-id', id);
buy.textContent = 'Buy now';
actions.append(add, buy);
card.append(title, code, actions);
grid.append(card);
}
</script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/commerce-elements/env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Public storefront identifiers (safe in the browser)
VITE_GODADDY_API_HOST=api.godaddy.com
VITE_GODADDY_STORE_ID=
VITE_GODADDY_CLIENT_ID=
VITE_GODADDY_CHANNEL_ID=
VITE_GODADDY_STORE_NAME=

# Products to render: comma-separated `skuId|label`
VITE_COMMERCE_DEMO_SKUS=

# Optional hosted-checkout navigation overrides (default: this page)
# VITE_COMMERCE_RETURN_URL=
# VITE_COMMERCE_SUCCESS_URL=
# Optional GoDaddy Payments application override for non-production card forms
# VITE_COMMERCE_GOPAY_APP_ID=

# Server-side only: used by the dev checkout route in vite.config.ts. Never expose to the page.
GODADDY_OAUTH_CLIENT_ID=
GODADDY_OAUTH_CLIENT_SECRET=

# Where cdn.html loads the managed runtime from: a deployed /v1/commerce.js, or
# the checkout repository's local elements server.
VITE_COMMERCE_CDN_URL=http://localhost:5181/v1/commerce.js
12 changes: 12 additions & 0 deletions examples/commerce-elements/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GoDaddy Commerce elements</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions examples/commerce-elements/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "commerce-elements",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"prepare:deps": "pnpm --filter @godaddy/localizations build && pnpm --filter @godaddy/react build && pnpm --filter @godaddy/commerce build",
"dev": "pnpm run prepare:deps && vite",
"dev:only": "vite",
"build": "pnpm run prepare:deps && tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@godaddy/commerce": "workspace:*",
"react": "19.2.0",
"react-dom": "19.2.0"
},
"devDependencies": {
"@godaddy/react": "workspace:*",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.7.0",
"typescript": "~5.7.3",
"vite": "^6.4.1"
}
}
187 changes: 187 additions & 0 deletions examples/commerce-elements/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {
type CommerceClient,
type CommerceConfig,
configureCommerce,
getCommerce,
itemCount,
} from '@godaddy/commerce';
import {
AddToCartButton,
BuyNowButton,
CartButton,
useCart,
} from '@godaddy/commerce/react';
import '@godaddy/commerce/styles.css';
import { StrictMode, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemePanel, useTheme } from './theme-panel';
import './styles.css';

const env = import.meta.env;
const apiHost = (env.VITE_GODADDY_API_HOST || 'api.godaddy.com').replace(
/^https?:\/\//,
''
);

/** `id|label` pairs, comma separated. */
const products: { id: string; label: string }[] = String(
env.VITE_COMMERCE_DEMO_SKUS || ''
)
.split(',')
.map((entry: string) => entry.trim())
.filter(Boolean)
.map((entry: string) => {
const [id, label] = entry.split('|');
return { id, label: label || id };
});

const checkout: CommerceConfig['checkout'] = {
storeName: env.VITE_GODADDY_STORE_NAME || undefined,
enableBillingAddressCollection: true,
enableShippingAddressCollection: false,
enableShipping: false,
enableLocalPickup: false,
enablePhoneCollection: false,
enableTaxCollection: false,
enablePaymentMethodCollection: true,
paymentMethods: {
card: { processor: 'godaddy', checkoutTypes: ['standard'] },
},
...(env.VITE_COMMERCE_RETURN_URL && { returnUrl: env.VITE_COMMERCE_RETURN_URL }),
...(env.VITE_COMMERCE_SUCCESS_URL && {
successUrl: env.VITE_COMMERCE_SUCCESS_URL,
}),
...(env.VITE_COMMERCE_GOPAY_APP_ID && {
experimental_rules: {
gopay_override: { enabled: true, goPayAppId: env.VITE_COMMERCE_GOPAY_APP_ID },
},
}),
};

// Configure once; Vite hot reloads re-run this module, so reuse the singleton.
const client = (() => {
try {
return getCommerce();
} catch {
return null;
}
})() ?? configureCommerce({
clientId: env.VITE_GODADDY_CLIENT_ID,
storeId: env.VITE_GODADDY_STORE_ID,
channelId: env.VITE_GODADDY_CHANNEL_ID,
apiHost,
locale: 'en-US',
// The server route creates the session with its OAuth client; no token
// reaches the page. In production the server also owns the `checkout`
// settings below instead of accepting them from the browser.
createSession: async input => {
const response = await fetch('/api/commerce/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = (await response.json()) as {
id?: string;
url?: string;
error?: string;
};
if (!response.ok) throw new Error(json.error || 'Checkout route failed');
return json;
},
checkout,
});
// Handy for driving the client from the browser console.
(window as Window & { gddy?: CommerceClient }).gddy = client;

function useElementErrors() {
const [errors, setErrors] = useState<string[]>([]);
useEffect(() => {
const onError = (event: Event) => {
const { error } = (event as CustomEvent<{ error: unknown }>).detail;
const message = error instanceof Error ? error.message : String(error);
setErrors(list => [...list.slice(-4), message]);
};
document.addEventListener('gddy:error', onError);
return () => document.removeEventListener('gddy:error', onError);
}, []);
return errors;
}

function App() {
const cart = useCart();
const errors = useElementErrors();
const { theme, setTheme, css } = useTheme();
return (
<div className='layout'>
<main className='page'>
<header className='bar'>
<div>
<h1>Commerce elements</h1>
<p className='muted'>
{apiHost} · store {env.VITE_GODADDY_STORE_ID?.slice(0, 8)}…
</p>
</div>
<CartButton className='cart'>Basket</CartButton>
</header>

<section className='grid' aria-label='Products'>
{products.length === 0 && (
<p className='muted'>
Set <code>VITE_COMMERCE_DEMO_SKUS</code> in <code>.env.local</code>.
</p>
)}
{products.map(product => (
<article className='card' key={product.id}>
<h2>{product.label}</h2>
<p className='muted mono'>{product.id}</p>
<div className='actions'>
<AddToCartButton skuId={product.id}>Add to cart</AddToCartButton>
<BuyNowButton skuId={product.id}>Buy now</BuyNowButton>
</div>
</article>
))}
</section>

<section className='debug' aria-label='Client state'>
<h2>useCart()</h2>
<dl>
<dt>status</dt>
<dd data-testid='status'>{cart.status}</dd>
<dt>pending</dt>
<dd data-testid='pending'>{cart.pending}</dd>
<dt>items</dt>
<dd data-testid='items'>{itemCount(cart.cart)}</dd>
<dt>cart id</dt>
<dd className='mono' data-testid='cart-id'>
{cart.cart?.id ?? '—'}
</dd>
<dt>subtotal</dt>
<dd data-testid='subtotal'>
{cart.cart?.totals?.subTotal
? `${cart.cart.totals.subTotal.value} ${cart.cart.totals.subTotal.currencyCode}`
: '—'}
</dd>
<dt>error</dt>
<dd data-testid='error'>{cart.error?.message ?? '—'}</dd>
<dt>checkout</dt>
<dd className='mono'>{cart.checkout?.url ?? '—'}</dd>
</dl>
{errors.length > 0 && (
<ul className='errors' aria-label='Element errors'>
{errors.map((message, index) => (
<li key={`${index}-${message}`}>{message}</li>
))}
</ul>
)}
</section>
</main>
<ThemePanel theme={theme} setTheme={setTheme} css={css} />
</div>
);
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
Loading