From 0e8a9684f678badbcb9431b56240d3bb7b3bb5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20=C3=81lvarez?= Date: Fri, 10 Jul 2026 11:22:03 +0200 Subject: [PATCH 1/2] Remove redundant Web event detail properties Assisted-By: devx/7de6cdaa-3194-43df-b108-d324ae4d378d --- platforms/web/src/checkout.test.ts | 34 +++++++++++++------------ platforms/web/src/checkout.ts | 41 ++++++------------------------ 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/platforms/web/src/checkout.test.ts b/platforms/web/src/checkout.test.ts index 0fbbfa23..71d5f402 100644 --- a/platforms/web/src/checkout.test.ts +++ b/platforms/web/src/checkout.test.ts @@ -1204,10 +1204,10 @@ describe("", () => { await wait; const event = spy.mock.calls[0]![0] as CustomEvent; - expect(event.detail).toEqual({ checkout: decodeCheckout(payload) }); + expect(event.detail).toStrictEqual({ checkout: decodeCheckout(payload) }); }); - it("ec.complete carries {checkout, order} with order derived from checkout", async () => { + it("ec.complete carries {checkout} with order nested in checkout", async () => { const { checkout, mockCheckoutWindow } = openPopupCheckout(); const spy = vi.fn(); const wait = waitForEvent(checkout, "ec.complete", spy); @@ -1224,11 +1224,11 @@ describe("", () => { const event = spy.mock.calls[0]![0] as CustomEvent; const decoded = decodeCheckout(payload); - expect(event.detail).toEqual({ checkout: decoded, order: decoded.order }); - expect(event.detail.order).toEqual(event.detail.checkout.order); + expect(event.detail).toStrictEqual({ checkout: decoded }); + expect(event.detail.checkout.order).toEqual(decoded.order); }); - it("ec.complete leaves order undefined when the checkout has no order", async () => { + it("ec.complete keeps an absent order nested in checkout", async () => { const { checkout, mockCheckoutWindow } = openPopupCheckout(); const spy = vi.fn(); const wait = waitForEvent(checkout, "ec.complete", spy); @@ -1240,7 +1240,9 @@ describe("", () => { await wait; const event = spy.mock.calls[0]![0] as CustomEvent; - expect(event.detail.order).toBeUndefined(); + const decoded = decodeCheckout(payload); + expect(event.detail).toStrictEqual({ checkout: decoded }); + expect(event.detail.checkout.order).toBeUndefined(); }); it("ec.error carries {error}", async () => { @@ -1255,10 +1257,10 @@ describe("", () => { await wait; const event = spy.mock.calls[0]![0] as CustomEvent; - expect(event.detail).toEqual({ error: decodeError(errorParams) }); + expect(event.detail).toStrictEqual({ error: decodeError(errorParams) }); }); - it("ec.line_items.change carries {lineItems, checkout}", async () => { + it("ec.line_items.change carries {checkout} with lineItems nested in checkout", async () => { const { checkout, mockCheckoutWindow } = openPopupCheckout(); const spy = vi.fn(); const wait = waitForEvent(checkout, "ec.line_items.change", spy); @@ -1271,11 +1273,11 @@ describe("", () => { const event = spy.mock.calls[0]![0] as CustomEvent; const decoded = decodeCheckout(payload); - expect(event.detail.lineItems).toEqual(decoded.lineItems); - expect(event.detail.checkout).toEqual(decoded); + expect(event.detail).toStrictEqual({ checkout: decoded }); + expect(event.detail.checkout.lineItems).toEqual(decoded.lineItems); }); - it("ec.totals.change carries {totals, checkout}", async () => { + it("ec.totals.change carries {checkout} with totals nested in checkout", async () => { const { checkout, mockCheckoutWindow } = openPopupCheckout(); const spy = vi.fn(); const wait = waitForEvent(checkout, "ec.totals.change", spy); @@ -1288,11 +1290,11 @@ describe("", () => { const event = spy.mock.calls[0]![0] as CustomEvent; const decoded = decodeCheckout(payload); - expect(event.detail.totals).toEqual(decoded.totals); - expect(event.detail.checkout).toEqual(decoded); + expect(event.detail).toStrictEqual({ checkout: decoded }); + expect(event.detail.checkout.totals).toEqual(decoded.totals); }); - it("ec.messages.change carries {messages, checkout}", async () => { + it("ec.messages.change carries {checkout} with messages nested in checkout", async () => { const { checkout, mockCheckoutWindow } = openPopupCheckout(); const spy = vi.fn(); const wait = waitForEvent(checkout, "ec.messages.change", spy); @@ -1305,8 +1307,8 @@ describe("", () => { const event = spy.mock.calls[0]![0] as CustomEvent; const decoded = decodeCheckout(payload); - expect(event.detail.messages).toEqual(decoded.messages ?? []); - expect(event.detail.checkout).toEqual(decoded); + expect(event.detail).toStrictEqual({ checkout: decoded }); + expect(event.detail.checkout.messages).toEqual(decoded.messages); }); it("ec.close carries no detail", () => { diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index a6b2fb9e..dc7d106a 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -17,10 +17,7 @@ import type { CheckoutTarget, TypedEventListener, Checkout, - LineItem, - Message, CheckoutAppearance, - CheckoutTotal, ErrorResponse, } from "./checkout.types"; @@ -232,7 +229,7 @@ export class ShopifyCheckout * @returns The current Checkout, or undefined before the first notification. * @example * checkout.addEventListener('ec.start', (event) => { - * const {line_items, totals, buyer} = event.detail.checkout; + * const {lineItems, totals, buyer} = event.detail.checkout; * }); */ get checkout(): Checkout | undefined { @@ -587,7 +584,7 @@ export class ShopifyCheckout }) .on(Event.complete, (checkout) => { this.#checkout = checkout; - this.dispatchEvent(new ShopifyCheckoutCompleteEvent({ checkout, order: checkout.order })); + this.dispatchEvent(new ShopifyCheckoutCompleteEvent({ checkout })); }) .on(Event.error, (error) => { this.#error = error; @@ -603,30 +600,15 @@ export class ShopifyCheckout }) .on(Event.lineItemsChange, (checkout) => { this.#checkout = checkout; - this.dispatchEvent( - new ShopifyCheckoutLineItemsChangeEvent({ - checkout, - lineItems: checkout.lineItems, - }), - ); + this.dispatchEvent(new ShopifyCheckoutLineItemsChangeEvent({ checkout })); }) .on(Event.totalsChange, (checkout) => { this.#checkout = checkout; - this.dispatchEvent( - new ShopifyCheckoutTotalsChangeEvent({ - checkout, - totals: checkout.totals, - }), - ); + this.dispatchEvent(new ShopifyCheckoutTotalsChangeEvent({ checkout })); }) .on(Event.messagesChange, (checkout) => { this.#checkout = checkout; - this.dispatchEvent( - new ShopifyCheckoutMessagesChangeEvent({ - checkout, - messages: checkout.messages ?? [], - }), - ); + this.dispatchEvent(new ShopifyCheckoutMessagesChangeEvent({ checkout })); }) .on(Event.windowOpen, (request) => this.#handleWindowOpen(request)); } @@ -801,7 +783,6 @@ export interface ShopifyCheckoutStartEventDetail { export interface ShopifyCheckoutCompleteEventDetail { /** Final checkout snapshot from the ECP `ec.complete` notification. */ checkout: Checkout; - order?: Checkout["order"]; } export interface ShopifyCheckoutErrorEventDetail { @@ -810,23 +791,17 @@ export interface ShopifyCheckoutErrorEventDetail { } export interface ShopifyCheckoutLineItemsChangeEventDetail { - /** Updated cart line items. */ - lineItems: readonly LineItem[]; - /** Full checkout snapshot for handlers that want broader context. */ + /** Checkout snapshot with updated cart line items. */ checkout: Checkout; } export interface ShopifyCheckoutTotalsChangeEventDetail { - /** Updated totals. */ - totals: readonly CheckoutTotal[]; - /** Full checkout snapshot for handlers that want broader context. */ + /** Checkout snapshot with updated totals. */ checkout: Checkout; } export interface ShopifyCheckoutMessagesChangeEventDetail { - /** Updated checkout-level messages (warnings, errors, info). */ - messages: readonly Message[]; - /** Full checkout snapshot for handlers that want broader context. */ + /** Checkout snapshot with updated warnings, errors, and informational messages. */ checkout: Checkout; } From 50390a78601d237e3d5ee31dc627d375afb4e890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20=C3=81lvarez?= Date: Fri, 10 Jul 2026 16:20:18 +0200 Subject: [PATCH 2/2] Document canonical Web event detail payloads Assisted-By: devx/7de6cdaa-3194-43df-b108-d324ae4d378d --- platforms/web/README.md | 64 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/platforms/web/README.md b/platforms/web/README.md index 39f0e373..fd414ffe 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -134,7 +134,7 @@ checkout.target = 'popup'; document.body.append(checkout); checkout.addEventListener('ec.complete', (event) => { - console.log('Order complete', event.detail.order.id); + console.log('Order complete', event.detail.checkout.order?.id); }); checkout.open(); @@ -181,7 +181,7 @@ export function BuyNowButton({checkoutUrl}: {checkoutUrl: string}) { checkout.addEventListener( 'ec.complete', - (event) => console.log('Order complete', event.detail.order.id), + (event) => console.log('Order complete', event.detail.checkout.order?.id), {signal}, ); checkout.addEventListener('ec.close', () => console.log('Dismissed'), { @@ -200,10 +200,10 @@ export function BuyNowButton({checkoutUrl}: {checkoutUrl: string}) { } ``` -`event` is fully typed inside each listener — `event.detail.order` on -`ec.complete`, and so on — courtesy of the element's overloaded -`addEventListener` signatures. See [Checkout lifecycle](#checkout-lifecycle) -for the full event list. +`event` is fully typed inside each listener. For example, order data for +`ec.complete` is available at `event.detail.checkout.order`. The element's +overloaded `addEventListener` signatures provide these types. See +[Checkout lifecycle](#checkout-lifecycle) for the full event list. TypeScript doesn't know about the `` tag in JSX out of the box. Declare it once, anywhere in your project's type definitions: @@ -418,28 +418,29 @@ DOM — including a single delegated listener at `document` if you have many elements on the page. Each event carries a typed `event.detail` payload with exactly the fields relevant to that moment. -| Event | `event.detail` | When it fires | -| ---------------------- | ----------------------- | -------------------------------------------------------------------------- | -| `ec.start` | `{checkout}` | Checkout has loaded and is interactive. | -| `ec.complete` | `{checkout, order}` | The buyer completed the order successfully. | -| `ec.close` | _(none)_ | The popup was dismissed (by the buyer, by `close()`, or by `focus` loss). | -| `ec.error` | `{error}` | Session-level fatal error — tear down the embedded context. | -| `ec.line_items.change` | `{checkout, lineItems}` | The cart's line items changed (item added/removed/quantity updated). | -| `ec.totals.change` | `{checkout, totals}` | The cart totals changed (subtotal, tax, shipping, discounts, total). | -| `ec.messages.change` | `{checkout, messages}` | Checkout-level warnings/errors/info shown inside the checkout changed. | +| Event | `event.detail` | When it fires | +| ---------------------- | -------------- | -------------------------------------------------------------------------- | +| `ec.start` | `{checkout}` | Checkout has loaded and is interactive. | +| `ec.complete` | `{checkout}` | The buyer completed the order successfully. | +| `ec.close` | _(none)_ | The popup was dismissed (by the buyer, by `close()`, or by `focus` loss). | +| `ec.error` | `{error}` | Session-level fatal error — tear down the embedded context. | +| `ec.line_items.change` | `{checkout}` | The cart's line items changed (item added/removed/quantity updated). | +| `ec.totals.change` | `{checkout}` | The cart totals changed (subtotal, tax, shipping, discounts, total). | +| `ec.messages.change` | `{checkout}` | Checkout-level warnings/errors/info shown inside the checkout changed. | -The `checkout` field on every change event is the full UCP `Checkout` -snapshot, included for handlers that want broader context. Most handlers only -need the named slice (e.g. `event.detail.totals`). +`ec.start`, `ec.complete`, and the change events carry the full UCP `Checkout` +snapshot in `event.detail.checkout` for handlers that need broader context. ```ts checkout.addEventListener('ec.complete', (event) => { - const {order} = event.detail; - analytics.track('checkout_complete', {orderId: order.id}); + const {order} = event.detail.checkout; + if (order) { + analytics.track('checkout_complete', {orderId: order.id}); + } }); checkout.addEventListener('ec.totals.change', (event) => { - miniCart.updateTotals(event.detail.totals); + miniCart.updateTotals(event.detail.checkout.totals); }); checkout.addEventListener('ec.close', () => { @@ -447,10 +448,9 @@ checkout.addEventListener('ec.close', () => { }); ``` -Reach for `event.detail.checkout` when a handler needs fields beyond the -named slice. It carries the full UCP `Checkout` snapshot at the moment the -event was dispatched. For example, rendering an inline cart summary on -`ec.start` requires line items, totals, and currency together: +Because these events carry the full snapshot, one handler can combine fields. +For example, rendering an inline cart summary on `ec.start` requires line +items, totals, and currency together: ```ts checkout.addEventListener('ec.start', (event) => { @@ -458,18 +458,18 @@ checkout.addEventListener('ec.start', (event) => { loadingSpinner.hide(); cartSummary.render({ currency: snapshot.currency, - items: snapshot.line_items, + items: snapshot.lineItems, totals: snapshot.totals, }); }); ``` -The full UCP `Checkout` snapshot is also mirrored to the -[`element.checkout`](#) property every time a payload-carrying event arrives, -and the latest error is mirrored to [`element.error`](#) when `ec.error` -fires — useful for handlers that don't have a reference to the originating -event. TypeScript users get fully typed events via overloaded -`addEventListener` signatures — no additional setup required. +The latest full UCP `Checkout` snapshot is also mirrored to `element.checkout` +whenever an event with `{checkout}` arrives. The latest error is mirrored to +`element.error` when `ec.error` fires. These properties are useful for handlers +that don't have a reference to the originating event. TypeScript users get +fully typed events through overloaded `addEventListener` signatures with no +additional setup. > [!NOTE] > Most public `ec.*` DOM event names mirror the underlying