> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Astro

> Integrate Commerce Engine Checkout into Astro sites using the root checkout helpers. No dedicated binding needed — works with ClientRouter and framework islands.

Astro does not need a dedicated checkout binding. Use the root `@commercengine/checkout` helpers (`initCheckout`, `getCheckout`, `subscribeToCheckout`) directly in layout scripts or client modules.

### NPM Package

<Card title="@commercengine/checkout" icon="npm" href="https://www.npmjs.com/package/@commercengine/checkout">
  Root checkout helpers for Astro. Import from `@commercengine/checkout` (not a framework-specific subpath).
</Card>

### Installation

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/checkout
```

### Setup

Call `initCheckout` once in your root layout script. Listen for `astro:page-load` so checkout re-initializes after `ClientRouter` page swaps:

```astro src/layouts/Layout.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
---
// src/layouts/Layout.astro
---

<html lang="en">
  <head><slot name="head" /></head>
  <body>
    <slot />

    <script type="module">
      import { initCheckout } from "@commercengine/checkout";

      function init() {
        initCheckout({
          storeId: "store_xxx",
          apiKey: "ak_xxx",
          environment: "production",
          onComplete: (order) => {
            window.location.href = `/thank-you?order=${order.orderNumber}`;
          },
        });
      }

      init();
      document.addEventListener("astro:page-load", init);
    </script>
  </body>
</html>
```

<Info>
  Feature flags, login methods, drawer direction, and payment provider are managed in Checkout Studio (Config API), not as SDK init options. Use `theme` only for per-session overrides.
</Info>

### Imperative API

Since there is no framework-specific hook, use `getCheckout()` and `subscribeToCheckout()` for imperative control:

```astro src/components/CartButton.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
<button id="cart-btn" disabled>Cart (0)</button>

<script>
  import { getCheckout, subscribeToCheckout } from "@commercengine/checkout";

  const btn = document.getElementById("cart-btn")!;

  subscribeToCheckout(
    (state) => state.isReady,
    (isReady) => {
      (btn as HTMLButtonElement).disabled = !isReady;
    }
  );

  subscribeToCheckout(
    (state) => state.cartCount,
    (count) => {
      btn.textContent = `Cart (${count})`;
    }
  );

  btn.addEventListener("click", () => getCheckout().openCart());
</script>
```

### Add to cart

```astro src/components/AddToCartButton.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
<button class="add-to-cart" data-product-id="product_abc" data-variant-id="variant_xyz">
  Add to Cart
</button>

<script>
  import { getCheckout } from "@commercengine/checkout";

  document.querySelectorAll(".add-to-cart").forEach((btn) => {
    btn.addEventListener("click", () => {
      const productId = btn.getAttribute("data-product-id")!;
      const variantId = btn.getAttribute("data-variant-id");
      getCheckout().addToCart(productId, variantId, 1);
    });
  });
</script>
```

### Framework islands

If you use React, Vue, Svelte, or Solid islands inside Astro, you can use the corresponding framework binding's hook/composable/store inside the island. The checkout instance is shared — `initCheckout` only needs to be called once in the layout.

```tsx src/components/CartButton.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
// React island example
import { useCheckout } from "@commercengine/checkout/react";

export function CartButton() {
  const { openCart, cartCount, isReady } = useCheckout();
  return (
    <button onClick={openCart} disabled={!isReady}>
      Cart ({cartCount})
    </button>
  );
}
```

```astro src/pages/index.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
---
import { CartButton } from "../components/CartButton";
---

<CartButton client:load />
```

### Storefront SDK integration

If your Astro app also uses the `@commercengine/storefront` SDK, you **must** use `authMode: "provided"` with two-way token sync. Create a client module that owns both the storefront session and checkout init:

```typescript src/lib/storefront-client.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { getCheckout, initCheckout } from "@commercengine/checkout";
import { BrowserTokenStorage, createStorefront, Environment } from "@commercengine/storefront";

const tokenStorage = new BrowserTokenStorage("myapp_");

const storefront = createStorefront({
  storeId: import.meta.env.PUBLIC_STORE_ID ?? "",
  apiKey: import.meta.env.PUBLIC_API_KEY ?? "",
  environment:
    import.meta.env.PUBLIC_CE_ENV === "staging" ? Environment.Staging : Environment.Production,
  session: {
    tokenStorage,
    onTokensUpdated: (accessToken, refreshToken) => {
      getCheckout().updateTokens(accessToken, refreshToken);
    },
  },
});

const sessionSdk = storefront.session();
let initPromise: Promise<void> | null = null;

export function initHostedCheckout() {
  if (initPromise) return initPromise;

  initPromise = (async () => {
    const accessToken = await sessionSdk.ensureAccessToken();
    const refreshToken = await tokenStorage.getRefreshToken();

    initCheckout({
      storeId: import.meta.env.PUBLIC_STORE_ID ?? "",
      apiKey: import.meta.env.PUBLIC_API_KEY ?? "",
      environment: import.meta.env.PUBLIC_CE_ENV === "staging" ? "staging" : "production",
      authMode: "provided",
      accessToken: accessToken ?? undefined,
      refreshToken: refreshToken ?? undefined,
      onTokensUpdated: ({ accessToken, refreshToken }) => {
        void sessionSdk.setTokens(accessToken, refreshToken);
      },
    });
  })().catch((error) => {
    initPromise = null;
    throw error;
  });

  return initPromise;
}
```

Then call it from the layout:

```astro src/layouts/Layout.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
<script>
  import { initHostedCheckout } from "@/lib/storefront-client";

  function bootstrap() {
    void initHostedCheckout();
  }

  bootstrap();
  document.addEventListener("astro:page-load", bootstrap);
</script>
```

<Tip>
  If your Astro app uses the first-party `@commercengine/storefront/astro` wrapper instead of the base SPA SDK, see the [Astro SDK Integration](/sdk/astro-integration#hosted-checkout--astro) page for the equivalent pattern using `storefront.bootstrap()` and `clientStorefront()`.
</Tip>

### Preserving the iframe across ClientRouter navigations

Astro's `ClientRouter` replaces `document.body` on each navigation, which remounts any body-mounted checkout iframe. If remounting is acceptable, no extra work is needed.

If you need to **keep the iframe alive** across page transitions, install a custom swap that only replaces your page content region instead of the full body:

<Steps>
  <Step title="Wrap page content in a data attribute">
    ```astro src/layouts/Layout.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
    <body>
      <div data-page-content>
        <slot />
      </div>

      <!-- checkout iframe lives outside data-page-content -->
      <script type="module">
        // ... initCheckout / initHostedCheckout here
      </script>
    </body>
    ```
  </Step>

  <Step title="Add a custom swap handler">
    ```astro src/layouts/Layout.astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
    <script>
      import { swapFunctions } from "astro:transitions/client";

      document.addEventListener("astro:before-swap", (event) => {
        event.swap = () => {
          swapFunctions.deselectScripts(event.newDocument);
          swapFunctions.swapRootAttributes(event.newDocument);
          swapFunctions.swapHeadElements(event.newDocument);
          const restoreFocus = swapFunctions.saveFocus();

          const nextContent = event.newDocument.querySelector("[data-page-content]");
          const currentContent = document.querySelector("[data-page-content]");

          if (nextContent && currentContent) {
            swapFunctions.swapBodyElement(nextContent, currentContent);
          } else {
            swapFunctions.swapBodyElement(event.newDocument.body, document.body);
          }

          restoreFocus();
        };
      });
    </script>
    ```
  </Step>
</Steps>

This swaps only the `[data-page-content]` region, leaving the checkout iframe (and any other body-level elements) untouched.

### API reference

Since Astro uses the root helpers, the imperative API is slightly different from the framework-specific hooks:

| Function                                  | Description                                           |
| ----------------------------------------- | ----------------------------------------------------- |
| `initCheckout(options)`                   | Create the checkout iframe. Call once at startup.     |
| `getCheckout()`                           | Returns the checkout instance for imperative control. |
| `subscribeToCheckout(selector, callback)` | Subscribe to state changes with a selector function.  |
| `destroyCheckout()`                       | Remove the iframe and clean up all listeners.         |

**`getCheckout()` methods:**

| Method                                       | Description                                              |
| -------------------------------------------- | -------------------------------------------------------- |
| `openCart()`                                 | Open the cart drawer                                     |
| `openCheckout()`                             | Open checkout directly (skips cart)                      |
| `close()`                                    | Close the overlay                                        |
| `addToCart(productId, variantId, quantity?)` | Add an item to the cart                                  |
| `updateTokens(accessToken, refreshToken?)`   | Sync auth tokens from your app                           |
| `getCart()`                                  | Returns `{ count, total, currency }`                     |
| `getTokens()`                                | Returns latest `{ accessToken, refreshToken }` or `null` |
| `destroy()`                                  | Remove the iframe and clean up                           |

**`subscribeToCheckout` state properties:**

| Property       | Type               | Description                               |
| -------------- | ------------------ | ----------------------------------------- |
| `isReady`      | `boolean`          | `true` when the checkout iframe is loaded |
| `isOpen`       | `boolean`          | `true` when the overlay is visible        |
| `cartCount`    | `number`           | Number of items in the cart               |
| `cartTotal`    | `number`           | Cart subtotal                             |
| `cartCurrency` | `string`           | Currency code                             |
| `isLoggedIn`   | `boolean`          | Whether a user is logged in               |
| `user`         | `UserInfo \| null` | Current user info                         |
