Skip to content
Open
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
11 changes: 10 additions & 1 deletion platforms/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,13 @@ 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), the [overlay scrim](#overlay-scrim)
> 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.

### `appearance`

Sets the checkout appearance preference. Defaults to `"storefront"`.
Expand Down Expand Up @@ -469,7 +476,9 @@ shopify-checkout {

While a popup is open the component renders a `<dialog>` 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:
Expand Down
76 changes: 74 additions & 2 deletions platforms/web/src/checkout-window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,85 @@ describe("<shopify-checkout>", () => {
category: "navigation",
stage: "presentation",
code: "blocked",
retryable: false,
retryable: true,
isRetry: false,
});
// Should not throw error when popup is blocked
});
});

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<HTMLDialogElement>("#overlay")!;
expect(dialogShowModalSpy).toHaveBeenCalledTimes(1);
expect(dialog.dataset.state).toBe("blocked");
expect(consoleWarnSpy).toHaveBeenCalledWith(
"<shopify-checkout>: 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 windowOpenSpy = vi
.spyOn(window, "open")
.mockReturnValueOnce(null)
.mockReturnValueOnce(createMockWindow());
vi.spyOn(HTMLDialogElement.prototype, "showModal").mockImplementation(() => {});
const closeEventSpy = vi.fn();
checkout.addEventListener("close", closeEventSpy);

checkout.open();
checkout.shadowRoot!.querySelector<HTMLButtonElement>("#overlay-retry-button")!.click();

const dialog = checkout.shadowRoot!.querySelector<HTMLDialogElement>("#overlay")!;
expect(windowOpenSpy).toHaveBeenCalledTimes(2);
expect(dialog.dataset.state).toBeUndefined();
expect(closeEventSpy).not.toHaveBeenCalled();
});
});

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("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 });
vi.spyOn(window, "open").mockReturnValue(null);
const closeEventSpy = vi.fn();
checkout.addEventListener("close", closeEventSpy);

checkout.open();
checkout
.shadowRoot!.querySelector<HTMLButtonElement>("#overlay-blocked-close-button")!
.click();

expect(closeEventSpy).toHaveBeenCalledTimes(1);
});
});

it("enforces maximum window size constraints", () => {
POPUP_TARGETS.forEach((target) => {
const windowOpenSpy = vi.spyOn(window, "open").mockReturnValue(createMockWindow());
Expand Down Expand Up @@ -296,7 +368,7 @@ describe("<shopify-checkout>", () => {
checkout.open();

const dialog = checkout.shadowRoot!.querySelector("dialog") as HTMLDialogElement;
dialog.dispatchEvent(new Event("close"));
dialog.close();

expect(mockPopup.close).toHaveBeenCalled();
expect(closeEventSpy).toHaveBeenCalled();
Expand Down
5 changes: 5 additions & 0 deletions platforms/web/src/checkout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
Loading
Loading