diff --git a/platforms/web/sample/README.md b/platforms/web/sample/README.md
index a7e2e2e6..b3a7d949 100644
--- a/platforms/web/sample/README.md
+++ b/platforms/web/sample/README.md
@@ -14,7 +14,7 @@ pnpm sample
Vite serves at `http://localhost:5173`. The page has three panels:
-- **Options** — form for `src`, `target` (`auto` | `popup` | `inline`),
+- **Options** — form for `src`, `target` (`auto` | `popup`),
`preload`, and `debug`, plus buttons for `open()`, `close()`, and `focus()`.
- **Demo Storefront** — a mocked product card with **Buy now** calling
`checkout.open()`. The button stays disabled until a checkout URL is set.
@@ -24,8 +24,7 @@ Vite serves at `http://localhost:5173`. The page has three panels:
time.
The element is mounted on `
`. For `popup` / `auto`, the visible UI is
-mostly the overlay scrim when checkout is open; for **inline**, checkout renders
-in an iframe inside the component shadow tree.
+mostly the overlay scrim while checkout is open in a separate window or tab.
## Build
diff --git a/platforms/web/src/checkout.styles.ts b/platforms/web/src/checkout.styles.ts
index 1a1dbd1d..d05d6ba0 100644
--- a/platforms/web/src/checkout.styles.ts
+++ b/platforms/web/src/checkout.styles.ts
@@ -28,7 +28,6 @@ export const STYLES: SafeMarkup = css`
box-sizing: border-box;
}
- #checkout-iframe,
#shopify-element-wrapper,
.Shopify-target {
inline-size: 100%;
@@ -36,20 +35,6 @@ export const STYLES: SafeMarkup = css`
border: none;
}
- #checkout-iframe {
- display: none;
- }
-
- .Shopify-target--inline {
- #checkout-iframe {
- display: block;
- }
-
- .overlay {
- display: none;
- }
- }
-
:host {
@media (prefers-reduced-motion: reduce) {
--shopify-checkout-overlay-transition-duration: 1ms;
diff --git a/platforms/web/src/checkout.test.ts b/platforms/web/src/checkout.test.ts
index 0c050920..03dfba93 100644
--- a/platforms/web/src/checkout.test.ts
+++ b/platforms/web/src/checkout.test.ts
@@ -97,19 +97,6 @@ describe("", () => {
checkout.target = newTarget;
expect(checkout.getAttribute("target")).toBe(newTarget);
});
-
- describe('when target is "inline"', () => {
- it("renders an iframe on mount without needing open()", () => {
- const checkout = renderCheckout({ target: "inline" });
-
- const iframe = checkout.shadowRoot!.querySelector("iframe");
- expect(iframe).not.toBeNull();
-
- const url = new URL(iframe!.src);
- expect(url.searchParams.get("ec_version")).toBe(EMBED_PROTOCOL_VERSION);
- expect(url.searchParams.get("ec_delegate")).toBe("window.open");
- });
- });
});
describe("preload", () => {
@@ -143,7 +130,7 @@ describe("", () => {
expect(checkout.getAttribute("preload")).toBeNull();
});
- it("adds a preload link to the iframe src when set to true", () => {
+ it("adds a preload link when set to true", () => {
const checkout = renderCheckout();
checkout.preload = true;
@@ -246,13 +233,10 @@ describe("", () => {
"blob:https://shop.example.com/abc",
"file:///etc/passwd",
"not a url",
- ])("renders empty iframe src and no overlay href when src is %s", (badSrc) => {
- const checkout = renderCheckout({ src: badSrc, target: "inline" });
+ ])("leaves overlay link without href when src is %s", (badSrc) => {
+ const checkout = renderCheckout({ src: badSrc });
- const iframe = checkout.shadowRoot!.querySelector("#checkout-iframe");
- // #srcAsURL returns undefined for non-https values, so the iframe
- // src is set to '' rather than the raw input.
- expect(iframe!.getAttribute("src")).toBe("");
+ expect(checkout.shadowRoot!.querySelector("iframe")).toBeNull();
const overlayLink = checkout.shadowRoot!.querySelector("#overlay-link");
expect(overlayLink!.hasAttribute("href")).toBe(false);
@@ -263,7 +247,7 @@ describe("", () => {
// The URL constructor accepts this (the `"` becomes part of the
// pathname); the value is rendered via DOM APIs rather than string
// interpolation, so no markup is parsed into the shadow root.
- const checkout = renderCheckout({ src, target: "inline" });
+ const checkout = renderCheckout({ src });
expect(checkout.shadowRoot!.querySelector("script")).toBeNull();
expect((window as unknown as { __xssed?: boolean }).__xssed).toBeUndefined();
@@ -477,40 +461,6 @@ describe("", () => {
});
});
- describe('when target="inline"', () => {
- it("shows the checkout in an iframe", () => {
- const checkout = renderCheckout({ target: "inline" });
-
- const iframe = checkout.shadowRoot!.querySelector("iframe");
- expect(iframe).not.toBeNull();
- expect(iframe!.getAttribute("allow")).toBe(
- "publickey-credentials-get https://pay.shopify.com https://shop.app; geolocation",
- );
-
- const url = new URL(iframe!.src);
- expect(url.searchParams.get("ec_version")).toBe(EMBED_PROTOCOL_VERSION);
- expect(url.searchParams.get("ec_delegate")).toBe("window.open");
- });
-
- it("sets the correct iframe security attributes", () => {
- const checkout = renderCheckout({ target: "inline" });
-
- const iframe = checkout.shadowRoot!.querySelector("iframe");
- expect(iframe).not.toBeNull();
-
- expect(iframe!.id).toBe("checkout-iframe");
- expect(iframe!.title).toBe("Checkout");
-
- expect(iframe!.getAttribute("allow")).toBe(
- "publickey-credentials-get https://pay.shopify.com https://shop.app; geolocation",
- );
-
- expect(iframe!.getAttribute("sandbox")).toBe(
- "allow-scripts allow-same-origin allow-forms allow-popups",
- );
- });
- });
-
describe('when target="_blank", "auto", or undefined', () => {
NEW_TAB_TARGETS.forEach((target) => {
it("opens in a new window", () => {
@@ -544,8 +494,6 @@ describe("", () => {
describe("focus", () => {
it("focuses the checkout window", () => {
- // Note: 'inline' target is excluded because the open() method returns early
- // for inline targets, so #checkoutWindow is never set and focus() has no effect
[...POPUP_TARGETS, "_blank"].forEach((target) => {
const checkout = renderCheckout({ target });
const mockPopup = {
@@ -563,16 +511,6 @@ describe("", () => {
});
describe("close", () => {
- describe('when target="inline"', () => {
- it("does not dispatch a close event", () => {
- const checkout = renderCheckout({ target: "inline" });
- const closeEventSpy = vi.fn();
- checkout.addEventListener("checkout:close", closeEventSpy);
- checkout.close();
- expect(closeEventSpy).not.toHaveBeenCalled();
- });
- });
-
describe('when target="popup", "auto", or undefined', () => {
it("dispatches close event when popup is closed", () => {
POPUP_TARGETS.forEach((target) => {
@@ -623,10 +561,6 @@ describe("", () => {
describe("it subscribes to checkout-protocol events", () => {
describe("ec.ready handshake", () => {
it("auto-responds with an empty result and does not dispatch a DOM event", async () => {
- // Use popup target so #checkoutWindow is a controllable mock
- // window we can use as the message source and spy on for the
- // wire-level response. (Inline iframes inside shadow DOM have a
- // null contentWindow in jsdom.)
const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onReadySpy = vi.fn();
// ec:ready is no longer a public event; cast through `never` to verify
@@ -664,12 +598,12 @@ describe("", () => {
describe("ec:start", () => {
it("updates the checkout property and dispatches an ec:start event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onStartSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:start", onStartSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.start", payload);
+ simulateProtocolMessageEvent(checkout, "ec.start", payload, { source: mockCheckoutWindow });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -679,12 +613,14 @@ describe("", () => {
describe("ec:complete", () => {
it("updates the checkout property and dispatches an ec:complete event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onCompleteSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:complete", onCompleteSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.complete", payload);
+ simulateProtocolMessageEvent(checkout, "ec.complete", payload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -694,12 +630,14 @@ describe("", () => {
describe("ec:error", () => {
it("updates the error property and dispatches an ec:error event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onErrorSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:error", onErrorSpy);
const errorPayload = makeErrorPayload();
- simulateProtocolMessageEvent(checkout, "ec.error", errorPayload);
+ simulateProtocolMessageEvent(checkout, "ec.error", errorPayload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.error).toStrictEqual(errorPayload);
@@ -709,12 +647,14 @@ describe("", () => {
describe("ec:lineItemsChange", () => {
it("updates the checkout property and dispatches an ec:lineItemsChange event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onLineItemsChangeSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:lineItemsChange", onLineItemsChangeSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.line_items.change", payload);
+ simulateProtocolMessageEvent(checkout, "ec.line_items.change", payload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -724,12 +664,14 @@ describe("", () => {
describe("ec:buyerChange", () => {
it("updates the checkout property and dispatches an ec:buyerChange event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onBuyerChangeSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:buyerChange", onBuyerChangeSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.buyer.change", payload);
+ simulateProtocolMessageEvent(checkout, "ec.buyer.change", payload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -739,12 +681,14 @@ describe("", () => {
describe("ec:totalsChange", () => {
it("updates the checkout property and dispatches an ec:totalsChange event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onTotalsChangeSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:totalsChange", onTotalsChangeSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.totals.change", payload);
+ simulateProtocolMessageEvent(checkout, "ec.totals.change", payload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -754,12 +698,14 @@ describe("", () => {
describe("ec:messagesChange", () => {
it("updates the checkout property and dispatches an ec:messagesChange event", async () => {
- const checkout = renderCheckout({ target: "inline" });
+ const { checkout, mockCheckoutWindow } = openPopupCheckout();
const onMessagesChangeSpy = vi.fn();
const listenForEvent = waitForEvent(checkout, "ec:messagesChange", onMessagesChangeSpy);
const payload = makeCheckoutPayload();
- simulateProtocolMessageEvent(checkout, "ec.messages.change", payload);
+ simulateProtocolMessageEvent(checkout, "ec.messages.change", payload, {
+ source: mockCheckoutWindow,
+ });
await listenForEvent;
expect(checkout.checkout).toBe(payload.checkout);
@@ -976,14 +922,6 @@ describe("", () => {
const url = new URL(expectWindowOpenArgs(windowOpenSpy)[0] as string);
expect(url.searchParams.get("ec_delegate")).toBe("window.open");
});
-
- it("declares window.open delegation in inline iframe URL", () => {
- const checkout = renderCheckout({ target: "inline" });
-
- const iframe = checkout.shadowRoot!.querySelector("#checkout-iframe") as HTMLIFrameElement;
- const url = new URL(iframe.src);
- expect(url.searchParams.get("ec_delegate")).toBe("window.open");
- });
});
describe("debug attribute", () => {
@@ -1039,27 +977,15 @@ describe("", () => {
describe("lifecycle", () => {
it("preserves the shadow tree across element moves", () => {
- const checkout = renderCheckout({ target: "inline" });
+ const checkout = renderCheckout();
const wrapper = checkout.shadowRoot!.querySelector("#shopify-element-wrapper");
- const iframe = checkout.shadowRoot!.querySelector("#checkout-iframe");
const newParent = document.createElement("div");
document.body.appendChild(newParent);
newParent.appendChild(checkout);
expect(checkout.shadowRoot!.querySelector("#shopify-element-wrapper")).toBe(wrapper);
- expect(checkout.shadowRoot!.querySelector("#checkout-iframe")).toBe(iframe);
- });
-
- it("does not add a duplicate iframe on reconnect of an inline element", () => {
- const checkout = renderCheckout({ target: "inline" });
- expect(checkout.shadowRoot!.querySelectorAll("#checkout-iframe")).toHaveLength(1);
-
- const newParent = document.createElement("div");
- document.body.appendChild(newParent);
- newParent.appendChild(checkout);
-
- expect(checkout.shadowRoot!.querySelectorAll("#checkout-iframe")).toHaveLength(1);
+ expect(checkout.shadowRoot!.querySelector("iframe")).toBeNull();
});
it("drops protocol messages while the element is disconnected", async () => {
@@ -1143,10 +1069,9 @@ describe("", () => {
* `origin` are derived from `checkout` so that the component's strict
* source-and-origin validation passes:
*
- * - `source`: the inline iframe's `contentWindow` if one exists,
- * otherwise `null`. Tests that exercise popup/new-tab targets must
- * pass `source` explicitly (typically the same mock window returned
- * from `window.open`).
+ * - `source`: pass the checkout browsing context (the mock window returned
+ * from `window.open` after `open()`, or another `MessageEventSource` to
+ * test drops). When omitted, defaults to `null` (messages are dropped).
* - `origin`: the origin of `checkout.src`. Override `origin` to test
* that messages from foreign origins are dropped.
*/
@@ -1160,8 +1085,7 @@ function simulateProtocolMessageEvent("#checkout-iframe");
- const source = options?.source === undefined ? (iframe?.contentWindow ?? null) : options.source;
+ const source = options?.source !== undefined ? options.source : null;
let origin = options?.origin;
if (origin === undefined) {
@@ -1243,9 +1167,8 @@ function createMockWindow() {
/**
* Sets up a popup-target checkout whose `#checkoutWindow` is a controllable
* mock window. Tests that exercise the strict source-and-origin validation
- * in `#handleMessage` should use this rather than inline target, because
- * jsdom does not create a browsing context for iframes inside shadow DOM
- * (so an inline iframe's `contentWindow` is always `null`).
+ * in `#handleMessage` should use this helper and pass `mockCheckoutWindow`
+ * as `source` in `simulateProtocolMessageEvent`.
*
* Callers receive the checkout, the mock window (use as both `source` for
* `simulateProtocolMessageEvent` and the spy target for response
diff --git a/platforms/web/src/checkout.ts b/platforms/web/src/checkout.ts
index e48e8fd6..2a01a389 100644
--- a/platforms/web/src/checkout.ts
+++ b/platforms/web/src/checkout.ts
@@ -72,14 +72,13 @@ const SHADOW_TEMPLATE = createTemplate(html`
`);
/**
- * An element that renders a Shopify Checkout. Checkout can be displayed either as a popup window (default)
- * or embedded as an iframe by setting the `mode` attribute. To use, create a `shopify-checkout` element,
- * set the `src` attribute to the checkout URL (typically retrieved from the `cart.checkoutUrl` field),
- * and then call `open()`.
+ * An element that renders a Shopify Checkout. Checkout opens in a popup or browser tab/window
+ * (see `target`). To use, create a `shopify-checkout` element, set the `src` attribute to the
+ * checkout URL (typically retrieved from the `cart.checkoutUrl` field), and then call `open()`.
*
* @attribute src - The URL of the checkout to load.
* @attribute preload - Whether to preload critical assets and data
- * @attribute target - Where the checkout is presented (auto, popup, new tab, or inline).
+ * @attribute target - Where the checkout is presented (auto, popup, new tab, or a named window).
*
* @event ec:start - Dispatched when the checkout has started
* @event ec:complete - Dispatched when the checkout was successfully completed
@@ -98,12 +97,6 @@ const SHADOW_TEMPLATE = createTemplate(html`
* checkout.setAttribute("src", cart.checkoutUrl);
* document.body.append(checkout);
* checkout.open();
- *
- * // Inline target
- * const checkout = document.createElement("shopify-checkout");
- * checkout.setAttribute("src", cart.checkoutUrl);
- * checkout.setAttribute("target", "inline");
- * document.body.append(checkout);
* ```
*/
export class ShopifyCheckout
@@ -262,10 +255,6 @@ export class ShopifyCheckout
return this.#error;
}
- get #iframeElement(): HTMLIFrameElement | undefined {
- return this.shadowRoot?.querySelector("#checkout-iframe") ?? undefined;
- }
-
get #dialogElement(): HTMLDialogElement | undefined {
return this.shadowRoot?.querySelector("#overlay") ?? undefined;
}
@@ -298,11 +287,6 @@ export class ShopifyCheckout
const { target } = this;
const src = this.#srcAsURL()?.href;
- // Inline targets render an iframe directly in the DOM when the element connects or target changes,
- // so no explicit open() call is needed. The close() method also has no effect on
- // inline targets since iframes don't respond to iframe.contentWindow.close().
- if (target === "inline") return;
-
if (!src) {
// eslint-disable-next-line no-console
console.warn("``: src property is empty or invalid, cannot open checkout");
@@ -433,15 +417,6 @@ export class ShopifyCheckout
this.#checkoutWindow?.focus();
}
- #updateIframeSrc() {
- const src = this.#srcAsURL()?.href;
- const iframeElement = this.#iframeElement;
-
- if (src && iframeElement && iframeElement.src !== src) {
- iframeElement.src = src;
- }
- }
-
/**
* Sets the overlay link href to the validated, parametrised checkout
* URL (matching what the popup would open)
@@ -541,30 +516,13 @@ export class ShopifyCheckout
return features;
}
- #addIframe() {
- const iframeEl = document.createElement("iframe");
- iframeEl.id = "checkout-iframe";
- iframeEl.title = "Checkout";
- iframeEl.setAttribute(
- "allow",
- "publickey-credentials-get https://pay.shopify.com https://shop.app; geolocation",
- );
- iframeEl.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
- iframeEl.src = this.#srcAsURL()?.href ?? "";
-
- this.#targetElement?.appendChild(iframeEl);
- this.#checkoutWindow = iframeEl.contentWindow ?? null;
- }
-
/* ------------------------------------------------------------
* Events
* ------------------------------------------------------------
*/
/**
- * Determines if a protocol message should dispatch a respondable event.
- * Only inline targets can respond to messages, and the message must have
- * an ID (requests) rather than being a notification.
+ * JSON-RPC request messages carry an `id`; notifications do not.
*/
#isRespondableRequest(
message: CheckoutProtocolMessage,
@@ -699,14 +657,6 @@ export class ShopifyCheckout
this.#applyTargetClass();
this.#updateOverlayLink();
- if (this.target === "inline") {
- if (this.#iframeElement) {
- this.#checkoutWindow = this.#iframeElement.contentWindow ?? null;
- } else {
- this.#addIframe();
- }
- }
-
this.#initCheckoutProtocol();
}
@@ -728,18 +678,10 @@ export class ShopifyCheckout
this.#updatePreloadLink();
break;
case "src":
- this.#updateIframeSrc();
this.#updatePreloadLink();
this.#updateOverlayLink();
break;
case "target": {
- if (oldValue === "inline" && newValue !== "inline") {
- this.#iframeElement?.remove();
- this.#checkoutWindow = null;
- } else if (newValue === "inline") {
- this.#addIframe();
- }
-
if (oldValue !== newValue && this.#currentOpen) {
this.close();
}
diff --git a/platforms/web/src/checkout.types.ts b/platforms/web/src/checkout.types.ts
index 809595c9..940996d7 100644
--- a/platforms/web/src/checkout.types.ts
+++ b/platforms/web/src/checkout.types.ts
@@ -44,7 +44,7 @@ import type { Checkout, EcReadyParams, ShopCash, UcpErrorResponse } from "./ucp-
// Documentation-safe types:
-export type CheckoutTarget = "auto" | "popup" | "inline" | "_blank";
+export type CheckoutTarget = "auto" | "popup" | "_blank";
export interface CheckoutAttributes {
src?: string;
@@ -89,7 +89,6 @@ export interface CheckoutProperties {
/**
* The mode in which to display the checkout when opened. Defaults to `'auto'`.
* - `'popup'`: Opens checkout in a popup window
- * - `'inline'`: Embeds checkout in an iframe within the component
* - `'_blank' | `'auto'`: Opens checkout in a new tab (default)
* - `string`: Opens checkout in a new named window
*
diff --git a/platforms/web/vite.config.ts b/platforms/web/vite.config.ts
index 43ea93e3..1ca1a12d 100644
--- a/platforms/web/vite.config.ts
+++ b/platforms/web/vite.config.ts
@@ -42,7 +42,7 @@ export default defineConfig({
passWithNoTests: true,
environmentOptions: {
happyDOM: {
- // Prevent iframes from actually fetching their src in unit tests.
+ // Prevent checkout URLs from being fetched in unit tests.
settings: {
disableIframePageLoading: true,
disableErrorCapturing: true,