From d550169bea939a33e66296e5ddc7efddb027bbba Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Thu, 17 Sep 2026 21:58:42 -0300 Subject: [PATCH 1/7] Fix blocked popup session state --- platforms/web/src/checkout-window.test.ts | 11 +++++++++-- platforms/web/src/checkout.ts | 23 ++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/platforms/web/src/checkout-window.test.ts b/platforms/web/src/checkout-window.test.ts index 098a4b0c7..62120cb97 100644 --- a/platforms/web/src/checkout-window.test.ts +++ b/platforms/web/src/checkout-window.test.ts @@ -241,15 +241,23 @@ describe("", () => { }); }); - it("handles popup blocked scenario gracefully", () => { + it("does not open an overlay or session when the popup is blocked", () => { POPUP_TARGETS.forEach((target) => { const telemetrySpy = vi.spyOn(mockTelemetry(), "recordError"); const checkout = renderCheckout({ target }); const windowOpenSpy = vi.spyOn(window, "open").mockReturnValue(null); + const dialogShowModalSpy = vi + .spyOn(HTMLDialogElement.prototype, "showModal") + .mockImplementation(() => {}); + const closeEventSpy = vi.fn(); + checkout.addEventListener("ec.close", closeEventSpy); checkout.open(); + checkout.close(); expect(windowOpenSpy).toHaveBeenCalled(); + expect(dialogShowModalSpy).not.toHaveBeenCalled(); + expect(closeEventSpy).not.toHaveBeenCalled(); expect(telemetrySpy).toHaveBeenCalledWith({ category: "navigation", stage: "presentation", @@ -257,7 +265,6 @@ describe("", () => { retryable: false, isRetry: false, }); - // Should not throw error when popup is blocked }); }); diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index bf0ddb2cf..855e752e8 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -470,6 +470,17 @@ export class ShopifyCheckout } } + if (!checkoutWindow) { + this.#recorder?.recordError({ + category: "navigation", + stage: "presentation", + code: "blocked", + retryable: false, + isRetry: false, + }); + return; + } + const abortController = new AbortController(); // Opens a dialog element to act as a scrim over the current window while the popup is open. @@ -566,17 +577,7 @@ export class ShopifyCheckout this.#currentOpen = { controller: abortController }; this.#checkoutWindow = checkoutWindow; - this.#navigationStartedAt = checkoutWindow && this.telemetry ? navigationStartedAt : undefined; - - if (!checkoutWindow) { - this.#recorder?.recordError({ - category: "navigation", - stage: "presentation", - code: "blocked", - retryable: false, - isRetry: false, - }); - } + this.#navigationStartedAt = this.telemetry ? navigationStartedAt : undefined; } close(): void { From 6fa68ac5d498453a10e02c5b8bb8e5dcf8bc68bc Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Sun, 20 Sep 2026 17:48:26 -0300 Subject: [PATCH 2/7] Warn and document when the checkout window is blocked --- platforms/web/README.md | 6 ++++ platforms/web/src/checkout-window.test.ts | 36 ++++++++++++++++++++++- platforms/web/src/checkout.ts | 4 +++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/platforms/web/README.md b/platforms/web/README.md index 9bdf573ea..c2ef19bca 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -352,6 +352,12 @@ Where the checkout is presented. Defaults to `"auto"`. > the host page away. The component falls back to `"auto"` if you set one, > and logs a warning at `log-level="warn"` or more verbose. +> [!NOTE] +> If the browser refuses to open the window (for example, a popup blocker, or +> `open()` called outside a user gesture), `open()` does nothing: no overlay is +> shown, no session starts, and `ec.close` does not fire. The component logs a +> warning at `log-level="warn"` or more verbose. + ### `appearance` Sets the checkout appearance preference. Defaults to `"storefront"`. diff --git a/platforms/web/src/checkout-window.test.ts b/platforms/web/src/checkout-window.test.ts index 62120cb97..8658d65c7 100644 --- a/platforms/web/src/checkout-window.test.ts +++ b/platforms/web/src/checkout-window.test.ts @@ -244,8 +244,9 @@ describe("", () => { it("does not open an overlay or session when the popup is blocked", () => { POPUP_TARGETS.forEach((target) => { const telemetrySpy = vi.spyOn(mockTelemetry(), "recordError"); - const checkout = renderCheckout({ target }); + const checkout = renderCheckout({ target, "log-level": "warn" }); const windowOpenSpy = vi.spyOn(window, "open").mockReturnValue(null); + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const dialogShowModalSpy = vi .spyOn(HTMLDialogElement.prototype, "showModal") .mockImplementation(() => {}); @@ -258,6 +259,9 @@ describe("", () => { expect(windowOpenSpy).toHaveBeenCalled(); expect(dialogShowModalSpy).not.toHaveBeenCalled(); expect(closeEventSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + ": checkout window could not be opened; the browser may have blocked it", + ); expect(telemetrySpy).toHaveBeenCalledWith({ category: "navigation", stage: "presentation", @@ -268,6 +272,36 @@ describe("", () => { }); }); + it("can open successfully after a blocked popup", () => { + POPUP_TARGETS.forEach((target) => { + const checkout = renderCheckout({ target }); + const mockWindow = createMockWindow(); + const windowOpenSpy = vi + .spyOn(window, "open") + .mockReturnValueOnce(null) + .mockReturnValueOnce(mockWindow); + const dialogShowModalSpy = vi + .spyOn(HTMLDialogElement.prototype, "showModal") + .mockImplementation(() => {}); + const closeEventSpy = vi.fn(); + checkout.addEventListener("ec.close", closeEventSpy); + + checkout.open(); + expect(dialogShowModalSpy).not.toHaveBeenCalled(); + + checkout.open(); + expect(windowOpenSpy).toHaveBeenCalledTimes(2); + expect(dialogShowModalSpy).toHaveBeenCalledTimes(1); + + checkout.focus(); + expect(mockWindow.focus).toHaveBeenCalled(); + + checkout.close(); + expect(mockWindow.close).toHaveBeenCalled(); + expect(closeEventSpy).toHaveBeenCalledTimes(1); + }); + }); + it("enforces maximum window size constraints", () => { POPUP_TARGETS.forEach((target) => { const windowOpenSpy = vi.spyOn(window, "open").mockReturnValue(createMockWindow()); diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index 855e752e8..c1b9daaa6 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -470,7 +470,11 @@ export class ShopifyCheckout } } + // The browser refused to open the window (typically a popup blocker, or + // `open()` was called outside a user gesture). There is no window to focus + // and nothing for the buyer to return to, so no overlay or session is created. if (!checkoutWindow) { + this.#logger.warn("checkout window could not be opened; the browser may have blocked it"); this.#recorder?.recordError({ category: "navigation", stage: "presentation", From abdad1af27f0a012af004e45ae88ac3d3f94fcff Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Wed, 23 Sep 2026 22:37:33 -0300 Subject: [PATCH 3/7] Show a retry overlay when the checkout window is blocked --- platforms/web/README.md | 11 +- platforms/web/src/checkout-window.test.ts | 74 +++++--- platforms/web/src/checkout.css | 5 + platforms/web/src/checkout.ts | 215 +++++++++++++++------- 4 files changed, 207 insertions(+), 98 deletions(-) diff --git a/platforms/web/README.md b/platforms/web/README.md index c2ef19bca..18edc8780 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -354,9 +354,10 @@ Where the checkout is presented. Defaults to `"auto"`. > [!NOTE] > If the browser refuses to open the window (for example, a popup blocker, or -> `open()` called outside a user gesture), `open()` does nothing: no overlay is -> shown, no session starts, and `ec.close` does not fire. The component logs a -> warning at `log-level="warn"` or more verbose. +> `open()` called outside a user gesture), the [overlay scrim](#overlay-scrim) +> says so and offers a button to try again. Closing it dispatches `ec.close`. +> If the overlay is hidden, nothing is shown and no events fire. The component +> logs a warning at `log-level="warn"` or more verbose. ### `appearance` @@ -475,7 +476,9 @@ shopify-checkout { While a popup is open the component renders a `` scrim over the host page, with a "Continue your purchase in the checkout window" link and a close -button. Hide it by either: +button. If the browser blocks the window, the scrim instead says "Your browser +blocked the checkout window." with an "Open checkout" button that tries again. +Hide it by either: - Setting `display: none` on the element itself, or - Targeting the `overlay` shadow part: diff --git a/platforms/web/src/checkout-window.test.ts b/platforms/web/src/checkout-window.test.ts index 8658d65c7..a636b7957 100644 --- a/platforms/web/src/checkout-window.test.ts +++ b/platforms/web/src/checkout-window.test.ts @@ -241,63 +241,79 @@ describe("", () => { }); }); - it("does not open an overlay or session when the popup is blocked", () => { + it("handles popup blocked scenario gracefully", () => { POPUP_TARGETS.forEach((target) => { const telemetrySpy = vi.spyOn(mockTelemetry(), "recordError"); - const checkout = renderCheckout({ target, "log-level": "warn" }); + const checkout = renderCheckout({ target }); const windowOpenSpy = vi.spyOn(window, "open").mockReturnValue(null); - const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - const dialogShowModalSpy = vi - .spyOn(HTMLDialogElement.prototype, "showModal") - .mockImplementation(() => {}); - const closeEventSpy = vi.fn(); - checkout.addEventListener("ec.close", closeEventSpy); checkout.open(); - checkout.close(); expect(windowOpenSpy).toHaveBeenCalled(); - expect(dialogShowModalSpy).not.toHaveBeenCalled(); - expect(closeEventSpy).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - ": checkout window could not be opened; the browser may have blocked it", - ); expect(telemetrySpy).toHaveBeenCalledWith({ category: "navigation", stage: "presentation", code: "blocked", - retryable: false, + retryable: true, isRetry: false, }); + // Should not throw error when popup is blocked }); }); - it("can open successfully after a blocked popup", () => { + it("shows the blocked overlay when the popup is blocked", () => { + POPUP_TARGETS.forEach((target) => { + const checkout = renderCheckout({ target, "log-level": "warn" }); + vi.spyOn(window, "open").mockReturnValue(null); + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const dialogShowModalSpy = vi + .spyOn(HTMLDialogElement.prototype, "showModal") + .mockImplementation(() => {}); + + checkout.open(); + + const dialog = checkout.shadowRoot!.querySelector("#overlay")!; + expect(dialogShowModalSpy).toHaveBeenCalledTimes(1); + expect(dialog.dataset.state).toBe("blocked"); + expect(consoleWarnSpy).toHaveBeenCalledWith( + ": checkout window could not be opened; the browser may have blocked it", + ); + }); + }); + + it("opens checkout when the blocked overlay's retry button is clicked", () => { POPUP_TARGETS.forEach((target) => { const checkout = renderCheckout({ target }); - const mockWindow = createMockWindow(); const windowOpenSpy = vi .spyOn(window, "open") .mockReturnValueOnce(null) - .mockReturnValueOnce(mockWindow); - const dialogShowModalSpy = vi - .spyOn(HTMLDialogElement.prototype, "showModal") - .mockImplementation(() => {}); + .mockReturnValueOnce(createMockWindow()); + vi.spyOn(HTMLDialogElement.prototype, "showModal").mockImplementation(() => {}); const closeEventSpy = vi.fn(); checkout.addEventListener("ec.close", closeEventSpy); checkout.open(); - expect(dialogShowModalSpy).not.toHaveBeenCalled(); + checkout.shadowRoot!.querySelector("#overlay-retry-button")!.click(); - checkout.open(); + const dialog = checkout.shadowRoot!.querySelector("#overlay")!; expect(windowOpenSpy).toHaveBeenCalledTimes(2); - expect(dialogShowModalSpy).toHaveBeenCalledTimes(1); + expect(dialog.dataset.state).toBeUndefined(); + expect(closeEventSpy).not.toHaveBeenCalled(); + }); + }); + + it("dispatches close when the blocked overlay is dismissed", () => { + POPUP_TARGETS.forEach((target) => { + const checkout = renderCheckout({ target }); + vi.spyOn(window, "open").mockReturnValue(null); + const closeEventSpy = vi.fn(); + checkout.addEventListener("ec.close", closeEventSpy); - checkout.focus(); - expect(mockWindow.focus).toHaveBeenCalled(); + checkout.open(); + checkout + .shadowRoot!.querySelector("#overlay-blocked-close-button")! + .click(); - checkout.close(); - expect(mockWindow.close).toHaveBeenCalled(); expect(closeEventSpy).toHaveBeenCalledTimes(1); }); }); @@ -337,7 +353,7 @@ describe("", () => { checkout.open(); const dialog = checkout.shadowRoot!.querySelector("dialog") as HTMLDialogElement; - dialog.dispatchEvent(new Event("close")); + dialog.close(); expect(mockPopup.close).toHaveBeenCalled(); expect(closeEventSpy).toHaveBeenCalled(); diff --git a/platforms/web/src/checkout.css b/platforms/web/src/checkout.css index 24bc4475f..f2d915fa5 100644 --- a/platforms/web/src/checkout.css +++ b/platforms/web/src/checkout.css @@ -81,6 +81,11 @@ } } +.overlay[data-state="blocked"] slot[name="overlay"], +.overlay:not([data-state="blocked"]) slot[name="overlay-blocked"] { + display: none; +} + .overlay-content-wrapper { display: grid; grid-template-rows: 1fr 20%; diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index c1b9daaa6..79c682eb0 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -153,6 +153,24 @@ const SHADOW_TEMPLATE = createTemplate(html` + +
+
+ Your browser blocked the checkout window.
+ +
+ +
+
@@ -206,6 +224,8 @@ export class ShopifyCheckout // Manages the listeners for the popup window, new tabs, and scrim dialog #currentOpen: { controller: AbortController } | null = null; + // Manages the listeners for the scrim dialog while it shows the blocked-window state + #blockedOpen: { controller: AbortController } | null = null; // Manages the global message event listener for checkout protocol communication #checkoutProtocolController: { controller: AbortController } | null = null; // Shared protocol client that decodes messages and dispatches to handlers @@ -408,6 +428,14 @@ export class ShopifyCheckout return this.shadowRoot?.querySelector("#overlay-link") ?? undefined; } + get #dialogRetryButtonElement(): HTMLButtonElement | undefined { + return this.shadowRoot?.querySelector("#overlay-retry-button") ?? undefined; + } + + get #dialogBlockedCloseButtonElement(): HTMLButtonElement | undefined { + return this.shadowRoot?.querySelector("#overlay-blocked-close-button") ?? undefined; + } + get #targetElement(): HTMLDivElement | undefined { return this.shadowRoot?.querySelector(".Shopify-target") ?? undefined; } @@ -436,10 +464,10 @@ export class ShopifyCheckout return; } + const isRetry = this.#blockedOpen !== null; + // Close any existing sessions before opening a new one - if (this.#currentOpen) { - this.close(); - } + this.close(); this.#checkout = undefined; this.#error = undefined; @@ -470,18 +498,16 @@ export class ShopifyCheckout } } - // The browser refused to open the window (typically a popup blocker, or - // `open()` was called outside a user gesture). There is no window to focus - // and nothing for the buyer to return to, so no overlay or session is created. if (!checkoutWindow) { this.#logger.warn("checkout window could not be opened; the browser may have blocked it"); this.#recorder?.recordError({ category: "navigation", stage: "presentation", code: "blocked", - retryable: false, - isRetry: false, + retryable: true, + isRetry, }); + this.#showBlockedOverlay(); return; } @@ -490,65 +516,48 @@ export class ShopifyCheckout // Opens a dialog element to act as a scrim over the current window while the popup is open. // The dialog can be closed by the user, or will close itself when the popup is closed. const dialog = this.#dialogElement; - const dialogBackground = this.#dialogBackgroundElement; const dialogCloseButton = this.#dialogCloseButtonElement; const dialogButton = this.#dialogButtonElement; - if (dialog && dialogBackground) { - // By default we show the scrim. - // If a consumer wants to hide it, they can either: - // 1. Set `display: none` on the `` element itself - // 2. Set `display: none` on the overlay using CSS parts, e.g., - // ``` - // shopify-checkout::part(overlay) { - // display: none; - // } - // ``` - // It's important not to call `dialog.showModal()` if the dialog is not visible because it traps focus and - // hides the rest of the page from the accessibility tree. - const isElementHidden = window.getComputedStyle(this).getPropertyValue("display") === "none"; - const isOverlayHidden = - window.getComputedStyle(dialogBackground).getPropertyValue("display") === "none"; - const showDialog = !isElementHidden && !isOverlayHidden; - - if (showDialog) { - dialog.showModal(); - - dialogCloseButton?.addEventListener( - "click", - () => { - dialog.close(); - }, - { - signal: abortController.signal, - }, - ); + if (dialog && this.#isDialogVisible()) { + dialog.showModal(); - dialog.addEventListener( - "close", - () => { - abortController.abort(); - }, - { - signal: abortController.signal, - }, - ); + dialogCloseButton?.addEventListener( + "click", + () => { + dialog.close(); + }, + { + signal: abortController.signal, + }, + ); - dialogButton?.addEventListener( - "click", - (event: MouseEvent) => { - event.preventDefault(); - this.#checkoutWindow?.focus(); - }, - { - signal: abortController.signal, - }, - ); + dialog.addEventListener( + "close", + () => { + // `close` fires asynchronously; ignore it if the dialog has since been re-shown + if (dialog.open) return; + abortController.abort(); + }, + { + signal: abortController.signal, + }, + ); - abortController.signal.addEventListener("abort", () => { - dialog.close(); - }); - } + dialogButton?.addEventListener( + "click", + (event: MouseEvent) => { + event.preventDefault(); + this.#checkoutWindow?.focus(); + }, + { + signal: abortController.signal, + }, + ); + + abortController.signal.addEventListener("abort", () => { + dialog.close(); + }); } abortController.signal.addEventListener("abort", () => { @@ -585,9 +594,85 @@ export class ShopifyCheckout } close(): void { - if (this.#currentOpen) { - this.#currentOpen.controller.abort(); - } + this.#blockedOpen?.controller.abort(); + this.#currentOpen?.controller.abort(); + } + + /** + * By default we show the scrim. If a consumer wants to hide it, they can either: + * 1. Set `display: none` on the `` element itself + * 2. Set `display: none` on the overlay using CSS parts, e.g., + * ``` + * shopify-checkout::part(overlay) { + * display: none; + * } + * ``` + * It's important not to call `dialog.showModal()` if the dialog is not visible because it traps + * focus and hides the rest of the page from the accessibility tree. + */ + #isDialogVisible(): boolean { + const dialogBackground = this.#dialogBackgroundElement; + if (!dialogBackground) return false; + + const isElementHidden = window.getComputedStyle(this).getPropertyValue("display") === "none"; + const isOverlayHidden = + window.getComputedStyle(dialogBackground).getPropertyValue("display") === "none"; + return !isElementHidden && !isOverlayHidden; + } + + #showBlockedOverlay(): void { + const dialog = this.#dialogElement; + if (!dialog || !this.#isDialogVisible()) return; + + const abortController = new AbortController(); + + dialog.dataset.state = "blocked"; + dialog.showModal(); + + let retrying = false; + + this.#dialogRetryButtonElement?.addEventListener( + "click", + () => { + retrying = true; + this.open(); + retrying = false; + }, + { + signal: abortController.signal, + }, + ); + + this.#dialogBlockedCloseButtonElement?.addEventListener( + "click", + () => { + dialog.close(); + }, + { + signal: abortController.signal, + }, + ); + + dialog.addEventListener( + "close", + () => { + // `close` fires asynchronously; ignore it if the dialog has since been re-shown + if (dialog.open) return; + abortController.abort(); + }, + { + signal: abortController.signal, + }, + ); + + abortController.signal.addEventListener("abort", () => { + delete dialog.dataset.state; + if (dialog.open) dialog.close(); + this.#blockedOpen = null; + if (!retrying) this.dispatchEvent(new ShopifyCheckoutCloseEvent()); + }); + + this.#blockedOpen = { controller: abortController }; } #recordNavigationSuccess(): void { @@ -964,7 +1049,7 @@ export class ShopifyCheckout switch (name) { case "target": { - if (oldValue !== newValue && this.#currentOpen) { + if (oldValue !== newValue && (this.#currentOpen || this.#blockedOpen)) { this.close(); } From ffb6e0015bfd5f5326378cbbe16ffe33702645bb Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Thu, 24 Sep 2026 13:53:09 -0300 Subject: [PATCH 4/7] Treat any open() while the blocked overlay shows as a retry --- platforms/web/src/checkout-window.test.ts | 15 +++++++++++++++ platforms/web/src/checkout.ts | 11 ++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/platforms/web/src/checkout-window.test.ts b/platforms/web/src/checkout-window.test.ts index a636b7957..1e68b73fb 100644 --- a/platforms/web/src/checkout-window.test.ts +++ b/platforms/web/src/checkout-window.test.ts @@ -302,6 +302,21 @@ describe("", () => { }); }); + it("does not dispatch close when open() is called while the blocked overlay is showing", () => { + POPUP_TARGETS.forEach((target) => { + const checkout = renderCheckout({ target }); + vi.spyOn(window, "open").mockReturnValue(null); + vi.spyOn(HTMLDialogElement.prototype, "showModal").mockImplementation(() => {}); + const closeEventSpy = vi.fn(); + checkout.addEventListener("ec.close", closeEventSpy); + + checkout.open(); + checkout.open(); + + expect(closeEventSpy).not.toHaveBeenCalled(); + }); + }); + it("dispatches close when the blocked overlay is dismissed", () => { POPUP_TARGETS.forEach((target) => { const checkout = renderCheckout({ target }); diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index 79c682eb0..bb37bbd33 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -118,6 +118,8 @@ function originMatchesPattern(pattern: string, origin: URL): boolean { const WINDOW_OPEN_INVALID_URL_WARNING = "ec.window.open_request received without a valid url"; +const RETRY_ABORT_REASON = "retry"; + const EMBED_DELEGATIONS = [EmbeddedCheckoutProtocol.Delegations.windowOpen] as const; const CHECKOUT_APPEARANCES = new Map([ ["app:light", { colorScheme: "light", branding: "app" }], @@ -467,6 +469,7 @@ export class ShopifyCheckout const isRetry = this.#blockedOpen !== null; // Close any existing sessions before opening a new one + this.#blockedOpen?.controller.abort(RETRY_ABORT_REASON); this.close(); this.#checkout = undefined; @@ -629,14 +632,10 @@ export class ShopifyCheckout dialog.dataset.state = "blocked"; dialog.showModal(); - let retrying = false; - this.#dialogRetryButtonElement?.addEventListener( "click", () => { - retrying = true; this.open(); - retrying = false; }, { signal: abortController.signal, @@ -669,7 +668,9 @@ export class ShopifyCheckout delete dialog.dataset.state; if (dialog.open) dialog.close(); this.#blockedOpen = null; - if (!retrying) this.dispatchEvent(new ShopifyCheckoutCloseEvent()); + if (abortController.signal.reason !== RETRY_ABORT_REASON) { + this.dispatchEvent(new ShopifyCheckoutCloseEvent()); + } }); this.#blockedOpen = { controller: abortController }; From 7f31272b6dae065be83162be3351d4e04a59b702 Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Fri, 25 Sep 2026 14:42:44 -0300 Subject: [PATCH 5/7] Use the renamed close event in blocked-overlay tests and README --- platforms/web/README.md | 2 +- platforms/web/src/checkout-window.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platforms/web/README.md b/platforms/web/README.md index 18edc8780..3e4503848 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -355,7 +355,7 @@ Where the checkout is presented. Defaults to `"auto"`. > [!NOTE] > If the browser refuses to open the window (for example, a popup blocker, or > `open()` called outside a user gesture), the [overlay scrim](#overlay-scrim) -> says so and offers a button to try again. Closing it dispatches `ec.close`. +> says so and offers a button to try again. Closing it dispatches `close`. > If the overlay is hidden, nothing is shown and no events fire. The component > logs a warning at `log-level="warn"` or more verbose. diff --git a/platforms/web/src/checkout-window.test.ts b/platforms/web/src/checkout-window.test.ts index 1e68b73fb..e3216f002 100644 --- a/platforms/web/src/checkout-window.test.ts +++ b/platforms/web/src/checkout-window.test.ts @@ -290,7 +290,7 @@ describe("", () => { .mockReturnValueOnce(createMockWindow()); vi.spyOn(HTMLDialogElement.prototype, "showModal").mockImplementation(() => {}); const closeEventSpy = vi.fn(); - checkout.addEventListener("ec.close", closeEventSpy); + checkout.addEventListener("close", closeEventSpy); checkout.open(); checkout.shadowRoot!.querySelector("#overlay-retry-button")!.click(); @@ -308,7 +308,7 @@ describe("", () => { vi.spyOn(window, "open").mockReturnValue(null); vi.spyOn(HTMLDialogElement.prototype, "showModal").mockImplementation(() => {}); const closeEventSpy = vi.fn(); - checkout.addEventListener("ec.close", closeEventSpy); + checkout.addEventListener("close", closeEventSpy); checkout.open(); checkout.open(); @@ -322,7 +322,7 @@ describe("", () => { const checkout = renderCheckout({ target }); vi.spyOn(window, "open").mockReturnValue(null); const closeEventSpy = vi.fn(); - checkout.addEventListener("ec.close", closeEventSpy); + checkout.addEventListener("close", closeEventSpy); checkout.open(); checkout From 8f58d6aa3c1fc342b6b36d1f034773fa3854e6e8 Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Fri, 25 Sep 2026 15:05:48 -0300 Subject: [PATCH 6/7] Keep the blocked-overlay close event out of the custom elements manifest --- platforms/web/src/checkout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts index bb37bbd33..78c4b932e 100644 --- a/platforms/web/src/checkout.ts +++ b/platforms/web/src/checkout.ts @@ -669,6 +669,7 @@ export class ShopifyCheckout if (dialog.open) dialog.close(); this.#blockedOpen = null; if (abortController.signal.reason !== RETRY_ABORT_REASON) { + /** @ignore - Events are documented by the class @event tags. */ this.dispatchEvent(new ShopifyCheckoutCloseEvent()); } }); From 1e038d4918b8edecd53d00b564167a2fb845b271 Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Fri, 25 Sep 2026 17:55:30 -0300 Subject: [PATCH 7/7] Document the blocked-window telemetry mapping --- telemetry/contract/metrics.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telemetry/contract/metrics.md b/telemetry/contract/metrics.md index de4418183..5f9b0a208 100644 --- a/telemetry/contract/metrics.md +++ b/telemetry/contract/metrics.md @@ -91,6 +91,12 @@ A terminal `ec.error` protocol message is recorded as `category=protocol`, payload additionally records `checkout_kit_protocol_decode_error` with `method=ec.error`. +A checkout window the browser blocks on web is recorded as +`category=navigation`, `stage=presentation`, `code=blocked`, +`retryable=true`, and `is_retry=false`. A block that happens while the blocked +overlay is already showing, such as a retry from its Open checkout button, is +recorded with `is_retry=true`. + ## Prohibited data - Checkout, cart, order, shop, customer, or payment identifiers