> **Note**: this package is to be used on stores built with **Shopify Hydrogen v2**. If your store is built with the deprecated Shopify Hydrogen v1, please use the [version 1](https://www.npmjs.com/package/@okendo/shopify-hydrogen/v/1.6.6) of this package.

> **Note**: the new version of Shopify Hydrogen v2 uses **React Router**. Previous versions used **Remix**. If your store is built with Remix, please use [version `2.4`](https://www.npmjs.com/package/@okendo/shopify-hydrogen/v/2.4.0) of this package.

# Okendo Hydrogen (React Router) React Components

This package brings Okendo's [Reviews widgets](https://okendo.io/reviews) and [Loyalty widgets](https://okendo.io/loyalty) to a Shopify Hydrogen store.

## Requirements

- You need a Shopify store with a [Hydrogen](https://hydrogen.shopify.dev/) storefront and [Okendo](https://apps.shopify.com/okendo-reviews) installed and configured.
- For existing merchants, your store must be using Okendo's Widget Plus widgets. [Contact us](mailto:support@okendo.io) if it's not the case, it's free to upgrade.
- **[Contact us](mailto:support@okendo.io) to enable the Storefront Metafields feature on your account.** This is necessary to enable server-side rendering using Shopify metafields.

## Demo Store

Our demo store, which is based on the demo store provided by Shopify, can be found [here](https://github.com/okendo/okendo-shopify-hydrogen-demo).

> Note: there have been multiple versions of Shopify's Hydrogen demo store. If your project is based on an old version of it, consult the [history of our demo store's repository](https://github.com/okendo/okendo-shopify-hydrogen-demo/commits/master/).

## Installation

This package provides:

- one function: `getOkendoProviderData`,
- one provider: `OkendoProvider`,
- three React components: `OkendoStarRating`, `OkendoReviews`, and `OkendoReviewsCarousel`.

The function `getOkendoProviderData` needs to be called in the `loader` function of `root.tsx` in your Hydrogen store. The data is then passed to `OkendoProvider`, which is added to your website's `body` and wraps everything in it.

Then, the React components can be added on your store pages. There are a few more bits of configuration to do, please see below.

> The code examples provided in this section are based on the Shopify template store created by running `npm create @shopify/hydrogen@latest` (see [Shopify's documentation](https://shopify.dev/docs/custom-storefronts/hydrogen/getting-started)). You will find the following steps already done in [our demo store](https://github.com/okendo/okendo-shopify-hydrogen-demo).

Run:

```bash
npm i @okendo/shopify-hydrogen
```

### `app/root.tsx`

Open `app/root.tsx` and add the following import:

```ts
import {
  OkendoProvider,
  getOkendoProviderData,
} from '@okendo/shopify-hydrogen';
```

Locate the `loadDeferredData` function, append `okendoProviderData` to the returned data as shown below, and set `subscriberId` to your Okendo subscriber ID.

```ts
// ...
return {
  cart: cart.get(),
  isLoggedIn: customerAccount.isLoggedIn(),
  footer,
  okendoProviderData: getOkendoProviderData({
    context,
    subscriberId: '<your-okendo-subscriber-id>',
  }),
};
```

Locate the `Layout` component, add the `meta` tag `oke:subscriber_id` to `head`, and place your Okendo subscriber ID in its content:

```tsx
<head>
  <meta charSet="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
  ...
</head>
```

Locate the `App` function, append `OkendoProvider`, and pass it the promise returned by `getOkendoProviderData`:

```tsx
<body>
  return (
    <OkendoProvider okendoProviderData={data.okendoProviderData}>
      <Analytics.Provider
        cart={data.cart}
        shop={data.shop}
        consent={data.consent}
      >
        <PageLayout {...data}>
          <Outlet />
        </PageLayout>
      </Analytics.Provider>
    </OkendoProvider>
  );
```

### `app/entry.server.tsx`

> This section is necessary only if Content Security Policy is active in your project.

Locate the call to `createContentSecurityPolicy`, and ensure your configuration includes the entries below:

```ts
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  defaultSrc: [
    'https://www.google.com',
    'https://www.gstatic.com',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://d3g5hqndtiniji.cloudfront.net',
    'https://dov7r31oq5dkj.cloudfront.net',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
    'https://api.okendo.io',
    'data:',
  ],
  styleSrc: [
    'https://fonts.googleapis.com',
    'https://fonts.gstatic.com',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
  ],
  connectSrc: [
    'https://api.okendo.io',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
    'https://api.raygun.com',
    'https://www.google.com',
    'https://www.gstatic.com',
  ],
  frameSrc: ['https://www.google.com', 'https://www.gstatic.com'],
});
```

### `app/lib/fragments.ts`

Add the following GraphQL fragment at the bottom of the file:

```ts
export const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
  fragment OkendoStarRatingSnippet on Product {
    okendoStarRatingSnippet: metafield(
      namespace: "app--1576377--reviews"
      key: "star_rating_snippet"
    ) {
      value
    }
  }
` as const;

export const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
  fragment OkendoReviewsSnippet on Product {
    okendoReviewsSnippet: metafield(
      namespace: "app--1576377--reviews"
      key: "reviews_widget_snippet"
    ) {
      value
    }
  }
` as const;
```

### `app/routes/_index.tsx`

Add the following import:

```ts
import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
```

Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `RECOMMENDED_PRODUCTS_QUERY`:

```ts
const RECOMMENDED_PRODUCTS_QUERY = `#graphql
  ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
  fragment RecommendedProduct on Product {
    id
    title
    handle
    priceRange {
      minVariantPrice {
        amount
        currencyCode
      }
    }
    images(first: 1) {
      nodes {
        id
        url
        altText
        width
        height
      }
    }
    ...OkendoStarRatingSnippet
  }
  query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
    @inContext(country: $country, language: $language) {
    products(first: 4, sortKey: UPDATED_AT, reverse: true) {
      nodes {
        ...RecommendedProduct
      }
    }
  }
` as const;
```

### `app/routes/collections.all.tsx`

Add the following import:

```ts
import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
```

Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `COLLECTION_ITEM_FRAGMENT`:

```ts
const COLLECTION_ITEM_FRAGMENT = `#graphql
  ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
  fragment MoneyCollectionItem on MoneyV2 {
    amount
    currencyCode
  }
  fragment CollectionItem on Product {
    id
    handle
    title
    featuredImage {
      id
      altText
      url
      width
      height
    }
    priceRange {
      minVariantPrice {
        ...MoneyCollectionItem
      }
      maxVariantPrice {
        ...MoneyCollectionItem
      }
    }
    ...OkendoStarRatingSnippet
  }
` as const;
```

### `app/routes/collections.$handle.tsx`

Add the following import:

```ts
import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
```

Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `COLLECTION_ITEM_FRAGMENT`:

```ts
const PRODUCT_ITEM_FRAGMENT = `#graphql
  ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
  fragment MoneyProductItem on MoneyV2 {
    amount
    currencyCode
  }
  fragment ProductItem on Product {
    id
    handle
    title
    featuredImage {
      id
      altText
      url
      width
      height
    }
    priceRange {
      minVariantPrice {
        ...MoneyProductItem
      }
      maxVariantPrice {
        ...MoneyProductItem
      }
    }
    ...OkendoStarRatingSnippet
  }
` as const;
```

### `app/components/ProductItem.tsx`

Add the following import:

```ts
import { OkendoStarRating } from '@okendo/shopify-hydrogen';
```

Add `OkendoStarRating` to the `RecommendedProducts` component — for instance, we can add it below the product title, like this:

```tsx
<Image
  data={product.images.nodes[0]}
  aspectRatio="1/1"
  sizes="(min-width: 45em) 20vw, 50vw"
/>
<h4>{product.title}</h4>
<OkendoStarRating
  className="mb-2"
  productId={product.id}
  okendoStarRatingSnippet={product.okendoStarRatingSnippet}
/>
<small>
  <Money data={product.priceRange.minVariantPrice} />
</small>
```

> Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.

We now have the Okendo Star Rating widget visible on our page:

![Okendo's Star Rating widget](./res/okendo-star-rating-widget.webp)

### `app/routes/products.$handle.tsx`

Add the following imports:

```ts
import { OkendoReviews, OkendoStarRating } from '@okendo/shopify-hydrogen';
import {
  OKENDO_PRODUCT_REVIEWS_FRAGMENT,
  OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
} from '~/lib/fragments';
```

Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}`, `${OKENDO_PRODUCT_REVIEWS_FRAGMENT}`, `...OkendoStarRatingSnippet`, and `...OkendoReviewsSnippet` to `PRODUCT_FRAGMENT`:

```ts
const PRODUCT_FRAGMENT = `#graphql
  ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
  ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
  fragment Product on Product {
    id
    title
    vendor
    handle
    descriptionHtml
    description
    encodedVariantExistence
    encodedVariantAvailability
    options {
      name
      optionValues {
        name
        firstSelectableVariant {
          ...ProductVariant
        }
        swatch {
          color
          image {
            previewImage {
              url
            }
          }
        }
      }
    }
    selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
      ...ProductVariant
    }
    adjacentVariants (selectedOptions: $selectedOptions) {
      ...ProductVariant
    }
    seo {
      description
      title
    }
    ...OkendoStarRatingSnippet
    ...OkendoReviewsSnippet
  }
  ${PRODUCT_VARIANT_FRAGMENT}
` as const;
```

Add `OkendoStarRating` and `OkendoReviews` to the `Product` component:

```tsx
<>
  <div className="product">
    <ProductImage image={selectedVariant?.image} />
    <div className="product-main">
      <h1>{title}</h1>
      <OkendoStarRating
        className="mb-4"
        productId={product.id}
        okendoStarRatingSnippet={product.okendoStarRatingSnippet}
      />
      <ProductPrice
        price={selectedVariant?.price}
        compareAtPrice={selectedVariant?.compareAtPrice}
      />
      ...
    </div>
    ...
  </div>

  <OkendoReviews
    productId={product.id}
    okendoReviewsSnippet={product.okendoReviewsSnippet}
  />
</>
```

> Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.

We now have the Okendo Star Rating and Reviews widgets visible on our product page:

![Okendo's Star Rating and Reviews widgets](./res/okendo-star-rating-and-reviews-widgets.webp)

### All-Reviews Widget - Client Side Only

If you would like to include a copy of the Okendo Reviews Widget which displays all reviews for a given store (to be used on a reviews page for example), please add `OkendoReviews` without supplying the `productId`.

Please note the all-reviews widget loads on the client, not the server.

```tsx
import { type MetaFunction } from 'react-router';
import { OkendoReviews } from '@okendo/shopify-hydrogen';

export const meta: MetaFunction = () => {
  return [{ title: `Hydrogen | Okendo All Reviews` }];
};

export default function ReviewsPage() {
  return (
    <div className="all-reviews">
      <h1>All-Reviews Widget</h1>
      <OkendoReviews />
    </div>
  );
}
```

### Okendo Reviews Carousel Widget - Client Side Only

If you would like to include a copy of the Okendo Reviews Carousel Widget which displays reviews by product or group for a given store (to be used on a homepage or featured page for example), please add `OkendoReviewsCarousel` with or without the `productId` or `groupId`.

Please note the carousel widget loads on the client, not the server.

```tsx
import { type MetaFunction } from 'react-router';
import { OkendoReviews } from '@okendo/shopify-hydrogen';

export const meta: MetaFunction = () => {
  return [{ title: `Hydrogen | Okendo Reviews Carousel` }];
};

export default function AFeaturedPage() {
  return (
    <div className="all-reviews">
      <h1>Reviews Carousel Widget</h1>
      <OkendoReviewsCarousel productId={product.id} />
    </div>
  );
}
```

You can also use `OkendoReviewsCarousel` without `productId`, in order to display reviews for all products. For instance, we can add it to the homepage in `app/routes/_index.tsx`:

```ts
export default function Homepage() {
  const data = useLoaderData<typeof loader>();
  return (
    <div className="home">
      <FeaturedCollection collection={data.featuredCollection} />
      <RecommendedProducts products={data.recommendedProducts} />
      <OkendoReviewsCarousel />
    </div>
  );
}
```

# Loyalty Widgets

## Installation

To include Loyalty Widgets in your Shopify Hydrogen store, you will need to make the following changes:

These are the instructions for stores which use the **Customer Account API** for customer login. If you are using the **Storefront API** for customer login then please skip ahead to [Storefront API Login](#storefront-api-login) below

1. Add `customerAccessToken: await args.context.customerAccount.getAccessToken(),` to your `loader` function, this will be used to log your customer into the Loyalty App.

2. Add `okendoProducts: ['reviews', 'loyalty'],` as a property to `getOkendoProviderData` in your `loader` function, alongside the existing `context` and `subscriberId` arguments.

> Note: If you only wish to use the Loyalty product and not reviews then simply leave out the `'reviews'` from the array like so: `okendoProducts: ['loyalty'],`.

The relevant section should now look something like this:

```ts
return defer({
  // ...
  customerAccessToken: await args.context.customerAccount.getAccessToken(),
  okendoProviderData: getOkendoProviderData({
    context: args.context,
    subscriberId: '<your-okendo-subscriber-id>',
    okendoProducts: ['reviews', 'loyalty'],
  }),
});
```

3. Add `customerAccessToken={data.customerAccessToken}` to the `OkendoProvider` component, it should now look like:

```tsx
<OkendoProvider
  okendoProviderData={data.okendoProviderData}
  customerAccessToken={data.customerAccessToken}
>
  ...
</OkendoProvider>
```

If your Okendo Loyalty Settings are [correctly set up](https://support.okendo.io/en/collections/8270193-okendo-loyalty) and your program has launched, the Loyalty Floating Widget will now appear on your store.

### Storefront API Login

Instead of passing `customerAccessToken` into the `OkendoProvider` as above, you will need to pass in two other properties:

- `storefrontCustomerAccessToken`: When the user logs in with a username and password, the response from Shopify should look like `{ customerAccessToken: { accessToken: string }}`. The value of the `accessToken` string is what we require for the `storefrontCustomerAccessToken`, but the method of accessing it in your project's `loader` function will differ depending on your setup.

- `storefrontAccessToken`: a static value which is most likely in your environment variables as `PUBLIC_STOREFRONT_API_TOKEN` and being used for your `X-Shopify-Storefront-Access-Token` header. If not, it can usually be found in `Shopify Admin -> Sales channels -> Hydrogen/Headless -> Pick your storefront -> Storefront Settings -> Storefront API -> Public access token (Use this token to access the Storefront API in client-side contexts)`

Your `OkendoProvider` component should now look something like this:

```tsx
<OkendoProvider
  okendoProviderData={data.okendoProviderData}
  storefrontCustomerAccessToken={data.storefrontCustomerAccessToken}
  storefrontAccessToken={data.storefrontAccessToken}
>
  ...
</OkendoProvider>
```

## Dedicated Loyalty Page

Add `<OkendoLoyaltyEmbeddedWidget />` to any components/pages where you wish to have the Dedicated Loyalty Page appear.

_Make sure you are importing the component from the `okendo-shopify-hydrogen` package: `import {OkendoLoyaltyEmbeddedWidget} from '@okendo/shopify-hydrogen';`_
