Skip to content

Commit

Permalink
Update hydrogen (#30)
Browse files Browse the repository at this point in the history
* Updating Hydrogen
* Adding nonce 

---------

Co-authored-by: Ilja Ganulevics <ilja.ganulevics@nosto.com>
Co-authored-by: olsi-qose <olsi.qose@nosto.com>
Co-authored-by: Mikko Poutanen <12759991+nosto-mikpou@users.noreply.github.com>
  • Loading branch information
4 people authored Sep 13, 2024
1 parent d588703 commit d0361e2
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 85 deletions.
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ The library uses [@nosto/nosto-react](https://github.com/Nosto/nosto-react) unde
- The NostoProvider component is **required** and provides the Nosto functionality.
- It must wrap all other Nosto components.
- Pass your Nosto merchant ID via the `account` prop.
- Imports the Nosto client script into the window environment. This is used to controll all of Nosto functionality.
- Pass Shopify Hydrogen nonce as a nonce prop. This is used to add your stores content security policy nonce to a Nosto script.
- Imports the Nosto client script into the window environment. This is used to control all of Nosto functionality.
- Remix separates the App and ErrorBoundary within the root. Make sure to add <NostoProvider/> to both for also enabling Nosto on 404 pages.
- The `currentVariation` prop is automatically detected and managed within the NostoProvider component. However, if you prefer to set it manually, you can simply pass the prop directly yourself.

Expand All @@ -97,14 +98,13 @@ The library uses [@nosto/nosto-react](https://github.com/Nosto/nosto-react) unde
import { NostoProvider } from "@nosto/shopify-hydrogen";

export default function App() {

const nonce = useNonce();
const data = useLoaderData();
const locale = data.selectedLocale ?? DEFAULT_LOCALE;


return (
...
<body>
<NostoProvider account="shopify-11368366139" recommendationComponent={<NostoSlot />}>
<NostoProvider account="shopify-11368366139" recommendationComponent={<NostoSlot />} nonce={nonce}>
<Layout>
<Outlet/>
</Layout>
Expand All @@ -118,7 +118,6 @@ export default function App() {
export function ErrorBoundary() {

const [root] = useMatches();
const locale = root?.data?.selectedLocale ?? DEFAULT_LOCALE;

return (
...
Expand All @@ -141,13 +140,13 @@ export function ErrorBoundary() {

```jsx
// Enable with automatic market and language detection:
<NostoProvider shopifyMarkets={true} account="shopify-11368366139" />
<NostoProvider shopifyMarkets={true} account="shopify-11368366139" nonce={nonce}/>

// Manually set only the language of the market:
<NostoProvider shopifyMarkets={{ language: "EN" }} account="shopify-11368366139" />
<NostoProvider shopifyMarkets={{ language: "EN" }} account="shopify-11368366139" nonce={nonce}/>

// Manually set both the language and ID of the market:
<NostoProvider shopifyMarkets={{ language: "EN", marketId: '123456789' }} account="shopify-11368366139" />
<NostoProvider shopifyMarkets={{ language: "EN", marketId: '123456789' }} account="shopify-11368366139" nonce={nonce}/>

```

Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"shopify",
"hydrogen"
],
"version": "2.2.3",
"version": "2.2.4",
"files": [
"dist",
"src"
Expand All @@ -24,15 +24,15 @@
".": "./src/index.js"
},
"dependencies": {
"@nosto/nosto-react": "^0.4.1",
"js-sha256": "^0.2.0"
"@nosto/nosto-react": "^2.2.2",
"crypto-es": "^2.1.0"
},
"peerDependencies": {
"@remix-run/react": "^1.19.1",
"@shopify/hydrogen": "^2023.4.5"
"@remix-run/react": "^2.10.3",
"@shopify/hydrogen": "^2024.7.4"
},
"bugs": {
"url": "https://github.com/Nosto/shopify-hydrogen/issues",
"homepage": "https://github.com/Nosto/shopify-hydrogen#readme"
}
}
}
6 changes: 6 additions & 0 deletions src/components/Nosto404.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNosto404 } from "@nosto/nosto-react"

export default function Nosto404(props) {
useNosto404(props)
return null
}
6 changes: 6 additions & 0 deletions src/components/NostoCategory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoCategory } from "@nosto/nosto-react"

export default function NostoCategory(props) {
useNostoCategory(props)
return null
}
6 changes: 6 additions & 0 deletions src/components/NostoCheckout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoCheckout } from "@nosto/nosto-react"

export default function NostoCheckout(props) {
useNostoCheckout(props)
return null
}
6 changes: 6 additions & 0 deletions src/components/NostoHome.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoHome } from "@nosto/nosto-react"

export default function NostoHome(props) {
useNostoHome(props)
return null
}
6 changes: 6 additions & 0 deletions src/components/NostoOrder.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoOrder } from "@nosto/nosto-react"

export default function NostoOrder(props) {
useNostoOrder(props)
return null
}
6 changes: 6 additions & 0 deletions src/components/NostoOther.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoOther } from "@nosto/nosto-react"

export default function NostoOther(props) {
useNostoOther(props)
return null
}
5 changes: 5 additions & 0 deletions src/components/NostoPlacement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NostoPlacement as NostoComponent } from "@nosto/nosto-react"

export default function NostoPlacement(props) {
return <NostoComponent {...props} />
}
10 changes: 10 additions & 0 deletions src/components/NostoProduct.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useNostoProduct } from "@nosto/nosto-react"

export default function NostoProduct(props) {
const { selectedVariant } = props.tagging
useNostoProduct({
product: props.product,
tagging: { product_id: props.product, selected_sku_id: selectedVariant?.sku }
})
return null
}
56 changes: 35 additions & 21 deletions src/components/NostoProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
import { NostoProvider as NostoComponent } from "@nosto/nosto-react";
import { NostoSession } from '@nosto/shopify-hydrogen'
import { useMatches } from '@remix-run/react'
import { parseGid } from '@shopify/hydrogen'
import { NostoProvider as NostoComponent } from "@nosto/nosto-react"
import { NostoSession } from "@nosto/shopify-hydrogen"
import { useMatches } from "@remix-run/react"
import { parseGid } from "@shopify/hydrogen"
import createScriptLoader from "../createScriptLoader.js";

export default function ({ children, shopifyMarkets: shopifyMarketsProp, ...props }) {
export default function ({
children,
shopifyMarkets: shopifyMarketsProp,
...props
}) {
const [root] = useMatches()
const { language } = root?.data?.selectedLocale || {}
const { market } = root?.data?.nostoProviderData?.localization?.country || {}

//Get nostoData from root remix loader:
const [root] = useMatches();
const { language } = root?.data?.selectedLocale || {}
const { market } = root?.data?.nostoProviderData?.localization?.country || {}
const currentVariation = props?.currentVariation || root?.data?.selectedLocale?.currency;
const { id: marketId } = parseGid(market?.id)
const scriptLoader = createScriptLoader(props.nonce)

const shopifyMarkets = {
marketId: shopifyMarketsProp?.marketId || marketId,
language: shopifyMarketsProp?.language || language
}
const currentVariation =
props?.currentVariation || root?.data?.selectedLocale?.currency
const { id: marketId } = parseGid(market?.id)

return (
<NostoComponent {...props} shopifyMarkets={shopifyMarkets} currentVariation={currentVariation} >
<NostoSession />
{children}
</NostoComponent>
)
const shopifyMarkets = {
marketId: shopifyMarketsProp?.marketId || marketId,
language: shopifyMarketsProp?.language || language,
}

return (
<>
<NostoComponent
{...props}
shopifyMarkets={shopifyMarkets}
currentVariation={currentVariation}
scriptLoader={scriptLoader}
>
<NostoSession/>
{children}
</NostoComponent>
</>
)
}
6 changes: 6 additions & 0 deletions src/components/NostoSearch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useNostoSearch } from "@nosto/nosto-react"

export default function NostoSearch(props) {
useNostoSearch(props)
return null
}
60 changes: 31 additions & 29 deletions src/components/NostoSession.jsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
import { NostoSession as NostoComponent } from "@nosto/nosto-react";
import { sha256 } from "js-sha256";
import { useMatches, Await, useAsyncValue } from "@remix-run/react";
import { Suspense } from "react";
import { NostoSession as NostoComponent } from "@nosto/nosto-react"
import crypto from "crypto-es"
import { Await, useAsyncValue, useMatches } from "@remix-run/react"

//Polyfill the Array.prototype.at() method for all browsers
if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, 'at', {
Object.defineProperty(Array.prototype, "at", {
value: function (index) {
if (index >= 0) {
return this[index];
return this[index]
} else {
return this[this.length + index];
return this[this.length + index]
}
},
enumerable: false,
configurable: true,
writable: true
});
writable: true,
})
}

function AsyncSessionWrapper() {

//Resolve async data:
const { customer: customerData = {}, cart: shopifyCart, storeDomain } = useAsyncValue() || {};
const {
customer: customerData = {},
cart: shopifyCart,
storeDomain,
} = useAsyncValue() || {}

//Get customer data to sync with Nosto:
let customerId = customerData?.id?.split('/').at(-1);
let customer_reference = customerId && storeDomain ? sha256(customerId + storeDomain) : undefined;
let customerId = customerData?.id?.split("/").at(-1)
let customer_reference =
customerId && storeDomain
? crypto.SHA256(customerId + storeDomain).toString()
: undefined
let customer = {
customer_reference,
first_name: customerData?.firstName || undefined,
last_name: customerData?.lastName || undefined,
email: customerData?.email || undefined,
newsletter: customerData?.acceptsMarketing ?? undefined
newsletter: customerData?.acceptsMarketing ?? undefined,
}

//Get cart data to sync with Nosto:
let items = shopifyCart?.lines?.edges?.map(item => item.node)
let nostoCart = items?.map(item => {
let items = shopifyCart?.lines?.edges?.map((item) => item.node)
let nostoCart = items?.map((item) => {
return {
product_id: item?.merchandise?.product.id.split("/")?.at(-1),
name: item?.merchandise?.product.title,
sku_id: item?.merchandise?.id.split("/")?.at(-1),
product_id: item?.merchandise?.product?.id.split("/")?.at(-1),
name: item?.merchandise?.product?.title,
sku_id: item?.merchandise?.id?.split("/")?.at(-1),
quantity: item?.quantity,
unit_price: +item?.merchandise?.price?.amount,
price_currency_code: item?.merchandise?.price?.currencyCode
price_currency_code: item?.merchandise?.price?.currencyCode,
}
})
let cart = nostoCart ? { items: nostoCart } : undefined;
let cart = nostoCart ? { items: nostoCart } : undefined

return <NostoComponent customer={customer} cart={cart} />
return <NostoComponent customer={customer} cart={cart}/>
}

export default function NostoSession() {

//Get nostoSessionData promise from root remix loader:
const [root] = useMatches();
const [root] = useMatches()
const nostoPromise = root?.data?.nostoSessionData

return (
<Suspense>
<Await resolve={nostoPromise}>
<AsyncSessionWrapper />
</Await>
</Suspense>
<Await resolve={nostoPromise}>
<AsyncSessionWrapper/>
</Await>
)
}
26 changes: 26 additions & 0 deletions src/createScriptLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Creates a custom script loader function which passed to nosto-react component NostoProvider as a prop
* and used in useLoadClientScript hook for loading scripts. This will override the default script loader behaviour
* since a `nonce` attribute is required by Shopify for third party scripts. Additionally, this lets the useLoadClientScript
* to handle building the URL string (scriptSrc in the returned function param).
*/

export default function (nonce) {
return function (scriptSrc, options) {
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.type = "text/javascript"
script.src = scriptSrc
script.async = true
script.setAttribute("nonce", nonce)
script.onload = () => resolve()
script.onerror = () => reject()
Object.entries(options?.attributes ?? {}).forEach(([k, v]) => script.setAttribute(k, v))
if (options?.position === "head") {
document.head.appendChild(script)
} else {
document.body.appendChild(script)
}
})
}
}
22 changes: 10 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@

//Export components:
export {
Nosto404,
NostoCategory,
NostoCheckout,
NostoHome,
NostoOrder,
NostoOther,
NostoPlacement,
NostoProduct,
NostoSearch
} from "@nosto/nosto-react"
export { default as NostoProvider } from "./components/NostoProvider.jsx";
export { default as Nosto404 } from "./components/Nosto404.jsx"
export { default as NostoCategory } from "./components/NostoCategory.jsx"
export { default as NostoCheckout } from "./components/NostoCheckout.jsx"
export { default as NostoHome } from "./components/NostoHome.jsx"
export { default as NostoOrder } from "./components/NostoOrder.jsx"
export { default as NostoOther } from "./components/NostoOther.jsx"
export { default as NostoPlacement } from "./components/NostoPlacement.jsx"
export { default as NostoProduct } from "./components/NostoProduct.jsx"
export { default as NostoSearch } from "./components/NostoSearch.jsx"
export { default as NostoProvider } from "./components/NostoProvider.jsx"
export { default as NostoSession } from "./components/NostoSession.jsx"

//Export server fetching function:
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# yarn lockfile v1


"@nosto/nosto-react@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@nosto/nosto-react/-/nosto-react-0.4.1.tgz#2c497d0df4d0c961ee6166d18cebbf70df46385d"
integrity sha512-vNlOCf1IZDX2gNUz4vcGP040Leyazqw6jNSiqY2txiuB/jMnz9f3ruDEPAiY6gjx3uOaItZFsDPoXUbffGCDtg==
"@nosto/nosto-react@^2.2.2":
version "2.2.2"
resolved "https://registry.yarnpkg.com/@nosto/nosto-react/-/nosto-react-2.2.2.tgz#bd70dba325abc6126b9b7bc4ac79b6aec6e25d1f"
integrity sha512-QQ8Lmrgz7iEKPne6r+GejIBQGx3IdY/95gRtPCvIIwWUa304KSCg0fgM0bHADfminTnUYmU5OmKon/SjIEpgiQ==

js-sha256@^0.2.0:
version "0.2.3"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.2.3.tgz#6da15baa3dd930be559d4be6473fb4ed859b48e6"
integrity sha512-QZgK5gMtVdatXbdzGVcJhmiPwwyRtGDBeIEZ1ZUpao4ev6Hhvgucp0jjqgb9ncgiiNaqvIGX7TW4952aoLkHbA==
crypto-es@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/crypto-es/-/crypto-es-2.1.0.tgz#1095f324ffd7dc1ccab8e21d0960e17025da8ce9"
integrity sha512-C5Dbuv4QTPGuloy5c5Vv/FZHtmK+lobLAypFfuRaBbwCsk3qbCWWESCH3MUcBsrgXloRNMrzwUAiPg4U6+IaKA==

0 comments on commit d0361e2

Please sign in to comment.