2.5.0 • Published 5 months ago

@okendo/shopify-hydrogen v2.5.0

Weekly downloads
-
License
SEE LICENSE IN LI...
Repository
-
Last release
5 months ago

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 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 of this package.

Okendo Hydrogen (React Router) React Components

This package brings Okendo's Reviews widgets and Loyalty widgets to a Shopify Hydrogen store.

Requirements

  • A Shopify store with a Hydrogen storefront and Okendo installed and configured.
  • For existing merchants, your store must be using Okendo's Widget Plus widgets. Contact us if it's not the case, it's free to upgrade.

Demo Store

Our demo store, which is based on the demo store provided by Shopify, can be found here.

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.

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). You will find the following steps already done in our demo store.

Run:

npm i @okendo/shopify-hydrogen

app/root.tsx

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

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.

// ...
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:

<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>

Append OkendoProvider to body, and pass it the promise returned by getOkendoProviderData. If Content Security Policy is active in your project, you also need to provide the nonce (available with const nonce = useNonce() in Shopify's Hydrogen demo store):

<body>
  {data ? (
    <OkendoProvider nonce={nonce} okendoProviderData={data.okendoProviderData}>
      <Analytics.Provider
        cart={data.cart}
        shop={data.shop}
        consent={data.consent}
      >
        <PageLayout {...data}>{children}</PageLayout>
      </Analytics.Provider>
    </OkendoProvider>
  ) : (
    children
  )}
  <ScrollRestoration nonce={nonce} />
  <Scripts nonce={nonce} />
</body>

app/entry.server.tsx

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

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

Note that it's necessary to to add the default values ('self', etc.) when extending the CSP. The call to createContentSecurityPolicy should now look like the following:

const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  defaultSrc: [
    "'self'",
    'localhost:*',
    'https://cdn.shopify.com',
    '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:',
  ],
  imgSrc: [
    "'self'",
    'https://cdn.shopify.com',
    'data:',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://d3g5hqndtiniji.cloudfront.net',
    'https://dov7r31oq5dkj.cloudfront.net',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
  ],
  mediaSrc: [
    "'self'",
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://d3g5hqndtiniji.cloudfront.net',
    'https://dov7r31oq5dkj.cloudfront.net',
    'https://cdn-static.okendo.io',
  ],
  styleSrc: [
    "'self'",
    "'unsafe-inline'",
    'https://cdn.shopify.com',
    'https://fonts.googleapis.com',
    'https://fonts.gstatic.com',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
  ],
  scriptSrc: [
    "'self'",
    'https://cdn.shopify.com',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://dov7r31oq5dkj.cloudfront.net',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
    'https://api.okendo.io',
    'https://www.google.com',
    'https://www.gstatic.com',
  ],
  fontSrc: [
    "'self'",
    'https://fonts.gstatic.com',
    'https://d3hw6dc1ow8pp2.cloudfront.net',
    'https://dov7r31oq5dkj.cloudfront.net',
    'https://cdn.shopify.com',
    'https://cdn-static.okendo.io',
    'https://surveys.okendo.io',
  ],
  connectSrc: [
    "'self'",
    'https://monorail-edge.shopifysvc.com',
    'localhost:*',
    'ws://localhost:*',
    'ws://127.0.0.1:*',
    '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:

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:

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

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

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:

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

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

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:

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

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

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:

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

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

<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

app/routes/products.$handle.tsx

Add the following imports:

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:

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:

<>
  <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

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.

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.

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:

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:

  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:

return defer({
  // ...
  customerAccessToken: await args.context.customerAccount.getAccessToken(),
  okendoProviderData: getOkendoProviderData({
    context: args.context,
    subscriberId: '<your-okendo-subscriber-id>',
    okendoProducts: ['reviews', 'loyalty'],
  }),
});
  1. Add customerAccessToken={data.customerAccessToken} to the OkendoProvider component, it should now look like:
<OkendoProvider
  nonce={nonce}
  okendoProviderData={data.okendoProviderData}
  customerAccessToken={data.customerAccessToken}
>
  ...
</OkendoProvider>

If your Okendo Loyalty Settings are correctly set up and your program has launched, the Loyalty Floating Widget will now appear on your store.

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';

2.4.0

7 months ago

2.2.7

12 months ago

2.2.6

12 months ago

2.3.0

11 months ago

2.5.0

5 months ago

2.3.2

10 months ago

2.3.1

11 months ago

2.2.5

12 months ago

2.2.4

1 year ago

2.2.3

1 year ago

2.2.1

1 year ago

2.2.0

1 year ago

2.2.2

1 year ago

2.1.6

1 year ago

2.1.5

1 year ago

2.1.4

2 years ago

2.1.3

2 years ago

2.1.2

2 years ago

2.1.1

2 years ago

2.1.0

2 years ago

2.0.3

2 years ago

2.0.2

2 years ago

1.6.6

2 years ago

1.3.1

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.3.0

3 years ago

1.2.4

3 years ago

1.2.3

3 years ago

1.2.2

3 years ago

1.2.1

3 years ago

1.2.0

3 years ago

1.1.0

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago

0.0.13

3 years ago

0.0.12

3 years ago

0.0.11

3 years ago

0.0.10

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago