npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

captain-shipping-protection-react

v0.1.1

Published

React components and hooks for adding Captain Shipping Protection to a Shopify headless storefront.

Downloads

316

Readme

Captain Shipping Protection React

React components and hooks for adding Captain Shipping Protection to a Shopify headless storefront.

The package is designed for cart pages built with Hydrogen, Next.js, Remix, or a client-side React application. It calculates the correct protection variant for the current cart, renders the merchant-configured widget, and exposes the state needed to add protection immediately before checkout.

What this package does

  • Loads the Captain Shipping Protection configuration for a Shopify store.
  • Calculates the current protection price and Shopify variant.
  • Applies the merchant's text, icon, colors, layout, and default opt-in setting.
  • Hides the widget when the cart is empty or excluded.
  • Detects and helps remove a protection line that is already in the cart.
  • Keeps the shopper's opt-in intent without mutating the cart on every toggle.
  • Recalculates the latest protection variant immediately before checkout.
  • Includes TypeScript declarations.

Your storefront remains responsible for reading and updating the Shopify cart. This package does not call Shopify's Storefront Cart API on your behalf.

Requirements

  • React 18 or later.
  • A Captain Shipping Protection configuration for the target Shopify store.
  • A browser with fetch and Web Crypto support.
  • HTTPS in production. Web Crypto is also available on localhost during local development.
  • Cart prices passed to the SDK must be integer minor units. For USD, 1299 means $12.99.

The package is ESM-only.

Installation

Install both the React package and its SDK peer dependency:

npm install captain-shipping-protection-react captain-shipping-protection-sdk

With pnpm:

pnpm add captain-shipping-protection-react captain-shipping-protection-sdk

With Yarn:

yarn add captain-shipping-protection-react captain-shipping-protection-sdk

Recommended integration

Use ShippingProtectionWidget for the UI and useShippingProtectionController for state and SDK communication.

The intended cart flow is:

  1. Convert the current Shopify cart to SdkCartData.
  2. Render the widget with useShippingProtectionController.
  3. Let the widget toggle update the shopper's intent only.
  4. If an old protection line is detected, remove it through the removeProtection callback and refresh the host cart.
  5. When the shopper clicks checkout, call prepareCheckout().
  6. If it returns protection info, add that variant to Shopify.
  7. Redirect immediately to the checkout URL returned by the updated cart.

Why protection is added at checkout

The protection variant depends on the latest eligible cart total. Adding it only at checkout avoids repeatedly adding, removing, or replacing a Shopify cart line while the shopper edits quantities or products.

The widget toggle therefore does not add or remove a cart line. It records whether the shopper wants protection.

Step 1: Convert your Shopify cart

The controller accepts SdkCartData. Shopify's Storefront API normally returns GraphQL GIDs and decimal money strings, so convert IDs to numbers and prices to minor units.

import type {SdkCartData} from "captain-shipping-protection-react";

type StorefrontCart = {
  id: string;
  totalQuantity: number;
  cost: {
    subtotalAmount: {
      amount: string;
      currencyCode: string;
    };
  };
  lines: {
    nodes: Array<{
      id: string;
      quantity: number;
      cost: {
        totalAmount: {
          amount: string;
        };
      };
      merchandise: {
        id: string;
        sku?: string | null;
        title: string;
        product: {
          id: string;
          title: string;
        };
      };
    }>;
  };
};

function numericShopifyId(gid: string): number {
  const value = Number(gid.split("/").pop());

  if (!Number.isFinite(value)) {
    throw new Error(`Invalid Shopify GID: ${gid}`);
  }

  return value;
}

function toMinorUnits(amount: string): number {
  return Math.round(Number(amount) * 100);
}

export function toSdkCartData(cart: StorefrontCart): SdkCartData {
  return {
    token: cart.id,
    currency: cart.cost.subtotalAmount.currencyCode,
    total_price: toMinorUnits(cart.cost.subtotalAmount.amount),
    item_count: cart.totalQuantity,
    items: cart.lines.nodes.map((line) => ({
      id: numericShopifyId(line.merchandise.id),
      key: line.id,
      quantity: line.quantity,
      variant_id: numericShopifyId(line.merchandise.id),
      product_id: numericShopifyId(line.merchandise.product.id),
      final_line_price: toMinorUnits(line.cost.totalAmount.amount),
      title: line.merchandise.title,
      product_title: line.merchandise.product.title,
      sku: line.merchandise.sku ?? "",
    })),
  };
}

Use the cart subtotal before shipping and tax. Include discounts in final_line_price and total_price when those discounts affect the amount the shopper will pay.

Create a new SdkCartData object whenever the host cart changes. The controller uses cart changes to recalculate the current protection information.

Step 2: Render the widget

The following example keeps Shopify mutations behind host-provided adapter functions. Replace those adapters with your Storefront API, Hydrogen cart handler, or commerce backend implementation.

import {
  ShippingProtectionWidget,
  useShippingProtectionController,
  type SdkCartData,
} from "captain-shipping-protection-react";

interface CartProtectionProps {
  cart: SdkCartData;
  currency: string;
  findProtectionLineIds: (productId: string) => string[];
  removeCartLines: (lineIds: string[]) => Promise<void>;
  refreshCart: () => Promise<void>;
  addProtectionAndGetCheckoutUrl: (
    variantGid: string,
  ) => Promise<string>;
  getCheckoutUrl: () => Promise<string>;
}

function toVariantGid(variantId: string): string {
  return variantId.startsWith("gid://shopify/ProductVariant/")
    ? variantId
    : `gid://shopify/ProductVariant/${variantId}`;
}

export function CartProtection({
  cart,
  currency,
  findProtectionLineIds,
  removeCartLines,
  refreshCart,
  addProtectionAndGetCheckoutUrl,
  getCheckoutUrl,
}: CartProtectionProps) {
  const protection = useShippingProtectionController({
    shop: "example.myshopify.com",
    country: "US",
    locale: "en",
    currency,
    cart,

    // Called when the cart already contains Captain's protection product.
    removeProtection: async (info) => {
      const lineIds = findProtectionLineIds(info.productId);

      if (lineIds.length > 0) {
        await removeCartLines(lineIds);
        await refreshCart();
      }
    },

    // Optional analytics or application-state notification.
    onToggle: async (checked, info) => {
      console.log("Protection intent changed", {
        checked,
        variantId: info.variantsId,
      });
    },

    // Optional notification when price, variant, or eligibility changes.
    onInfoChange: (info) => {
      console.log("Latest protection info", info);
    },
  });

  async function handleCheckout() {
    const latestInfo = await protection.prepareCheckout();

    const checkoutUrl = latestInfo
      ? await addProtectionAndGetCheckoutUrl(
          toVariantGid(latestInfo.variantsId),
        )
      : await getCheckoutUrl();

    window.location.assign(checkoutUrl);
  }

  return (
    <>
      {protection.visible && protection.info ? (
        <ShippingProtectionWidget
          checked={protection.checked}
          currency={currency}
          disabled={
            protection.togglePending ||
            protection.removePending
          }
          info={protection.info}
          onToggle={protection.toggle}
          setting={protection.setting}
        />
      ) : null}

      {protection.error ? (
        <p role="alert">
          Shipping Protection is temporarily unavailable.
        </p>
      ) : null}

      {protection.removeError ? (
        <div role="alert">
          <p>Could not remove the existing protection line.</p>
          <button type="button" onClick={() => void protection.refresh()}>
            Retry
          </button>
        </div>
      ) : null}

      <button
        type="button"
        disabled={
          protection.initPending ||
          protection.infoPending ||
          protection.removePending
        }
        onClick={() => void handleCheckout()}
      >
        Checkout
      </button>
    </>
  );
}

Important checkout behavior

After adding the variant returned by prepareCheckout(), redirect to checkout using the URL returned by that same cart mutation.

Do not intentionally publish the newly added protection line back to a cart page and remain there. The controller removes existing protection lines from cart pages so that the next checkout attempt can recalculate the correct variant.

If prepareCheckout() returns null, continue checkout without adding a protection line. This is expected when:

  • The shopper turned protection off.
  • The cart is empty.
  • The cart contains only the protection product.
  • The cart or shopper is excluded by the merchant's rules.
  • An existing protection line could not be safely cleaned up.

Step 3: Implement Shopify cart adapters

Your host application must provide three operations:

  • Remove existing protection: Find lines whose Shopify product ID matches info.productId, remove those line IDs, and refresh the cart passed to the controller.
  • Add protection for checkout: Add one unit of info.variantsId and obtain the resulting cart's checkout URL.
  • Continue without protection: Obtain the current cart's checkout URL without adding a line.

When using Shopify Storefront GraphQL:

  • Use cartLinesRemove for removeProtection.
  • Use cartLinesAdd for the result of prepareCheckout().
  • Request cart.checkoutUrl from the mutation response.
  • Use a quantity of 1 for the protection variant.
  • Match existing protection by product ID, not only by variant ID. The correct variant may change when the cart total changes.

Example add mutation:

mutation AddShippingProtection(
  $cartId: ID!
  $lines: [CartLineInput!]!
) {
  cartLinesAdd(cartId: $cartId, lines: $lines) {
    cart {
      id
      checkoutUrl
    }
    userErrors {
      field
      message
    }
  }
}

Example variables:

{
  "cartId": "gid://shopify/Cart/your-cart-id",
  "lines": [
    {
      "merchandiseId": "gid://shopify/ProductVariant/123456789",
      "quantity": 1
    }
  ]
}

Always inspect Shopify userErrors before redirecting.

SSR frameworks

The controller starts SDK requests from React effects, so it does not fetch Captain settings during the server render. The initial server output contains no widget, and the widget appears after the client initializes the controller.

Next.js App Router

Place the integration in a Client Component:

"use client";

import {
  ShippingProtectionWidget,
  useShippingProtectionController,
} from "captain-shipping-protection-react";

export function ClientCartProtection(props: CartProtectionProps) {
  // Use the same controller and widget flow shown above.
}

Pass serializable cart data from the Server Component to the Client Component. Do not call prepareCheckout() from a Server Component.

Hydrogen, Remix, and React Router

The package can be imported normally in a component that hydrates in the browser. Build SdkCartData from loader data, then perform Storefront cart mutations through your action, fetcher, or cart handler.

No dynamic import is required by the package itself.

Client-side React applications

Vite, Create React App, and other browser-rendered React applications can import the package directly:

import {
  ShippingProtectionWidget,
  useShippingProtectionController,
} from "captain-shipping-protection-react";

Content Security Policy

The SDK sends configuration and eligibility requests to:

https://insurance.captaintop.com

If your storefront has a Content Security Policy, add that origin to connect-src:

connect-src 'self' https://insurance.captaintop.com;

The merchant may configure an external protection icon. Add the actual icon origin to img-src if your CSP restricts images.

The React package does not require an external script, iframe, or font origin. Do not add broad CSP allowances such as * or 'unsafe-inline' solely for this package.

API reference

useShippingProtectionController(params)

This is the recommended high-level integration hook.

Required parameters:

  • shop: Shopify domain, for example example.myshopify.com.
  • country: shopper country code used for eligibility, for example US.
  • locale: storefront locale, for example en.
  • currency: active ISO currency code, for example USD.
  • cart: current cart in SdkCartData format.

Optional callbacks:

  • removeProtection(info): removes existing protection lines from the host cart. The controller calls it when includedProtection is true.
  • onToggle(checked, info): runs after the shopper changes the widget intent. It may be asynchronous. If it rejects, the controller restores the previous checked state and leaves the stored preference unchanged.
  • onInfoChange(info): runs when the resolved product, variant, price, inclusion state, or exclusion state changes.

Checked-state persistence:

  • The controller stores the shopper's last successful choice in localStorage, using a key isolated by shop.
  • A cart that already contains protection takes priority, is treated as checked, and stores that checked preference before the old line is removed.
  • For an eligible cart without protection, the stored preference takes priority over tm_default_display_status.
  • Excluded or empty carts hide and temporarily uncheck the widget without deleting the stored preference. The preference is restored when the cart becomes eligible again.
  • If browser storage is unavailable, the controller safely falls back to tm_default_display_status.

Returned state:

  • checked: the shopper's current protection intent.
  • visible: whether the standard widget should be rendered.
  • info: latest ShippingProtectionInfo, or null before resolution.
  • setting: merchant widget configuration, or null before initialization.
  • excludedReason: reason the cart is not eligible.
  • error: SDK initialization or information request error.
  • initPending: configuration initialization is running.
  • infoPending: cart information is being resolved.
  • togglePending: an asynchronous onToggle callback is running.
  • removePending: removeProtection is running.
  • removeError: latest removeProtection error.

Returned actions:

  • toggle(checked, info?): updates the shopper's intent. Pass this directly to ShippingProtectionWidget.
  • refresh(): resolves protection information again for the current cart. Use it to retry after a cleanup error.
  • prepareCheckout(): resolves the latest cart price and variant. It returns ShippingProtectionInfo when a protection line should be added, otherwise null.

ShippingProtectionWidget

Pure presentation component. It does not initialize the SDK and does not mutate the Shopify cart.

Props:

  • checked: current opt-in state.
  • currency: ISO currency code used to format the displayed price.
  • info: latest ShippingProtectionInfo.
  • setting: merchant configuration returned by the controller.
  • onToggle(checked, info): called when the shopper uses the switch or checkbox.
  • disabled: disables interaction while the host is busy.
  • className: optional class added to the widget root.

The component injects scoped styles using the csp- class namespace and applies a local element reset to reduce interference from storefront CSS.

ShippingProtection

Convenience component that combines the controller and standard widget:

import {ShippingProtection} from "captain-shipping-protection-react";

<ShippingProtection
  shop="example.myshopify.com"
  country="US"
  locale="en"
  currency="USD"
  cart={sdkCart}
  removeProtection={removeProtectionLine}
  onInfoChange={(info) => console.log(info)}
  onToggle={(checked) => console.log(checked)}
/>;

Use this component when you only need the standard UI and callbacks. For a complete headless checkout integration, prefer useShippingProtectionController plus ShippingProtectionWidget, because the host must call prepareCheckout() immediately before checkout.

useShippingProtection

Low-level hook that exposes init() and getInfo(cart). Most storefronts should use useShippingProtectionController instead, because the controller manages initialization, checked intent, pending state, stale requests, cleanup, and checkout preparation.

Data types

ShippingProtectionInfo

interface ShippingProtectionInfo {
  productId: string;
  variantsId: string;
  price: string;
  includedProtection: boolean;
  isExcluded: boolean;
  excludedReason?: string;
}
  • productId: Captain protection Shopify product ID.
  • variantsId: protection variant selected for the current eligible total.
  • price: protection price in the currency's major unit.
  • includedProtection: whether the input cart already contains the Captain protection product.
  • isExcluded: whether protection must not be offered for this cart.
  • excludedReason: machine-readable exclusion reason.

Common exclusion reasons

  • empty_cart: the cart contains no lines.
  • invalid_cart: at least one line does not have a valid product ID.
  • only_shipping_protection: no non-protection product remains.
  • excluded_variant: the cart contains a merchant-configured excluded variant.
  • check_display_hidden: Captain's eligibility response hides the widget.

Treat exclusion reason values as extensible. Do not assume the list above is exhaustive.

Error handling

Recommended behavior:

  • Hide or replace the widget when error is present.
  • Disable checkout while removePending is true.
  • Show a retry action when removeError is present.
  • Let checkout continue without protection when the SDK is unavailable, unless your business requirements explicitly say otherwise.
  • Log Shopify userErrors separately from SDK errors.

onInfoChange consumer errors are isolated from SDK state. An onToggle rejection rolls the checked state back.

Styling

Merchant-controlled styles and copy come from Captain's configuration. Use the widget's className for host-page layout such as width or margin:

<ShippingProtectionWidget
  {...widgetProps}
  className="cart-shipping-protection"
/>
.cart-shipping-protection {
  margin-block: 16px;
  width: 100%;
}

Avoid targeting internal csp- classes unless you intentionally want to override merchant configuration.

Integration checklist

Before deploying:

  • Install both npm packages and confirm React 18 or later.
  • Confirm shop uses the example.myshopify.com format.
  • Pass the active shopper country, locale, and currency.
  • Convert Shopify GIDs to numeric IDs in SdkCartData.
  • Pass all prices as integer minor units.
  • Update the controller cart whenever products, quantities, or discounts change.
  • Implement removeProtection and refresh the host cart after removal.
  • Disable checkout while protection cleanup is pending.
  • Call prepareCheckout() immediately before adding protection.
  • Add exactly one unit of the returned variant.
  • Redirect using the checkout URL from the updated cart.
  • Add https://insurance.captaintop.com to CSP connect-src when CSP is enabled.
  • Test checked, unchecked, excluded, empty-cart, cleanup-failure, and API-failure flows.

Troubleshooting

The widget does not appear

Check error, info, visible, and excludedReason. The widget is expected to stay hidden for an empty or excluded cart. Also confirm that the store has an active Captain Shipping Protection configuration.

Toggling the widget does not add a Shopify cart line

This is intentional. The toggle stores shopper intent only. Add the variant returned by prepareCheckout() when checkout begins.

The protection line is removed after being added

The controller removes protection found on a cart page so it can calculate a fresh variant. Add protection and redirect immediately to the checkout URL. Avoid updating the visible cart page with the added line before redirecting.

The displayed price does not update

Pass a new SdkCartData object after every host cart change. Verify that total_price and final_line_price use minor units.

Cleanup repeats or checkout creates duplicate protection

After removeProtection succeeds, refresh the host cart and pass the updated cart without the protection product back to the controller. Match lines by info.productId, not only by variant ID.

Requests are blocked by CSP

Allow https://insurance.captaintop.com in connect-src. If the configured icon is blocked, allow its exact origin in img-src.

Updating

Check the installed version:

npm list captain-shipping-protection-react

Update both packages together:

npm install \
  captain-shipping-protection-react@latest \
  captain-shipping-protection-sdk@latest