Integrate into Shopify Hydrogen Storefronts

This guide will walk you through Purple Dot's Platform integration into any Shopify Hydrogen Storefront. If you are using our Shopify App, please seeInstalling our Shopify App instead. See Purple Dot Overview for the comparison of our Shopify App vs. Platform integrations.

If you want a deeper understanding of how the Purple Dot Platform integration works, you can read Integrate into Shopify Headless Storefronts.

Install the Purple Dot browser package

npm install @purple-dot/browser

Setup vite config

If you're using vite in your hydrogen storefront, update the vite.config.ts file with the following:

export default defineConfig({
  plugins: [
    // ...
  ],
  build: {
    // ...
  },
  ssr: {
    optimizeDeps: {
      include: [
        '@purple-dot/browser/*'],
      ],
    },
  },
});

Add config variables

In your .env file and in remix.env.d.ts, add PURPLE_DOT_API_KEY and set it to the public API key you can find in the integration page of your Purple Dot merchant portal. You must also have PUBLIC_STOREFRONT_API_TOKEN and PUBLIC_STORE_DOMAIN set.

Initialise the package

Before you can use the package, it needs to be initialised with your store's apiKey and the Storefront cart adapter. This needs to be initialised on each loader that requires the use of the package so that you can use the package in your backend handlers as well as React components:

// Server-side
import * as PurpleDot from '@purple-dot/browser';
import { ShopifyStorefrontCart } from '@purple-dot/browser/shopify-storefront-cart';

export async function loader({context}: LoaderArgs) {
  const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;
  const publicStorefrontApiToken = context.env.PUBLIC_STOREFRONT_API_TOKEN;
  const purpleDotApiKey = context.env.PURPLE_DOT_API_KEY;

  PurpleDot.init({
    apiKey: purpleDotApiKey,
    cartAdapter: new ShopifyStorefrontCart(
      publicStoreDomain,
      publicStorefrontApiToken,
    ),
  });

  // Use Purple Dot SDK...

  // Return data to page
  return defer(
    {
      // ...
      publicStoreDomain,
      publicStorefrontApiToken,
      purpleDotApiKey,
    },
    {headers},
  );
}

// Client-side

import * as PurpleDot from '@purple-dot/browser';
import { ShopifyStorefrontCart } from '@purple-dot/browser/shopify-storefront-cart';

export default function App() {
  const data = useLoaderData<typeof loader>();

  useEffect(() => {
    PurpleDot.init({
      apiKey: data.purpleDotApiKey,
      cartAdapter: new ShopifyStorefrontCart(
        data.publicStoreDomain,
        data.publicStorefrontApiToken,
      ),
    });
  }, []);

  return (
    <html lang="en">...</html>
  );
}

Set up product pages

On every product form, two elements are expected if a variant/product is on preorder:

  1. The waitlist shipping dates so that shoppers know when to expect their order to start shipping

  2. A Purple Dot learn more element so that shoppers can understand their preorder process with Purple Dot

A non-preorder product:

A pre-order product:

In the following example, we are going to fetch the preorder data in the loader:

// routes/products.$handle.tsx
import * as api from '@purple-dot/browser/api';

function idFromGuid(guid: string): number {
    return parseInt(guid.split('/')[4], 10);
}

export async function loader({params, request, context}: LoaderArgs) {
  const {handle} = params;
  const {storefront} = context;

  const product = await getProduct(handle, params);

  const productRes = await api.fetchProductsPreorderState(handle);
  const variantRes = await api.fetchVariantsPreorderState(idFromGuid(product.selectedVariant.id));

  return defer({
    product: {
      ...product,
      isPreorder: productRes?.state === 'ON_PREORDER',
      selectedVariant: {
        ...product.selectedVariant,
        isPreorder: variantRes?.state === 'ON_PREORDER',
        releaseId: variantRes?.waitlist?.id,
        estimatedShipDates: variantRes?.waitlist?.display_dispatch_date,
      },
    },
    variants,
  });
}

Then display the preorder information on the product form if the variant is on pre-order:

// routes/products.$handle.tsx
export default function Product() {
  const {product, variants} = useLoaderData<typeof loader>();
  const {selectedVariant} = product;

  return (
    <div className="product">
      <ProductImage image={selectedVariant?.image} />
      <ProductMain
        selectedVariant={selectedVariant}
        product={product}
        variants={variants}
      />
    </div>
  );
}

function ProductMain({
  selectedVariant,
  product,
  variants,
}: {
  product: ProductFragment;
  selectedVariant: ProductFragment['selectedVariant'];
  variants: Promise<ProductVariantsQuery>;
}) {
  const {title, descriptionHtml} = product;
  return (
    <div className="product-main">
      <h1>{title}</h1>
      <ProductPrice selectedVariant={selectedVariant} />
      <br />
      <Suspense
        fallback={
          <ProductForm
            product={product}
            selectedVariant={selectedVariant}
            variants={[]}
          />
        }
      >
        <Await
          errorElement="There was a problem loading product variants"
          resolve={variants}
        >
          {(data) => (
            <ProductForm
              product={product}
              selectedVariant={selectedVariant}
              variants={data.product?.variants.nodes || []}
            />
          )}
        </Await>
      </Suspense>
      <br />
      <br />
      <p>
        <strong>Description</strong>
      </p>
      <br />
      <div dangerouslySetInnerHTML={{__html: descriptionHtml}} />
      <br />
    </div>
  );
}

function ProductForm({
  product,
  selectedVariant,
  variants,
}: {
  product: ProductFragment;
  selectedVariant: ProductFragment['selectedVariant'] & { isPreorder: boolean, releaseId: string, estimatedShipDates: string };
  variants: Array<ProductVariantFragment>;
}) {
  const buttonLabel = selectedVariant?.availableForSale
    ? selectedVariant?.isPreorder ? 'Pre-order' : 'Add to cart'
    : 'Sold out';
  return (
    <div className="product-form">
      <VariantSelector
        handle={product.handle}
        options={product.options}
        variants={variants}
      >
        {({option}) => <ProductOptions key={option.name} option={option} />}
      </VariantSelector>
      <br />
      <AddToCartButton
        disabled={!selectedVariant || !selectedVariant.availableForSale}
        onClick={() => {
          window.location.href = window.location.href + '#cart-aside';
        }}
        lines={[
          {
            merchandiseId: selectedVariant.id,
            quantity: 1,
          }
        ]}
      >
        {buttonLabel}
      </AddToCartButton>
      {
         selectedVariant?.isPreorder &&
           <div>
             {selectedVariant?.estimatedShipDates}
           </div>
      }
      {selectedVariant?.isPreorder && <purple-dot-learn-more />}
    </div>
  );
}

Set the right line item properties when adding to cart

The Purple Dot package expects the __releaseId property to exist on the line item to detect preorders in the shopper’s cart. You can easily add this by modifying your add to cart calls. Here’s how you can do it using the standard AddToCartButton element.

// routes/products.$handle.tsx

function ProductForm({
  product,
  selectedVariant,
  variants,
}: {
  product: ProductFragment;
  selectedVariant: ProductFragment['selectedVariant'] & { isPreorder: boolean, releaseId: string, estimatedShipDates: string };
  variants: Array<ProductVariantFragment>;
}) {
  const buttonLabel = selectedVariant?.availableForSale
    ? selectedVariant?.isPreorder ? 'Pre-order' : 'Add to cart'
    : 'Sold out';

  function getAttributes(selectedVariant) {
    if (!selectedVariant.isPreorder) {
      return [];
    }

    return [
      {
        key: '__releaseId',
        value: selectedVariant.releaseId,
      },
      {
          key: 'Purple Dot Pre-order',
          value: selectedVariant.estimatedShipDates,
      },
    ];
  }

  return (
    <div className="product-form">
      <VariantSelector
        handle={product.handle}
        options={product.options}
        variants={variants}
      >
        {({option}) => <ProductOptions key={option.name} option={option} />}
      </VariantSelector>
      <br />
      <AddToCartButton
        disabled={!selectedVariant || !selectedVariant.availableForSale}
        onClick={() => {
          window.location.href = window.location.href + '#cart-aside';
        }}
        lines={
          selectedVariant
            ? [
                {
                  merchandiseId: selectedVariant.id,
                  quantity: 1,
                  attributes: getAttributes(selectedVariant),
                },
              ]
            : []
        }
      >
        {buttonLabel}
      </AddToCartButton>
      {
        selectedVariant?.isPreorder &&
          <div>
            {selectedVariant?.estimatedShipDates}
          </div>
      }
      {selectedVariant?.isPreorder && <purple-dot-learn-more />}
    </div>
  );
}

Display preorder shipping date in your side cart

You can use the Purple Dot Preorder property that you added in your line item to differentiate preorder items in the side cart. For example, with a CartLineItem.tsx file that contains the component to display each line item in the shoppers, here’s how you can do it:

// CartLineItem.tsx
export function CartLineItem(lineItem) {
  return (
    <LineItemContainer>
      <ProductImage variant={lineItem.variant} />
        <div>
          {lineItem.productName}
        </div>
        <ProductPrice variant={lineItem.variant} />
        {lineItem.attributes.find(({ key }) => key === 'Purple Dot Preorder')?.value}
      </LineItemContainer>
  );
}

Open the Purple Dot checkout

Once the shoppers have added their preorder items to the cart, it’s time to let them checkout through the Purple Dot checkout. You can do this on the cart page as shown below:

// app/pages/cart.tsx
import * as checkout from '@purple-dot/browser/checkout';
import { cartHasPreorderItem } from '@purple-dot/browser/cart';

export default function Cart() {
  const [root] = useMatches();
  const cart = root.data?.cart as Promise<CartApiQueryFragment | null>;

  return (
    <div className="cart">
      <h1>Cart</h1>
      <Suspense fallback={<p>Loading cart ...</p>}>
        <Await errorElement={<div>An error occurred</div>} resolve={cart}>
          {(cart) => {

            // Open Purple Dot checkout if the cart contains a pre-order
            useEffect(() => {
              if (cart) {
                cartHasPreorderItem(cart.lines.nodes).then((hasPreorderItem) => {
                  if (hasPreorderItem) {
                    checkout.open({
                      cartId: cart.id,
                    });
                  }
                });
              }
            }, [cart?.id]);

            return <CartMain layout="page" cart={cart} />;
          }}
        </Await>
      </Suspense>
    </div>
  );
}

This opens up an iframe that allows the shopper to checkout with Purple Dot.

Embed the Purple Dot self-service for shoppers to manage their preorders

Pick any page where you’d like to display the Purple Dot self-service and embed the Purple Dot self-service as shown below:

export function SelfServicePage() {
  return (
    <PageContainer>
      <purple-dot-self-service />
    </PageContainer>
  );
}

Add the URL of this page to the Cancel link within Settings > Email in the merchant portal.

Last updated