@okendo/shopify-hydrogen v2.5.0
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
, andOkendoReviewsCarousel
.
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 tocreateContentSecurityPolicy
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:
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:
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:
Add
customerAccessToken: await args.context.customerAccount.getAccessToken(),
to yourloader
function, this will be used to log your customer into the Loyalty App.Add
okendoProducts: ['reviews', 'loyalty'],
as a property togetOkendoProviderData
in yourloader
function, alongside the existingcontext
andsubscriberId
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'],
}),
});
- Add
customerAccessToken={data.customerAccessToken}
to theOkendoProvider
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';
7 months ago
12 months ago
12 months ago
11 months ago
5 months ago
10 months ago
11 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago