Integrate the Joy Loyalty widget to Hydrogen

Hand-on guide on how to integrate Joy Loyalty seamlessly with Shopify Headless Commerce or Custom Storefront.

Introduction to Joy Loyalty Widget Integration with Hydrogen

The Joy Loyalty Widget Integration is your bridge to creating a seamless, engaging loyalty experience in a Hydrogen-powered Shopify store. It's a comprehensive solution that transforms customer interactions into valuable, rewarding moments.

You can find a complete guide on our npm package here.

Why do you need to integrate

Building your Shopify store with Hydrogen, integrating Joy Loyalty is a game-changer. A loyalty program boosts customer retention, increases average order value, and strengthens brand loyalty. With Joy Loyalty, you can incentivize repeat purchases, stand out from competitors, and gain insights into shopper behavior—all while delivering a seamless experience.

How to enable custom storefront development

Go to Settings > Developers > Enable Custom Storefront development

How to set up your Hydrogen store

If you've already developed your Hydrogen store, you're all set! However, if you haven't started yet or are looking to test a Hydrogen store with Joy Loyalty, don't worry. You can easily set up a quick-start Hydrogen store by following this guide: Getting Started with Hydrogen.

If you are not on Shopify Plus plan but still want to test this out with development store first. You can update your .env file with Storefront API key from your Custom App

SESSION_SECRET="foobar"
PUBLIC_STOREFRONT_API_TOKEN="YOUR_KEY"
PUBLIC_STOREFRONT_API_VERSION="2023-04"
PUBLIC_STORE_DOMAIN="YOUR_STORE.myshopify.com"

Installation

npm install joyso-hydrogen-sdk
# or
yarn add joyso-hydrogen-sdk

Implementation

1. Load Joy Loyalty Data

In your app/root.jsx or app/layout.jsx loader:

import {getJoyConfigData} from 'joyso-hydrogen-sdk';
import {HEADER_QUERY} from '~/lib/fragments';

/**
 * Load critical data for the page
 */
async function loadCriticalData({context}) {
  const {storefront, session} = context;

  // Load both header and Joy data in parallel
  const [header, joyData] = await Promise.all([
    storefront.query(HEADER_QUERY, {
      cache: storefront.CacheLong(),
      variables: {
        headerMenuHandle: 'main-menu',
      },
    }),
    getJoyConfigData({storefront, session, shopId: 'JOY_APP_ID'})
  ]);

  return {
    header,
    ...joyData,
  };
}

export async function loader({context}) {
  const criticalData = await loadCriticalData({context});

  return {
    ...criticalData,
    // ... other loader data
  };
}

2. Initialize Joy Loyalty in Root Layout

In your app/root.jsx or app/layout.jsx:

import {useJoyLoyalty} from 'joyso-hydrogen-sdk';

export default function App() {
  const data = useLoaderData();

  useJoyLoyalty(data);

  return <Outlet />;
}

3. Add CSP whitelist

Update your entry.server.jsx to include the CSP whitelist for Joy Loyalty:

import {RemixServer} from '@remix-run/react';
import {isbot} from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';

/**
 * @param {Request} request
 * @param {number} responseStatusCode
 * @param {Headers} responseHeaders
 * @param {EntryContext} remixContext
 * @param {AppLoadContext} context
 */
export default async function handleRequest(
  request,
  responseStatusCode,
  responseHeaders,
  remixContext,
  context, // Prefix with _ to indicate it's intentionally unused
) {
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    scriptSrc: [
      'self',
      'https://cdn.shopify.com',
      'https://shopify.com',
      'https://www.google-analytics.com',
      'https://www.googletagmanager.com',
      'https://*.joy.avada.io',
      'https://joy.avada.io',
      'https://geoip.apps.getjoy.ai',
      ...(process.env.NODE_ENV !== 'production' ? ['http://localhost:*'] : []),
    ],
    connectSrc: [
      'self',
      'https://cdn.shopify.com',
      'https://shop.app',
      'https://*.joy.avada.io',
      'https://joy.avada.io',
      'https://monorail-edge.shopifysvc.com',
      'https://geoip.apps.getjoy.ai',
      'https://thomas-shopify-production-36.myshopify.com',
      'ws://127.0.0.1:*',
      'ws://localhost:*',
      'ws://*:*',
    ],
    styleSrc: [
      'self',
      'unsafe-inline',
      'https://cdn.shopify.com',
      'https://cdn.builder.io',
      'https://fonts.bunny.net',
      'https://*.joy.avada.io',
    ],
    imgSrc: [
      'self',
      'data:',
      'https://cdn.shopify.com',
      'https://*.joy.avada.io',
      'https://cdnapps.avada.io',
    ],
    frameSrc: [
      'self',
      'https://cdn.shopify.com',
      'https://shop.app',
      'https://*.joy.avada.io',
      'https://geoip.apps.getjoy.ai',
    ],
    fontSrc: ['self', 'https://fonts.bunny.net', 'data:', 'https://*.joy.avada.io'],
    mediaSrc: ['self'],
    objectSrc: ['none'],
    defaultSrc: ['self', 'https://*.joy.avada.io', 'https://joy.avada.io'],
  });

  const body = await renderToReadableStream(
    <NonceProvider>
      <RemixServer context={remixContext} url={request.url} nonce={nonce} />
    </NonceProvider>,
    {
      nonce,
      signal: request.signal,
      onError(error) {
        console.error(error);
        responseStatusCode = 500;
      },
    },
  );

  if (isbot(request.headers.get('user-agent'))) {
    await body.allReady;
  }

  responseHeaders.set('Content-Type', 'text/html');
  responseHeaders.set('Content-Security-Policy', header);

  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

/** @typedef {import('@shopify/remix-oxygen').EntryContext} EntryContext */
/** @typedef {import('@shopify/remix-oxygen').AppLoadContext} AppLoadContext */

After this, you should be able to see the Joy Loyalty widget in your storefront. Besides, the quickstart template of Hydrogen has box-sizing: content-box so the floating button may look a bit weird. You can add this CSS to app. css to fix it. Otherwise, there should be no CSS issues.

*,
*::before,
*::after {
    box-sizing: inherit;
}

html {
    box-sizing: border-box;
    height: 100%;
}

3. Point Calculator

GraphQL Query and Loader

First, update your product page loader and query:

// app/routes/products.$handle.jsx
import {AnalyticsPageType} from '@shopify/hydrogen';

async function loadCriticalData({context, params, request}) {
  const {handle} = params;
  const {storefront} = context;

  if (!handle) {
    throw new Error('Expected product handle to be defined');
  }

  const [{product}] = await Promise.all([
    storefront.query(PRODUCT_QUERY, {
      variables: {handle, selectedOptions: getSelectedProductOptions(request)},
    }),
    // Add other queries here, so that they are loaded in parallel
  ]);

  if (!product?.id) {
    throw new Response(null, {status: 404});
  }

  return {
    product,
    // We need this to detect the page type
    analytics: {
      pageType: AnalyticsPageType.product,
    },
  };
}

const PRODUCT_QUERY = `#graphql
  query Product($handle: String!, $selectedOptions: [SelectedOptionInput!]!) {
    product(handle: $handle) {
      id
      title
      handle
      productType
      tags

      selectedVariant: selectedOrFirstAvailableVariant {
        id
        price {
          amount
        }
        compareAtPrice {
          amount
        }
        selectedOptions {
          name
          value
        }
      }

      # Need to add this if have not had it in the query
      collections(first: 100) {
        nodes {
          title
        }
      }

      # Need to add this if have not had it in the query
      variants(first: 250) {
        nodes {
          ...ProductVariant
          selectedOptions {
            name
            value
          }
          # Required for limitations
          limitation: metafield(namespace: "custom", key: "limitation") {
            value
          }
        }
      }
    }
  }
`;

Component Implementation

In your product page (e.g., app/routes/products.$handle.jsx):

import {useJoyLoyaltyCalculator} from 'joyso-hydrogen-sdk';

export default function Product() {
  // 1. Get product data from loader
  const {product, analytics} = useLoaderData();

  // 2. Initialize the calculator
  useJoyLoyaltyCalculator({
    product,
    analytics,
  });

  // 3. Render product details with calculator
  return (
    <div className="product">
      <ProductImage image={selectedVariant?.image} />
      <div className="product-main">
        <h1>{title}</h1>
        <ProductPrice
          price={selectedVariant?.price}
          compareAtPrice={selectedVariant?.compareAtPrice}
        />
        <br />
        {/* Add the calculator block */}
        <div className="joy-points-calculator__block"></div>
        <br />
        <ProductForm
          productOptions={productOptions}
          selectedVariant={selectedVariant}
        />
      </div>
    </div>
  );
}

Test the widget

After these are implemented, you will see the widget display on your custom storefront according to your settings. However, you can compare the widgets on the custom storefront and the Online Store channel to make sure both are working properly. With the widget, you can earn points, redeem points, and use coupons seamlessly on your store.

Wrap up

Our app has other features such as Joy Point Calculator, Joy Rewards Page, etc. If you want to build your own Rewards page using Hydrogen, we will have another post showing how to implement this Rewards page using our Public API and JavaScript SDK.

We hope that this guide provides you with the first step on how to integrate the Joy Loyalty Program widget into your Headless Commerce store.

Last updated

Was this helpful?