Skip to content
Draft
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
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@
# Storefront details
STOREFRONT_DOMAIN=your-store.myshopify.com
STOREFRONT_ACCESS_TOKEN=your-public-storefront-access-token

# Universal Checkout web sample (optional). A comma-separated list preselects
# shops on page load; leave the key unset to use STOREFRONT_DOMAIN instead.
# Set it to an empty string to start without selected shops.
# CHECKOUT_KIT_UC_SHOP_DOMAINS=store-one.myshopify.com,store-two.myshopify.com
# Additional exact domains approved for manually added shops.
CHECKOUT_KIT_UC_ALLOWED_SHOP_DOMAINS=
# Trusted session service URLs are server-only. Configure the environments you use.
# The production endpoint must use the shop.app host.
CHECKOUT_KIT_UC_SESSION_CREATE_URL=
CHECKOUT_KIT_UC_DEVELOPMENT_SESSION_CREATE_URL=
# The exact development continuation hostname is sent at runtime to the local page.
VITE_CHECKOUT_KIT_UC_DEVELOPMENT_CONTINUATION_HOST=

# Optional Apple Pay merchant identifiers used by accelerated checkout flows.
# Will be read in sample apps as APPLE_PAY_MERCHANT_IDENTIFIER
REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=
Expand Down
139 changes: 121 additions & 18 deletions platforms/web/sample/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
# Web Component Playground

A development harness for the `<shopify-checkout>` web component. It imports the same entry as published consumers (`@shopify/checkout-kit`, aliased to `../src/index.ts` in dev), registers the custom element, and logs Checkout Kit lifecycle events.
This two-page Vite playground exercises the single-checkout
`<shopify-checkout>` and experimental Universal Checkout
`<shopify-universal-checkout>` web components. Each page imports its package
entry point, aliased to local sources by Vite. The Universal API and sample
are an experimental preview and have not been released as a supported API.

## Run locally

```bash
cd platforms/web
From `platforms/web`, install the workspace dependencies and start Vite:

```sh
pnpm install
pnpm sample
```

Vite serves the single-checkout sample at `http://localhost:5173/` and the Universal Checkout sample at `http://localhost:5173/universal.html`. The topbar links the two pages.

## Universal Checkout sample

The Universal page uses the same three-panel layout and cart controls as the single-checkout page. Enter a storefront domain and choose **Add shop** to load its public `/products.json` catalog immediately. Each selected shop has its own cart, product loading status, Retry and Remove controls, and an individual cart permalink preview. You can add multiple shops, including products with the same variant ID in different shops; quantities remain separate.
Open the single-checkout or Universal Checkout URL printed by Vite. The
navigation at the top of either page switches between them. For the Universal
page, configure the local session environment as described below before
starting Vite.

Use a bare domain such as `store-one.myshopify.com` or its HTTPS homepage. The form rejects credentials, ports, other schemes, paths, and duplicate normalized domains. The selected domains, carts, and links stay in memory. Only presentation settings, panel collapse choices, and panel widths use Universal-specific local storage keys.
## Single checkout

The public products endpoint does not supply a currency code through this sample's catalog helper, so prices are shown without a currency symbol and carts are counted by items rather than combined into a cross-shop monetary total. A failed or empty shop remains selected and must be retried or removed before a Universal Checkout URL can be created. The session creation and Open controls are added in the following sample increments; this page currently stops at cart previews.

## What the demo shows
### What the demo shows

The default flow highlights a multi-item cart use case:

Expand All @@ -38,7 +41,7 @@ https://your-store.myshopify.com/cart/123:1,456:2

You can also choose **Use existing checkout source** in Settings. In that mode, the storefront and cart builder are hidden, and the center workspace shows a manual URL flow with its own **Open checkout** button.

## Panels
### Panels

- **Settings** — persisted storefront domain, flow, target (`popup` | `auto`), appearance (default `storefront` | `app:light` | `app:dark` | `app:automatic` | `storefront`), and log-level (`debug` | `warn` | `error` | `none`) settings. The storefront domain appears first because the cart builder cannot load products without it.
- **Center workspace** — build mode shows a storefront-style product grid plus sticky cart banner; manual mode shows a focused checkout URL/cart permalink input.
Expand All @@ -53,7 +56,7 @@ snapshots do not produce another update. The `error` event exposes a
`{code, message}` error. Checkout stays open for recoverable errors and closes
automatically only for errors with `unrecoverable` severity.

## Troubleshooting product loading
### Troubleshooting product loading

The demo relies on the public `/products.json` endpoint. If product loading fails:

Expand All @@ -63,12 +66,112 @@ The demo relies on the public `/products.json` endpoint. If product loading fail
- Confirm the storefront is reachable from your browser.
- Use **Use existing checkout source** if you already have a checkout URL or cart permalink and do not need product loading.

The single-checkout page does not call Storefront API `cartCreate`; it uses cart permalinks so the multi-item flow can be exercised without a Storefront access token. Universal session creation will use shop carts in its next increment.
This sample does not currently call Storefront API `cartCreate`; it uses cart permalinks so the multi-item flow can be exercised without a Storefront access token.

## Universal Checkout

> [!IMPORTANT]
> This is an experimental preview. Its API and end-to-end buyer flow remain
> subject to contract and live validation.

The Universal page creates an independent Storefront cart for each selected shop, creates a Universal Checkout session from the resulting cart IDs, and opens its keyed continuation URL with Checkout Kit. Each shop's cart permalink on this page is only a preview; cart IDs are the session inputs.

1. Configure the sample shops and session service in the repository root `.env` or `.env.local` as described below, then start Vite. Open the Universal page. In **Checkout session**, choose **Production** or **Local development** and a **Buyer country** supported by all test shops.
2. The configured shops appear in **Shops** and load products automatically. Add or remove storefront domains as needed. Products load from each shop's public `/products.json` endpoint. Choose available products and quantities for every selected shop. At most 15 shops can be in one session, with at most 50 distinct variants per shop cart. Every selected shop needs a nonempty cart.
3. Click **Create Universal Checkout URL**. The local Vite adapter calls Storefront API `2026-07` to create one cart per shop, checks that all cart currencies match, then sends their cart IDs to the Universal Checkout session endpoint. The keyed URL is held in page state and assigned to the component's `src` for opening; it is not visibly displayed or persisted. Changing shops, products, country, or environment invalidates it; create it again after an edit.
4. Leave **Use the URL created from selected carts** selected, then click **Open Universal Checkout**. This click calls the component's `open()` method directly, so the browser can open the checkout window. Sign in to the corresponding Shop Pay environment to continue as a buyer.
5. Watch **Resource state** and **Event timeline** in the checkout overlay. The **Events** panel on the page mirrors the received events. Expand a timeline entry for its redacted status, revision, and aggregate summary. **Focus checkout**, **Close checkout**, and **Clear events** operate on the current presentation; closing the window is a separate event from checkout completion.

The event view uses the preview Checkout Kit `start`, `update`, `complete`, `error`, and `close` events. It shows only allowlisted fields and labels resources as `Shop 1`, `Shop 2`, and so on. The timeline does not copy raw checkout payloads, shop IDs, cart IDs, order details, or the keyed URL. Opening a new presentation starts a fresh timeline; **Clear events** clears the timeline while keeping the current resource state.

### Environment configuration

The Vite development and preview servers read the same repository root `.env`
and `.env.local` used by the other sample apps. `.env.local` overrides `.env`;
exported shell values are a fallback for keys missing from both files. Restart
the server after changing either file. No shell export or sample rebuild is
needed for a file change.

Set `CHECKOUT_KIT_UC_SHOP_DOMAINS` to a comma-separated list of up to 15 exact
shop domains. When the key is absent, the sample uses `STOREFRONT_DOMAIN`.
Set the key to an empty value to start without selected shops. Configured shops
are selected on each page load; adding or removing shops through the UI affects
only the current page. Cart contents are never initialized or persisted.

These are synthetic examples for the root `.env.local`:

```dotenv
CHECKOUT_KIT_UC_SHOP_DOMAINS=store-one.myshopify.com,store-two.myshopify.com
CHECKOUT_KIT_UC_ALLOWED_SHOP_DOMAINS=another-store.example.test
```

The local adapter permits configured shops, additional exact domains listed in
`CHECKOUT_KIT_UC_ALLOWED_SHOP_DOMAINS`, and production domains shaped like
`<shop>.myshopify.com`. Local development shops must be explicitly configured.
Use shops that support public products, an eligible cart creation flow, and one
common cart currency. Buyer country affects cart currency, but a shared country
does not guarantee matching currencies.

Configure `CHECKOUT_KIT_UC_SESSION_CREATE_URL` with your trusted production
session endpoint on `https://shop.app`. For **Local development**, configure both
`CHECKOUT_KIT_UC_DEVELOPMENT_SESSION_CREATE_URL` and
`VITE_CHECKOUT_KIT_UC_DEVELOPMENT_CONTINUATION_HOST`. For example:

```dotenv
CHECKOUT_KIT_UC_DEVELOPMENT_SESSION_CREATE_URL=https://sessions.example.test/sessions
VITE_CHECKOUT_KIT_UC_DEVELOPMENT_CONTINUATION_HOST=checkout.example.test
```

The URLs must use HTTPS and cannot include credentials, query parameters, or
fragments. Session creation URLs stay on the local server. Only the selected
shop domains and the development continuation hostname are returned to the
local browser at runtime. Despite the legacy `VITE_` variable name, this host is
not compiled into the sample. Storefront tokens and other shared environment
values are neither returned to the page nor embedded in build output.

The generated and pasted continuation URL must use HTTPS, the selected
environment's trusted host, a `/checkouts/uc/<session-id>` path, and a nonempty
`key` query value. Keep keyed cart and checkout URLs out of logs, screenshots,
and issue reports.

The **Advanced: paste an existing Universal Checkout URL** option is for
testing an already created session. The pasted value is not persisted, is
cleared when switching URL source modes, and must match the selected
environment. Like a generated URL, it is assigned to Checkout Kit's `src`
when selected. It does not create carts or a new session.

### Troubleshooting

- If products do not load, check that the shop domain is allowed for the
selected environment, its storefront is reachable, and it has products
published to the Online Store. Use **Retry** after fixing the shop.
- If cart creation fails, check that the selected variants are available and
the shop permits Storefront API cart creation. If currencies differ, select
a buyer country supported by all shops and create the URL again.
- If session creation fails, check that the selected carts are still valid
and that the local adapter can reach the configured session service.
- If the checkout window does not open, use **Open Universal Checkout** directly
from a click and check the browser's popup permission. Sign in to Shop Pay
in the selected environment before continuing checkout.
- If the window opens but the timeline stays empty, verify that the checkout
implementation is emitting Universal events and inspect Checkout Kit's
`warn` diagnostics for dropped malformed events. An empty timeline does not
establish a purchase outcome.

The cart, catalog, and session routes exist in the local Vite development and
preview servers. Opening the built HTML file directly or serving it from an
unrelated static host will not provide those routes. The complete two-shop
buyer flow and real producer error events still require a recorded browser
run; unit tests and source inspection alone do not establish that validation.

## Build

```bash
pnpm sample:build # outputs index.html and universal.html to sample/dist/
From `platforms/web`, run:

```sh
pnpm sample:build
```

CI runs this on every PR (see `.github/workflows/web.yml`). The sample is not published to npm (`files` allowlist in `platforms/web/package.json`).
The build outputs to `platforms/web/sample/dist/`. CI runs it on every PR.
The sample is not published to npm (`files` allowlist in
`platforms/web/package.json`).
17 changes: 17 additions & 0 deletions platforms/web/sample/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,15 @@ button:disabled {
display: none;
}

.universal-page .uc-preparation-actions {
display: flex;
justify-content: flex-end;
}

.universal-page .uc-preparation-actions button {
width: 100%;
}

.universal-page .uc-shop-list {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -1243,6 +1252,14 @@ button:disabled {
line-height: 1.45;
}

.universal-page #uc-preparation-status[data-tone="error"] {
color: var(--error);
}

.universal-page #uc-preparation-status[data-tone="success"] {
color: var(--success);
}

.universal-page .events-collapsed #uc-runtime-notice {
display: none;
}
Expand Down
28 changes: 28 additions & 0 deletions platforms/web/sample/universal-main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { normalizeQuantity } from "./cart";
import { createColumnResizer } from "./column-resizer";
import { setDevelopmentContinuationHost } from "./universal/browser-policy";
import { loadSampleConfiguration } from "./universal/configuration";
import { createUniversalController } from "./universal/controller";
import { createSessionPreparationController } from "./universal/preparation";
import { isBuyerCountry, isCheckoutEnvironment } from "./universal/policy";
import {
createInitialState,
createUniversalStore,
Expand All @@ -20,6 +24,7 @@ import "./styles.css";
const refs = queryUniversalRefs();
const store = createUniversalStore(createInitialState(loadUniversalDisplay()));
const controller = createUniversalController({ store });
const preparation = createSessionPreparationController({ store });
const resizer = createColumnResizer({
layout: refs.layout,
leftPanel: refs.settingsPanel,
Expand Down Expand Up @@ -56,6 +61,10 @@ refs.form.addEventListener("change", (event) => {
if (!(target instanceof HTMLSelectElement)) return;
if (target === refs.targetInput) {
updateDisplay({ target: target.value === "auto" ? "auto" : "popup" });
} else if (target === refs.environmentInput) {
if (isCheckoutEnvironment(target.value)) preparation.setEnvironment(target.value);
} else if (target === refs.buyerCountryInput) {
if (isBuyerCountry(target.value)) preparation.setBuyerCountry(target.value);
} else if (target === refs.appearanceInput) {
updateDisplay({ appearance: target.value });
} else if (target === refs.logLevelInput) {
Expand All @@ -71,6 +80,10 @@ refs.form.addEventListener("change", (event) => {
}
});

refs.prepareButton.addEventListener("click", () => {
void preparation.prepare();
});

refs.settingsToggle.addEventListener("click", () => {
updateDisplay({ settingsCollapsed: !store.getState().display.settingsCollapsed });
});
Expand Down Expand Up @@ -138,10 +151,25 @@ refs.shopList.addEventListener("change", (event) => {

renderUniversalApp(refs, store.getState());
resizer.applyWidths();
void loadSampleConfiguration()
.then((configuration) => {
setDevelopmentContinuationHost(configuration.developmentContinuationHost);
for (const domain of configuration.shopDomains) {
if (!store.getState().shops.some((shop) => shop.domain === domain))
controller.addShop(domain);
}
return undefined;
})
.catch(() => {
controller.setRuntimeNotice(
"Could not load sample configuration. Start the local sample server, then reload the page.",
);
});
window.addEventListener("resize", resizer.reposition);
window.addEventListener("pagehide", (event) => {
if (event.persisted) return;
unsubscribe();
controller.dispose();
preparation.dispose();
window.removeEventListener("resize", resizer.reposition);
});
35 changes: 34 additions & 1 deletion platforms/web/sample/universal.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@ <h2 id="settings-heading">Settings</h2>
<p id="uc-add-error" class="notice" data-tone="error" role="alert" hidden></p>
</fieldset>

<fieldset class="source-group source-fields">
<legend>Checkout session</legend>
<label>
<span>Environment</span>
<select id="uc-environment" name="environment">
<option value="production">Production</option>
<option value="development">Local development</option>
</select>
</label>
<label>
<span>Buyer country</span>
<select id="uc-buyer-country" name="buyer-country">
<option value="CA">Canada (CAD)</option>
<option value="US">United States (USD)</option>
<option value="DE">Germany (EUR)</option>
<option value="GB">United Kingdom (GBP)</option>
<option value="AU">Australia (AUD)</option>
<option value="NZ">New Zealand (NZD)</option>
<option value="JP">Japan (JPY)</option>
</select>
</label>
<p class="field-help">
Each shop gets a separate Storefront cart. All carts must use the same currency.
</p>
</fieldset>

<fieldset class="source-group source-fields">
<legend>Presentation</legend>
<label>
Expand Down Expand Up @@ -122,6 +148,11 @@ <h2 id="uc-cart-heading">Selected carts</h2>
<p id="uc-readiness" class="notice" role="status">
Add a shop to start building carts.
</p>
<div class="uc-preparation-actions">
<button id="uc-prepare" type="button" class="primary-action" disabled>
Create Universal Checkout URL
</button>
</div>
</section>

<div class="panel-header storefront-header">
Expand Down Expand Up @@ -153,7 +184,9 @@ <h3>No shops selected</h3>
<div class="runtime-scroll">
<details id="component-state" open>
<summary>Preparation</summary>
<p id="uc-preparation-status" class="muted">Add a shop to start building carts.</p>
<p id="uc-preparation-status" class="muted" role="status" aria-live="polite">
Add a shop to start building carts.
</p>
</details>
<div class="panel-header events-header">
<h2 id="events-heading">Events</h2>
Expand Down
Loading
Loading