# Shopify Hydrogen - Combined Checkout

*This guide will walk you through Purple Dot's Platform integration into any Shopify Hydrogen Storefront.*

## Combined checkout overview

The Combined Checkout flow uses your store’s normal Shopify cart, unlike the Express flow, which uses Purple Dot’s own cart. This means that pre-order and in-stock items coexist within the same Shopify cart, and Purple Dot’s integration layers on top of your existing cart implementation to manage pre-order metadata and checkout routing.

There are 5 key requirements an integration needs to implement for a Combined Checkout:

* **Displaying pre-order items.** Purple Dot is the source of data for which items are on pre-order and their estimated ship dates. The integration needs to show that those items are on pre-order, with the estimated ship dates, the "Learn more” informational content, and a “Pre-order” call to action.
* **Adding pre-order items to cart.** When adding an item to the cart, the integration needs to determine if it is a pre-order and attach the waitlist ID and ship dates if it is.
* **Showing pre-order items in cart.** When a pre-order item is in the cart, the estimated ship dates need to be displayed.
* **Using the Combined pre-order checkout.** The Purple Dot pre-order checkout lets shoppers checkout with pre-order items. When a pre-order item is in the cart, the integration needs to direct the shopper to the Purple Dot pre-order checkout instead of the Shopify checkout.
* **Shopper portal.** The shopper self-service portal lets shoppers manage their pre-orders. The integration needs to be embedded on a page in the storefront that the shoppers will be directed to in their confirmation email.

## Install the Purple Dot browser package

```bash
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:

```javascript
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](https://www.purpledotprice.com/merchant-portal/integration) 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:

```tsx
// 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:

<figure><img src="https://35070260-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F58PWmT9iYQoKNKYtPSZy%2Fuploads%2FsRYNfMKWwDgWuDODZVZg%2FScreenshot%25202023-08-02%2520at%252010.48.19%2520PM.png?alt=media&#x26;token=4bcfae18-c9e1-400a-a50e-e8527260605a" alt=""><figcaption></figcaption></figure>

A pre-order product:

<figure><img src="https://35070260-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F58PWmT9iYQoKNKYtPSZy%2Fuploads%2FYY49fXqQxPaPnr0K2zOy%2FScreenshot%25202023-08-02%2520at%252010.47.50%2520PM.png?alt=media&#x26;token=f2820099-9ca0-4ff9-a630-fb691b2f5807" alt=""><figcaption></figcaption></figure>

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

```tsx
// 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,
        // Only required if using Shopify checkout
        sellingPlanId: variantRes?.waitlist?.selling_plan_id,
        compatibleCheckouts: variantRes?.waitlist?.compatible_checkouts,
      },
    },
    variants,
  });
}
```

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

```tsx
// 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 | null;
    estimatedShipDates: string | null;
    sellingPlanId?: string | null;
    compatibleCheckouts?: 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.

```tsx
// routes/products.$handle.tsx
import { purpleDotAttributes } from '@purple-dot/browser/cart';

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={
          selectedVariant
            ? [
                {
                  merchandiseId: selectedVariant.id,
                  quantity: 1,
                  sellingPlanId: selectedVariant.sellingPlanId,
                  attributes: purpleDotAttributes(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:

```tsx
// 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:

```tsx
// 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,
                      // Pass in a random, stable sessionId if using Shopify checkout
                      sessionId,
                    });
                  }
                });
              }
            }, [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:

```tsx
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.

<figure><img src="https://35070260-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F58PWmT9iYQoKNKYtPSZy%2Fuploads%2FMpTeWYuuLEH1ZTyIJHfb%2FCleanShot%202024-09-27%20at%2013.50.17.png?alt=media&#x26;token=a773d46e-cc3e-48b8-9160-8da8f7ba38e4" alt=""><figcaption></figcaption></figure>
