# @sonordev/commerce-kit

Products, services, event tickets and memberships for [Sonor](https://sonor.io)-powered Next.js sites, with checkout built in. Most sites don't sell anything online, and they don't need it. If yours does, you manage what you sell in the Sonor dashboard, and the kit shows it on your pages and carries the visitor through paying.

## Install

```bash
pnpm add @sonordev/commerce-kit
```

commerce-kit builds on [site-kit](https://sonor.dev/site-kit). It needs site-kit 8 or newer as a peer dependency, so it shares the one copy your site already has. A site that's still on site-kit 7 raises it in the same step:

```bash
pnpm add @sonordev/site-kit@^8 @sonordev/commerce-kit
```

Set site-kit up first, as in [Get started](https://sonor.dev/guides/getting-started): `SiteKitLayout` in your root layout and `SONOR_API_KEY` in `.env.local`. commerce-kit has nothing of its own to configure and adds no environment variable. It needs React 19 and Node 20.19 or newer, and it's ESM only. Next 16 is an optional peer that only `memberships/server` uses.

Checkout uses the payment processor you connect to the project in Sonor, Square or Stripe. Memberships and the inline ticket checkout (`EventCheckout`) need Square.

## Quick start

A product page and an event checkout. On the site, neither needs more than `SiteKitLayout`, because the components reach Sonor through the credential it already published.

```tsx
// app/shop/[slug]/page.tsx, e.g. /shop/house-blend
import { ProductPage } from '@sonordev/commerce-kit/commerce'

export default async function Product({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  return <ProductPage slug={slug} />
}
```

`ProductPage` loads the product by its slug and shows the gallery, variants, size chart, related products and an inline checkout.

```tsx
// components/ticket-checkout.tsx
'use client'

import { useEffect, useState } from 'react'
import { EventCheckout, fetchOffering } from '@sonordev/commerce-kit/commerce'
import type { CommerceOffering } from '@sonordev/commerce-kit'

export function TicketCheckout({ slug }: { slug: string }) {
  const [event, setEvent] = useState<CommerceOffering | null | undefined>(undefined)

  useEffect(() => {
    fetchOffering(slug).then(setEvent)
  }, [slug])

  if (event === undefined) return <p>Loading tickets…</p>
  if (event === null) return <p>We couldn’t load that event.</p>
  return <EventCheckout event={event} />
}
```

Render it from any page with `<TicketCheckout slug="saturday-cupping" />`. `EventCheckout` shows a row for each ticket tier (or a single ticket when the event has no tiers), collects the buyer's details, and takes the card in the page with Square, in one charge.

Both examples fetch in the browser. The server entry, `@sonordev/commerce-kit/commerce/server`, loads the same data on the server, for static paths, 404s, metadata and pages that render with the product already in the HTML: see [Render on the server](https://sonor.dev/commerce-kit/commerce#render-on-the-server).

## Modules

| Module                                                    | Import                                                                      | What it does                                                                                             |
| --------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [Commerce](https://sonor.dev/commerce-kit/commerce)       | `@sonordev/commerce-kit/commerce`, `/commerce/server` and `/commerce/utils` | Product pages, grids and cards, services, event calendars and tickets, and checkout.                     |
| [Memberships](https://sonor.dev/commerce-kit/memberships) | `@sonordev/commerce-kit/memberships` and `/memberships/server`              | Square-billed membership plans, an emailed sign-in link, members-only pages, and canceling on your site. |

The package has six entries. Import anything that runs from one of the five module entries. The root has only types, so a stray root import can't pull checkout or server code into a page.

| Entry                                       | Side        | What's in it                                                                                                                                                                             |
| ------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@sonordev/commerce-kit`                    | Types only  | `CommerceOffering`, `CommerceCategory`, `CommerceVariant`, `CommerceSchedule`, `OfferingType`, `Cart`, `CartItem`, `EventCheckoutProps` and `CheckoutLineItem`. Nothing that runs.       |
| `@sonordev/commerce-kit/commerce`           | Client      | Components, hooks and the browser calls.                                                                                                                                                 |
| `@sonordev/commerce-kit/commerce/server`    | Server only | `getOfferings`, `getOfferingBySlug`, `getUpcomingEvents`, `getNextEvent`, their `*Result` variants, the static-path helpers, `generateOfferingMetadata` and `createServerConfig`.        |
| `@sonordev/commerce-kit/commerce/utils`     | Anywhere    | `formatPrice`, `formatDate` and the other formatting helpers, plus `icsForEvent`, `googleCalendarUrl` and `icsResponse`. No React, so a route handler or server component can call them. |
| `@sonordev/commerce-kit/memberships`        | Client      | `MembershipCheckoutButton`, `MemberSignInForm`, `startMembershipCheckout` and `requestMemberSignIn`.                                                                                     |
| `@sonordev/commerce-kit/memberships/server` | Server only | `createMembers`, `getMembershipPlans`, `getMembershipByEmail` and `cancelMembershipByEmail`.                                                                                             |

## How it fits with site-kit

commerce-kit is a kit: it enhances site-kit and never rebuilds it. A site has one transport to Sonor, one credential and one analytics pipeline, and commerce-kit uses those instead of carrying copies. It imports from exactly three site-kit entries, each documented:

| What                   | From site-kit                                                                                                           | How commerce-kit uses it                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser calls to Sonor | `sonorFetch`, `getClientApiConfig` and `readAnalyticsIdentity`, from `@sonordev/site-kit/runtime`                       | Every call from the browser goes through them: the site credential, retries with a timeout, one refresh of an expired token, and a pause after an authentication failure. Calls that start a payment, a registration or a membership are never retried on their own. Checkout and registration also send the visitor's analytics session id, when there's one, so Sonor can attribute a sale to the visit. |
| Server calls to Sonor  | `serverApiFetch`, `resolveServerApiConfig`, `SITE_CACHE_TAGS` and `isNextControlFlow`, from `@sonordev/site-kit/server` | `commerce/server` and `memberships/server` read `SONOR_API_KEY` and `SONOR_API_URL` through them, with a time limit and retries, and cache commerce data under the `commerce` tag so an edit in Sonor refreshes the pages that show it.                                                                                                                                                                    |
| Analytics              | `trackEvent`, from `@sonordev/site-kit/analytics`                                                                       | The product and checkout components report shopping events with it (see [Analytics](https://sonor.dev/commerce-kit/commerce#analytics)). An event waits for site-kit's analytics to load, and a site with analytics turned off sends nothing.                                                                                                                                                              |

site-kit is a peer dependency and is never bundled into commerce-kit, so your site has one copy of each of those. A few things stay where they were:

- **One credential.** The only variable is `SONOR_API_KEY`, kept on the server. The components use the credential `SiteKitLayout` already published to the browser.
- **Your CSS keeps working.** The components keep their `site-kit-` class prefix (`site-kit-product-grid`, `site-kit-calendar-event` and the rest) because sites style those names. The prefix is a promise to your stylesheets, not a statement about which package ships the component.
- **Brand tokens come from site-kit.** The components read the same `--sk-*` tokens as forms and booking, such as `--sk-primary`. Import `@sonordev/site-kit/brand.css` once for the defaults and override them in your own CSS.

Building another kit? Take the transport from `@sonordev/site-kit/runtime`, as commerce-kit does: see [Building a kit](https://sonor.dev/site-kit/runtime). The other kits build on site-kit's server and client entries, and [Kits](https://sonor.dev/guides/kits) lists them.

## Moving from site-kit 7

Through site-kit 7.4, commerce lived inside site-kit at `@sonordev/site-kit/commerce`. In site-kit 8 it lives here, and nothing is left behind at the old paths. The code is the same: the package name changes and the subpath stays exactly as it was. Memberships never shipped in a site-kit release, so it's new to everyone.

### The short version

```bash
pnpm add @sonordev/site-kit@^8 @sonordev/commerce-kit
npx sonor-setup codemod --only site-kit-8           # dry run: prints the changes, writes nothing
npx sonor-setup codemod --only site-kit-8 --write   # applies them
pnpm build
```

The codemod needs sonor-setup 7.3.1 or newer. `npx` fetches the latest unless the project already has sonor-setup installed; if it does, update that copy first. The codemod reads every TypeScript and JavaScript file in the project, wherever it sits, except `node_modules`, build output and `public/`. It rewrites the imports in the table below, adds `@sonordev/commerce-kit` to your package.json and raises site-kit's range to `^8`, and flags anything it can't rewrite, such as a `require()` of an old path (commerce-kit is ESM only) or an `offerings` option with no reader, so you can finish those by hand.

- Nothing is written until you pass `--write`.
- `--only site-kit-8` runs just this move. Leave it off to run every move.
- `--check` exits 1 while a move is pending and never writes, which suits CI. A manual follow-up counts as pending.
- `--path <file>` limits the run to one file.

The originals of files it writes go to a temporary folder outside your project, and running it twice changes nothing the second time. In a monorepo, run it in each app that imports commerce.

No codemod handy? The swap is mechanical: change the package name in each import and add commerce-kit to package.json. If your package manager reports that commerce-kit needs `@sonordev/site-kit >=8.0.0` (npm stops with an `ERESOLVE` error, pnpm and Yarn warn), site-kit is still on 7.

### What changed

| Before (site-kit 7)                                         | After (site-kit 8 and commerce-kit 1)                                                                                                                 |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@sonordev/site-kit/commerce`                               | `@sonordev/commerce-kit/commerce`                                                                                                                     |
| `@sonordev/site-kit/commerce/server`                        | `@sonordev/commerce-kit/commerce/server`                                                                                                              |
| `@sonordev/site-kit/memberships`                            | `@sonordev/commerce-kit/memberships`                                                                                                                  |
| `@sonordev/site-kit/memberships/server`                     | `@sonordev/commerce-kit/memberships/server`                                                                                                           |
| Commerce types imported from the root, `@sonordev/site-kit` | The same names, from `@sonordev/commerce-kit`                                                                                                         |
| `sonorMcpServer({ offerings: { path: '/shop' } })`          | `sonorMcpServer({ offerings: { path: '/shop', read: getOfferingsResult } })`, with `getOfferingsResult` from `@sonordev/commerce-kit/commerce/server` |

The memberships rows only matter if you tried a pre-release build. The root types are `CommerceOffering`, `CommerceCategory`, `CommerceVariant`, `CommerceSchedule`, `OfferingType`, `Cart`, `CartItem`, `EventCheckoutProps` and `CheckoutLineItem`.

```tsx
// Before (site-kit 7)
import { OfferingList, ProductPage } from '@sonordev/site-kit/commerce'
import { getOfferingBySlugResult } from '@sonordev/site-kit/commerce/server'
import type { CommerceOffering } from '@sonordev/site-kit'

// After (site-kit 8 and commerce-kit)
import { OfferingList, ProductPage } from '@sonordev/commerce-kit/commerce'
import { getOfferingBySlugResult } from '@sonordev/commerce-kit/commerce/server'
import type { CommerceOffering } from '@sonordev/commerce-kit'
```

The one change that isn't a package-name swap is the opt-in `list_offerings` agent tool, which lists priced products, services and events. It used to read the commerce fetchers directly. It takes its reader as an option now, so site-kit doesn't depend on commerce-kit:

```ts
// Before (site-kit 7)
sonorMcpServer({ businessName: 'Example Studio', offerings: { path: '/shop' } })

// After (site-kit 8)
import { getOfferingsResult } from '@sonordev/commerce-kit/commerce/server'

sonorMcpServer({ businessName: 'Example Studio', offerings: { path: '/shop', read: getOfferingsResult } })
```

`path` is still optional and defaults to `/shop`. `read` is `getOfferingsResult`: when a request fails, the tool tells the agent the catalog is unavailable instead of reporting an empty one. The plain `getOfferings` from the same entry also works, but it reads an outage as an empty catalog. If `read` is missing, `sonorMcpServer` throws when the server is built, with a message that says what to pass. A site that doesn't pass `offerings` changes nothing, and neither does `list_services`: it reads the site's core data, not commerce.

Your package.json, before and after:

```diff
 "dependencies": {
+  "@sonordev/commerce-kit": "^1.0.0",
-  "@sonordev/site-kit": "^7.4.0",
+  "@sonordev/site-kit": "^8.0.0",
 }
```

### What didn't change

- **The components.** Names, props, hooks and what they render are the same code, now built and shipped from commerce-kit.
- **Your CSS.** The `site-kit-` class names, the `data-category` and `data-offering-type` attributes, and the `--sk-*` brand tokens are as they were.
- **Your credential.** `SONOR_API_KEY` is still the only variable. There's no new one.
- **Sonor.** Your products, events and payment connection stay as they are, and the components call the same Sonor endpoints.
- **The rest of site-kit.** `@sonordev/site-kit/commerce` and `/commerce/server` are the only entries site-kit 8 removes.

A few things are different besides the import path. When a server fetch fails, the log line starts with `[commerce-kit]` instead of `[site-kit/commerce]`, so update any log filter or alert that matches the old prefix. The shopping events (`product_view`, `add_to_cart`, `begin_checkout`, `checkout_start` and `purchase`) now reach Sonor; on a site using `SiteKitLayout`, site-kit 7 never sent them. Checkout no longer fails in a browser set to block site data. On the server, `commerce/server` is server only now, returns whole offerings (the gallery, size chart and options included) and caches each response for 60 seconds under the `commerce` tag, which Sonor expires when you edit an offering; pass `{ revalidate: 0 }` in the config if a page needs every request fresh. A product marked sold out keeps its page and comes back with `status: 'sold_out'`, so if your own markup draws a product, check `status` before showing a buy button. And the package has its own peers: site-kit 8 or newer, React 19, and Next 16 as an optional peer. See [Versions](#versions).

### Check that it worked

```bash
npx sonor-setup codemod --only site-kit-8 --check   # exits 0 when nothing is left to rewrite or finish by hand

grep -rnE "@sonordev/site-kit/(commerce|memberships)" . \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' \
  --exclude-dir=node_modules --exclude-dir=.next

grep -rnE "from ['\"]@sonordev/site-kit['\"]" . \
  --include='*.ts' --include='*.tsx' --exclude-dir=node_modules --exclude-dir=.next
```

1. The check lists nothing left to rewrite or finish by hand, and the first search finds no old paths. The root is types only apart from `SITE_KIT_VERSION`, so the second search finds only type imports and that constant, and none of them should name the root types listed above.
2. `pnpm build` passes. An import you missed fails the build and names the path, because nothing is left behind at the old paths.
3. `pnpm ls @sonordev/site-kit @sonordev/commerce-kit` shows site-kit 8.x and commerce-kit 1.x.
4. Load a product page and an event page, and start a checkout. If you sell memberships, load the sign-in page too. In the server log, lines that start with `[commerce-kit]` are fetch errors from `commerce/server`.
5. `npx sonor-setup verify` still exits 0.

### Questions

#### Do I need commerce-kit if I do not sell anything?

No. If your site doesn't import anything from `@sonordev/site-kit/commerce`, there's nothing to move: raise site-kit to `^8` whenever you like.

#### Can I stay on site-kit 7?

Yes. A `^7` range never picks up 8, and everything in 7.4 keeps working, commerce included, at `@sonordev/site-kit/commerce`. Two things to know: commerce-kit needs site-kit 8, so a site on 7 can't install it, and memberships exists only in commerce-kit. If an update raised your range to 8 before you were ready, pin it back with `pnpm add @sonordev/site-kit@^7.4.0`.

#### Will visitors notice the change?

Only for the better. The components render the same markup with the same class names, call the same Sonor endpoints with the same credential, and check out the same way. What a visitor can see: a product marked sold out keeps its page, shown as unavailable, instead of a 404, and an edit in Sonor reaches your pages sooner. In your server log, fetch errors start with `[commerce-kit]`.

#### Does the bundle get smaller?

No, and it was never meant to. site-kit's root entry was already types only and nothing in its core referenced commerce, so a visitor to a site without commerce never downloaded any of it. On a site that does sell, the components are the same code, so a page loads about what it did before. What shrinks is site-kit's package: about 8% fewer bytes to download and about 13% fewer on disk than 7.4.0. That's install size, not page weight.

#### Why is this site-kit 8, and not a 7.x release with the old paths still working?

Taking a published import path away is a breaking change, so it's a new major version, and a `^7` range wouldn't pick it up by surprise. Keeping the old paths alive would have meant a second copy of the code, or site-kit depending on a package that depends back on it. It wouldn't have saved you an edit either, since commerce-kit has to be added to package.json either way.

## Versions

| @sonordev/commerce-kit | Needs @sonordev/site-kit | Also needs                                                                                   |
| ---------------------- | ------------------------ | -------------------------------------------------------------------------------------------- |
| 1.x                    | 8.0.0 or newer           | React 19 and Node 20.19 or newer. Next 16 is optional and only `memberships/server` uses it. |

On site-kit 7 and earlier, commerce is built in at `@sonordev/site-kit/commerce` and commerce-kit isn't an option. What changed in each release is in the [changelog](https://sonor.dev/commerce-kit/changelog).
