This guide will walk you through Purple Dot's Platform integration into any Shopify Hydrogen Storefront. If you are using our Shopify App , please see Installing 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
Copy 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:
Copy 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:
Copy // 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:
The waitlist shipping dates so that shoppers know when to expect their order to start shipping
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:
Copy // 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:
Copy // 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.
Copy // 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:
Copy // 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:
Copy // 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:
Copy 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.