Skip to content

Commit 02f306f

Browse files
authored
Port web component over (#117)
Add initial version of Checkout Kit Web with support for popup and new tab targets.
1 parent 0946d24 commit 02f306f

17 files changed

Lines changed: 2975 additions & 140 deletions

platforms/web/.oxlintrc.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,15 @@
2626
"dist/**",
2727
"coverage/**",
2828
"node_modules/**"
29+
],
30+
"overrides": [
31+
{
32+
"files": ["**/*.test.ts"],
33+
"rules": {
34+
"typescript/no-non-null-assertion": "off",
35+
"vitest/require-mock-type-parameters": "off",
36+
"eslint/no-underscore-dangle": "off"
37+
}
38+
}
2939
]
3040
}

platforms/web/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
"./custom-elements.json": "./dist/custom-elements.json",
3333
"./package.json": "./package.json"
3434
},
35-
"sideEffects": false,
35+
"sideEffects": [
36+
"./src/index.ts",
37+
"./src/checkout-web-component.ts",
38+
"./dist/index.js"
39+
],
3640
"files": [
3741
"LICENSE",
3842
"README.md",

platforms/web/sample/README.md

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Web Component Playground
22

3-
A development harness for the `<shopify-checkout>` web component. Renders the
4-
component with adjustable options and logs all dispatched `checkout:*` events
5-
in real time.
3+
A development harness for the `<shopify-checkout>` web component. It imports
4+
the same entry as published consumers (`@shopify/checkout-kit`, aliased to
5+
`../src/index.ts` in dev), registers the custom element, and logs `ec:*` and
6+
`checkout:close` events.
67

78
## Run locally
89

@@ -13,43 +14,22 @@ pnpm sample
1314

1415
Vite serves at `http://localhost:5173`. The page has three panels:
1516

16-
- **Options** — form for setting the component's attributes (`src`,
17-
`target`) plus a small panel of manual method
18-
buttons (`open()`, `close()`, `focus()`) for ad-hoc debugging.
19-
- **Demo Storefront** — a mocked merchant product card with a **Buy now**
20-
button that calls `checkout.open()`. The button is disabled until you
21-
enter a checkout URL in the Options panel. Below the card, a collapsible
22-
readout shows the component's read-only state (`cart`, `locale`,
23-
`orderConfirmation`, `error`, `sessionId`).
24-
- **Events** — a chronological log of every `checkout:*` event the component
25-
dispatches, with a snapshot of component state at the moment the event
26-
fired. Respondable events are tagged with a badge.
27-
28-
The `<shopify-checkout>` element is appended to `<body>` rather than placed
29-
inside the storefront panel — for `popup` and `auto` targets, the element
30-
has no visible footprint of its own; only its internal dialog scrim appears
31-
when `open()` is called. `target="inline"` is intentionally not supported
32-
in the v1 of the component.
33-
34-
## Status
35-
36-
The `<shopify-checkout>` component implementation has not yet landed in
37-
`../src`. Until it does, the element renders as an unknown HTML element and
38-
dispatches no events — the playground is wired up against the component's
39-
eventual API surface but is **non-functional at runtime**.
40-
41-
The forward-looking API surface is declared in [`./types.d.ts`](./types.d.ts).
42-
Delete that file once `@shopify/checkout-kit` exports the real `ShopifyCheckout`
43-
types from `../src`.
17+
- **Options** — form for `src`, `target` (`auto` | `popup`), and `debug`,
18+
plus buttons for `open()`, `close()`, and `focus()`.
19+
- **Demo Storefront** — a mocked product card with **Buy now** calling
20+
`checkout.open()`. The button stays disabled until a checkout URL is set.
21+
The collapsible readout shows `checkout`, `error`, `target`, and `debug`.
22+
- **Events** — log of dispatched events with a JSON snapshot of state at fire
23+
time.
24+
25+
The element is mounted on `<body>`. For `popup` / `auto`, the visible UI is
26+
mostly the overlay scrim while checkout is open in a separate window or tab.
4427

4528
## Build
4629

4730
```bash
4831
pnpm sample:build # outputs to sample/dist/
4932
```
5033

51-
CI runs this on every PR (see `.github/workflows/web.yml`) so the sample stays
52-
buildable as the package evolves.
53-
54-
The sample is **not** published to npm — it's excluded by the `files`
55-
allowlist in `platforms/web/package.json`.
34+
CI runs this on every PR (see `.github/workflows/web.yml`). The sample is not
35+
published to npm (`files` allowlist in `platforms/web/package.json`).

platforms/web/sample/index.html

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ <h1>Checkout Kit</h1>
1313
<p class="subtitle">Web Component Playground</p>
1414
</div>
1515
<p class="status" role="status">
16-
Component not yet implemented in <code>../src</code> — clicking
17-
<strong>Buy&nbsp;now</strong> is a no-op until the element is registered.
16+
Loads the real <code>&lt;shopify-checkout&gt;</code> from
17+
<code>@shopify/checkout-kit</code> (dev alias). Use a valid checkout URL to try
18+
<strong>Buy&nbsp;now</strong>.
1819
</p>
1920
</header>
2021

@@ -36,11 +37,16 @@ <h2 id="options-heading">Options</h2>
3637
<label>
3738
<span>target</span>
3839
<select name="target">
40+
<option value="popup" selected>popup (window)</option>
3941
<option value="auto">auto (new tab)</option>
40-
<option value="popup">popup (window)</option>
4142
</select>
4243
</label>
4344

45+
<label class="checkbox">
46+
<input type="checkbox" name="debug" />
47+
<span>debug</span>
48+
</label>
49+
4450
<fieldset>
4551
<legend>Methods</legend>
4652
<div class="button-row">
@@ -108,16 +114,14 @@ <h3 class="product-title">Studio Plant Pot</h3>
108114
<details id="component-state">
109115
<summary>Component state</summary>
110116
<dl>
111-
<dt>cart</dt>
112-
<dd id="state-cart"></dd>
113-
<dt>locale</dt>
114-
<dd id="state-locale"></dd>
115-
<dt>orderConfirmation</dt>
116-
<dd id="state-order-confirmation"></dd>
117+
<dt>checkout</dt>
118+
<dd id="state-checkout"></dd>
117119
<dt>error</dt>
118120
<dd id="state-error"></dd>
119-
<dt>sessionId</dt>
120-
<dd id="state-session-id"></dd>
121+
<dt>target</dt>
122+
<dd id="state-target"></dd>
123+
<dt>debug</dt>
124+
<dd id="state-debug"></dd>
121125
</dl>
122126
</details>
123127
</section>
@@ -130,8 +134,8 @@ <h2 id="events-heading">Events</h2>
130134
</div>
131135
<ul id="event-log"></ul>
132136
<p id="event-empty" class="muted">
133-
Click <strong>Buy now</strong> to open checkout. Events the component dispatches will
134-
appear here.
137+
Open checkout and interact; <code>ec:*</code> and <code>checkout:close</code> events from
138+
the component appear here.
135139
</p>
136140
</section>
137141
</main>

platforms/web/sample/main.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
// Importing the package registers the <shopify-checkout> custom element
2-
// as a side effect — once the component implementation lands. Today the
3-
// package only exports `VERSION`, so the element below renders as an
4-
// unknown HTML element and the playground produces no events at runtime.
5-
import "../src";
1+
// Registers `<shopify-checkout>` (same entry as published `@shopify/checkout-kit`).
2+
import "@shopify/checkout-kit";
3+
import type { ShopifyCheckout } from "@shopify/checkout-kit";
64

75
import "./styles.css";
86

@@ -40,11 +38,10 @@ const buyNowButton = $<HTMLButtonElement>("#buy-now");
4038
const buyHint = $<HTMLParagraphElement>("#buy-hint");
4139

4240
const stateNodes = {
43-
cart: $<HTMLElement>("#state-cart"),
44-
locale: $<HTMLElement>("#state-locale"),
45-
orderConfirmation: $<HTMLElement>("#state-order-confirmation"),
41+
checkout: $<HTMLElement>("#state-checkout"),
4642
error: $<HTMLElement>("#state-error"),
47-
sessionId: $<HTMLElement>("#state-session-id"),
43+
target: $<HTMLElement>("#state-target"),
44+
debug: $<HTMLElement>("#state-debug"),
4845
};
4946

5047
// ───── Mount the component (off-layout) ───────────────────────────────────
@@ -55,9 +52,11 @@ const stateNodes = {
5552
// called, so we attach it to <body> and leave the storefront panel free for
5653
// the merchant's product UI.
5754

58-
const checkout = document.createElement("shopify-checkout");
55+
const checkout = document.createElement("shopify-checkout") as ShopifyCheckout;
5956
document.body.append(checkout);
6057

58+
const checkoutEl: HTMLElement = checkout;
59+
6160
// ───── Form ↔ attributes ──────────────────────────────────────────────────
6261

6362
function setStringAttribute(el: HTMLElement, name: string, value: FormDataEntryValue | null): void {
@@ -72,6 +71,11 @@ function syncAttributes(): void {
7271
const data = new FormData(form);
7372
setStringAttribute(checkout, "src", data.get("src"));
7473
setStringAttribute(checkout, "target", data.get("target"));
74+
if (data.has("debug")) {
75+
checkout.setAttribute("debug", "");
76+
} else {
77+
checkout.removeAttribute("debug");
78+
}
7579

7680
refreshBuyButton(data.get("src"));
7781
}
@@ -123,24 +127,22 @@ for (const swatch of swatches) {
123127

124128
// ───── Event log ──────────────────────────────────────────────────────────
125129

130+
/** Dispatched by `ShopifyCheckout` (see `src/checkout.ts`). */
126131
const EVENT_TYPES = [
127-
"checkout:start",
128-
"checkout:complete",
132+
"ec:start",
133+
"ec:complete",
129134
"checkout:close",
130-
"checkout:error",
131-
"checkout:addressChangeStart",
132-
"checkout:paymentMethodChangeStart",
133-
"checkout:submitStart",
135+
"ec:error",
136+
"ec:lineItemsChange",
137+
"ec:buyerChange",
138+
"ec:totalsChange",
139+
"ec:messagesChange",
134140
] as const;
135141

136-
const RESPONDABLE_EVENTS = new Set<string>([
137-
"checkout:addressChangeStart",
138-
"checkout:paymentMethodChangeStart",
139-
"checkout:submitStart",
140-
]);
142+
const RESPONDABLE_EVENTS = new Set<string>([]);
141143

142144
for (const type of EVENT_TYPES) {
143-
checkout.addEventListener(type, () => {
145+
checkoutEl.addEventListener(type, () => {
144146
appendLog(type);
145147
refreshState();
146148
});
@@ -152,20 +154,18 @@ clearLogButton.addEventListener("click", () => {
152154

153155
function snapshotState(): Record<string, unknown> {
154156
return {
155-
cart: checkout.cart,
156-
locale: checkout.locale,
157-
orderConfirmation: checkout.orderConfirmation,
157+
checkout: checkout.checkout,
158158
error: checkout.error,
159-
sessionId: checkout.sessionId,
159+
target: checkout.target,
160+
debug: checkout.debug,
160161
};
161162
}
162163

163164
function refreshState(): void {
164-
stateNodes.cart.textContent = formatValue(checkout.cart);
165-
stateNodes.locale.textContent = formatValue(checkout.locale);
166-
stateNodes.orderConfirmation.textContent = formatValue(checkout.orderConfirmation);
165+
stateNodes.checkout.textContent = formatValue(checkout.checkout);
167166
stateNodes.error.textContent = formatValue(checkout.error);
168-
stateNodes.sessionId.textContent = formatValue(checkout.sessionId);
167+
stateNodes.target.textContent = formatValue(checkout.target);
168+
stateNodes.debug.textContent = formatValue(checkout.debug);
169169
}
170170

171171
function appendLog(type: string): void {

platforms/web/sample/tsconfig.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
{
22
"extends": "../tsconfig.json",
33
"compilerOptions": {
4-
// The sample uses vanilla DOM patterns and a forward-looking shim
5-
// (types.d.ts) for the not-yet-implemented component. Don't enforce the
6-
// library-grade isolatedDeclarations rule here.
74
"isolatedDeclarations": false,
85
"declaration": false,
96
"noEmit": true,
10-
// vite/client provides the `*.css` module declaration so `import "./styles.css"` typechecks.
11-
"types": ["vite/client"]
7+
"types": ["vite/client"],
8+
"paths": {
9+
"@shopify/checkout-kit": ["../src/index.ts"]
10+
}
1211
},
1312
"include": ["./**/*.ts", "./**/*.d.ts"]
1413
}

platforms/web/sample/types.d.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

platforms/web/sample/vite.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ const here = dirname(fileURLToPath(import.meta.url));
88
export default defineConfig({
99
// Treat `sample/` as the project root so vite serves `index.html` from here.
1010
root: here,
11+
resolve: {
12+
alias: {
13+
// Same entry consumers use from npm (`import '@shopify/checkout-kit'`).
14+
"@shopify/checkout-kit": resolve(here, "../src/index.ts"),
15+
},
16+
},
1117
build: {
1218
outDir: resolve(here, "dist"),
1319
emptyOutDir: true,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
MIT License
3+
4+
Copyright 2023 - Present, Shopify Inc.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
/* eslint ssr-friendly/no-dom-globals-in-module-scope: off */
25+
26+
import { ShopifyCheckout } from "./checkout";
27+
28+
declare global {
29+
interface HTMLElementTagNameMap {
30+
"shopify-checkout": ShopifyCheckout;
31+
}
32+
}
33+
34+
customElements.define("shopify-checkout", ShopifyCheckout);

0 commit comments

Comments
 (0)