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

# Installing on Hydrogen

> Install the Order Protection cart widget on a Hydrogen (headless) Shopify storefront using the API widget and your Storefront-API cart.

Hydrogen storefronts are custom React applications built on Remix and deployed to Oxygen. A Hydrogen store has **no Liquid templates** — your storefront is your own code, and the cart is a React component backed by the **Storefront API**. Order Protection still works on the cart page, but the integration is different from a Liquid theme: instead of the self-driving cart widget (which manages the Shopify AJAX cart for you), Hydrogen uses the **API widget**, which renders the same UI and pricing but leaves the cart mutation to your app.

The result is the same as on any other store: the shopper toggles protection, the Order Protection line is added to the cart, and it carries through to checkout.

<Note>
  **Two surfaces, and you can use either or both.** The simplest supported option
  is the **checkout** UI extension (see [the last section](#order-protection-in-checkout)),
  which needs no Hydrogen-specific code. This guide covers the **cart-page**
  widget, which is a real integration in your Hydrogen app.
</Note>

<Warning>
  This is a **reference implementation.** It is built on Order Protection's
  documented headless API widget, but the exact wiring depends on your Hydrogen
  version and cart setup, and it has not been certified against every skeleton.
  Build it against a preview deployment and test the full **add → toggle →
  checkout** flow before shipping to Oxygen. Your Order Protection onboarding
  contact (`onboarding@orderprotection.com`) can confirm the store-specific
  values called out below.
</Warning>

## How it works

* Order Protection ships a per-brand widget bundle at `https://cdn.orderprotection.com/cart-widget/<brand-slug>/widget.js`. For a Hydrogen store, that URL serves the **API widget** build (`window.OrderProtection.init` / `setCartTotal` / `on` / `setState`) rather than the Liquid cart widget. This is an Order Protection deploy-side setting — your onboarding contact enables it for your brand.
* You load that script, render `<order-protection>` in your cart, tell the widget your cart subtotal, and listen for the toggle.
* When protection is toggled on, **your app** adds the Order Protection variant to the Storefront-API cart (`cartLinesAdd`); when it's off, you remove it (`cartLinesRemove`). Because it's a normal cart line, it flows to `cart.checkoutUrl` automatically.

<Tip>
  **Design the widget first.** Configure your cart widget's appearance, copy, and pricing display in the Order Protection app, then grab its config from **Get Code** — see [Widget Customization](/education-basecamp/customizations/widgets). The Hydrogen bundle renders whatever you've published there.
</Tip>

## Prerequisites

<ul>
  <li>A Hydrogen storefront running locally (`shopify hydrogen dev`).</li>
  <li>Your store onboarded with Order Protection, and the **API widget build** deployed to your brand's <code>cart-widget/\<brand-slug>/widget.js</code> path (ask your onboarding contact to confirm, or verify it yourself with the check below).</li>
  <li>Your <strong>brand slug</strong> (the CDN path segment, e.g. <code>cool-store-co</code> — the slugified brand name, <strong>not</strong> your <code>myshopify.com</code> domain) and your <strong>Order Protection store identifier</strong> passed to <code>init()</code> (typically your <code>.myshopify.com</code> domain — confirm with Order Protection). You can read the brand slug straight from your app URL — it's the path segment after <code>app.orderprotection.com/</code>, e.g. <code>app.orderprotection.com/order-protection-v2/settings/general</code> means your slug is <code>order-protection-v2</code>.</li>
  <li>The Order Protection product <strong>published to your Hydrogen sales channel</strong> so its variant is purchasable through the Storefront API. Order Protection can confirm the product is set up; publishing it to the storefront is done in Shopify admin (see the first step below).</li>
</ul>

<Warning>
  Confirm the bundle resolves before wiring anything up — a published widget
  config is **not** the same as a deployed bundle:

  ```bash theme={null}
  curl -s -o /dev/null -w "%{http_code}\n" -I \
    "https://cdn.orderprotection.com/cart-widget/<brand-slug>/widget.js"
  ```

  `200` means it's live; `404` means the bundle still needs to be deployed —
  contact `onboarding@orderprotection.com`. No terminal handy? Open the same URL
  in your browser — a page of JavaScript means it's live; a **Not Found** page
  means it isn't deployed yet.
</Warning>

## Installation

<Steps>
  <Step title="Confirm the Order Protection product is Unlisted">
    The Order Protection variant is added through the Storefront API, so it must be purchasable there — but you don't want shoppers finding it and buying it on its own. Shopify's [**Unlisted** product status](https://shopify.dev/docs/apps/build/product-merchandising/unlisted-products) is built for exactly this (warranty-style products): unlisted products behave like active ones in the cart, checkout, and orders, but are hidden from storefront search, collections, recommendations, and your sitemap — they stay reachable only by direct link and the Storefront API.

    Order Protection creates the product and sets it to **Unlisted** for you; your job is just to confirm it. In your Shopify admin:

    1. Go to `Products` and open the **Order Protection** product (vendor: `Order Protection`).
    2. Its status should read **Unlisted** — not **Active** or **Draft**. If it doesn't, contact `onboarding@orderprotection.com`.
    3. Make sure it's available to the storefront/sales channel your Hydrogen app queries, so the Storefront API can add it. If `cartLinesAdd` rejects the variant, it isn't available to your headless channel yet — confirm with Order Protection.
  </Step>

  <Step title="Load the widget script in your root layout">
    Add the script to your document in `app/root.tsx`, inside `<body>` before Remix's `<Scripts />`:

    ```tsx app/root.tsx theme={null}
    {/* inside the document <body>, before <Scripts /> */}
    <script src="https://cdn.orderprotection.com/cart-widget/your-brand-slug/widget.js" />
    ```

    The bundle registers the `<order-protection>` custom element and attaches `window.OrderProtection` (including `on`/`off`, which are available before `init()`).
  </Step>

  <Step title="Declare the custom element and the API for TypeScript">
    `<order-protection>` isn't a known JSX element, and `window.OrderProtection` isn't typed. Add a declaration (e.g. `app/order-protection.d.ts`):

    ```tsx app/order-protection.d.ts theme={null}
    import type React from 'react';

    declare global {
      namespace JSX {
        interface IntrinsicElements {
          'order-protection': React.DetailedHTMLProps<
            React.HTMLAttributes<HTMLElement>,
            HTMLElement
          >;
        }
      }

      interface Window {
        OrderProtection?: {
          init?: (o: {storeUrl: string; country?: string; currency?: string; checked?: boolean}) => Promise<void>;
          setCartTotal?: (cents: number) => void;
          setState?: (checked: boolean) => void;
          on?: (event: string, cb: (data: any) => void) => void;
          off?: (event: string, cb: (data: any) => void) => void;
        };
      }
    }
    ```

    <Note>
      Depending on your React version the JSX augmentation target differs — React 19
      uses `declare module 'react' { namespace JSX { ... } }` instead of the global
      `JSX` namespace. Match whichever your project uses.
    </Note>
  </Step>

  <Step title="Add a hook that wires the widget to your Storefront-API cart">
    This hook does the four jobs: wait for the script, `init` + feed the cart subtotal, capture the current Order Protection variant, and reconcile the widget's toggle against the actual cart line. Create `app/lib/useOrderProtection.ts`:

    ```tsx app/lib/useOrderProtection.ts theme={null}
    import {useEffect, useRef, useState} from 'react';
    import {useFetcher} from '@remix-run/react';
    import {CartForm} from '@shopify/hydrogen';

    const OP_VENDOR = 'Order Protection';
    const OP_HANDLE = 'order-protection';

    // Confirm these with your Order Protection onboarding contact.
    const OP_STORE_ID = 'your-store.myshopify.com';

    const isOpLine = (line: any) => {
      const p = line?.merchandise?.product;
      return p?.vendor === OP_VENDOR || p?.handle === OP_HANDLE;
    };

    export function useOrderProtection(cart: any | null) {
      const fetcher = useFetcher();
      const [ready, setReady] = useState(false);
      const [wantsProtection, setWantsProtection] = useState<boolean | null>(null);
      const inited = useRef(false);
      const expectedVariantId = useRef<number | null>(null);

      const lines = cart?.lines?.nodes ?? [];
      const opLine = lines.find(isOpLine);
      const currency = cart?.cost?.subtotalAmount?.currencyCode ?? 'USD';
      // Subtotal EXCLUDING Order Protection, in cents — feeding the OP line back in
      // would inflate its own price, so always exclude it.
      const subtotalCents = Math.round(
        lines
          .filter((l: any) => !isOpLine(l))
          .reduce((sum: number, l: any) => sum + Number(l.cost?.totalAmount?.amount ?? 0) * 100, 0),
      );

      // 1. Wait for the async script to attach window.OrderProtection.
      useEffect(() => {
        if (window.OrderProtection?.init) return setReady(true);
        const id = setInterval(() => {
          if (window.OrderProtection?.init) {
            setReady(true);
            clearInterval(id);
          }
        }, 50);
        return () => clearInterval(id);
      }, []);

      // 2. init once, then feed the cart subtotal on every change.
      useEffect(() => {
        const op = window.OrderProtection;
        if (!ready || !op?.init) return;
        const onCartUpdated = (d: any) => (expectedVariantId.current = d?.expectedVariant?.id ?? null);
        op.on?.('cart-updated', onCartUpdated);

        if (!inited.current) {
          inited.current = true;
          op.init({storeUrl: OP_STORE_ID, country: 'US', currency})
            .then(() => op.setCartTotal?.(subtotalCents))
            .catch(() => {});
        } else {
          op.setCartTotal?.(subtotalCents);
        }
        return () => op.off?.('cart-updated', onCartUpdated);
      }, [ready, subtotalCents, currency]);

      // 3. Track what the widget wants (user toggle + the store's default state).
      useEffect(() => {
        const op = window.OrderProtection;
        if (!ready || !op?.on) return;
        const onToggle = (d: any) => setWantsProtection(!!d.checked);
        const onPref = (d: any) => setWantsProtection(!!d.preference);
        op.on('toggle', onToggle);
        op.on('user-preference-loaded', onPref);
        op.on('user-preference-updated', onPref);
        return () => {
          op.off?.('toggle', onToggle);
          op.off?.('user-preference-loaded', onPref);
          op.off?.('user-preference-updated', onPref);
        };
      }, [ready]);

      // 4. Reconcile desired state against the real cart line.
      useEffect(() => {
        if (wantsProtection === null || fetcher.state !== 'idle') return;

        if (wantsProtection && !opLine && expectedVariantId.current) {
          fetcher.submit(
            {
              [CartForm.INPUT_NAME]: JSON.stringify({
                action: CartForm.ACTIONS.LinesAdd,
                inputs: {lines: [{merchandiseId: `gid://shopify/ProductVariant/${expectedVariantId.current}`, quantity: 1}]},
              }),
            },
            {method: 'POST', action: '/cart'},
          );
        }

        if (!wantsProtection && opLine) {
          fetcher.submit(
            {
              [CartForm.INPUT_NAME]: JSON.stringify({
                action: CartForm.ACTIONS.LinesRemove,
                inputs: {lineIds: [opLine.id]},
              }),
            },
            {method: 'POST', action: '/cart'},
          );
        }
      }, [wantsProtection, opLine, fetcher]);
    }
    ```
  </Step>

  <Step title="Render the widget in your cart summary">
    `<order-protection>` must **wrap** your subtotal and checkout button as direct light-DOM children (it hides/adjusts them based on toggle state). In the Hydrogen skeleton this is `app/components/CartSummary.tsx`:

    ```tsx app/components/CartSummary.tsx theme={null}
    import {useOrderProtection} from '~/lib/useOrderProtection';

    export function CartSummary({cart, layout}: CartSummaryProps) {
      useOrderProtection(cart);

      return (
        <div aria-labelledby="cart-summary" className={`cart-summary-${layout}`}>
          <h4>Totals</h4>
          <order-protection>
            <dl className="cart-subtotal">
              <dt>Subtotal</dt>
              <dd>{cart.cost?.subtotalAmount ? <Money data={cart.cost.subtotalAmount} /> : '-'}</dd>
            </dl>
            <a href={cart.checkoutUrl} target="_self">
              <p>Continue to Checkout &rarr;</p>
            </a>
          </order-protection>
        </div>
      );
    }
    ```

    <Note>
      The widget's own checkout-button interception is built for the Liquid `/checkout`
      flow, not Hydrogen's `checkoutUrl` anchor. This integration adds Order Protection
      on **toggle** (the hook above), so you do **not** need the widget to intercept
      your checkout button — leave your normal `checkoutUrl` navigation as-is.
    </Note>
  </Step>

  <Step title="Hide the Order Protection line from the cart display">
    Order Protection appears as a cart line with the vendor `Order Protection`. Filter it out of your **rendered** line items (never remove it from the cart — it must reach checkout). First request `vendor` in your cart query (e.g. `app/lib/fragments.ts`):

    ```graphql theme={null}
    merchandise {
      ... on ProductVariant {
        product { handle title vendor }
      }
    }
    ```

    Then filter where you render lines (e.g. `app/components/CartMain.tsx`), and base your "cart is empty" check on the filtered list so an Order-Protection-only cart doesn't render empty:

    ```tsx app/components/CartMain.tsx theme={null}
    const lines = cart?.lines?.nodes.filter(
      (line) =>
        line.merchandise.product.vendor !== 'Order Protection' &&
        line.merchandise.product.handle !== 'order-protection',
    );
    ```
  </Step>
</Steps>

## Order Protection in checkout

Your checkout is hosted by Shopify even on Hydrogen (you hand off via `cart.checkoutUrl`), so Order Protection's `checkout-widget` UI extension installs **identically to any other Shopify store** — no Hydrogen-specific work. Use it on its own, or alongside the cart widget above. See **[Checkout Extension](/developer/checkout-extension)**.

## Before you go live

* **Test the full flow** on a preview deployment: toggle on → Order Protection line is added → it's hidden from the cart list but present in the cart → it appears in checkout with the right price → toggle off removes it.

## Follow-ups

The points most likely to need Order Protection's involvement as you implement:

1. **API build deployment.** Order Protection deploys the API widget build to your brand's `cart-widget/<brand-slug>` path — it's a deploy-side setting on their end. Confirm it's live for your brand (see the bundle check under [Prerequisites](#prerequisites)).
2. **Product published to the Storefront channel.** The Order Protection product must be published to your Hydrogen Storefront sales channel, or `cartLinesAdd` will reject the variant.
3. **`storeUrl` vs. brand slug.** These are two different values — the identifier passed to `init()` (likely your `.myshopify.com` domain) and the brand slug in the CDN path. Confirm both.
4. **Multi-currency / Markets.** If you sell across markets, pass your presentment currency to `init()` and feed `setCartTotal()` amounts in that currency — the widget applies the exchange rate.

## Support

Questions or issues during setup? Contact [onboarding@orderprotection.com](mailto:onboarding@orderprotection.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.