@nosto/nosto-react 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +5 -2
  2. package/dist/{index.es.client.js → index.es.js} +127 -168
  3. package/dist/index.umd.js +9 -0
  4. package/package.json +15 -10
  5. package/src/components/Nosto404.tsx +47 -0
  6. package/src/components/{Category/index.client.tsx → NostoCategory.tsx} +18 -38
  7. package/src/components/NostoCheckout.tsx +47 -0
  8. package/src/components/{Home/index.client.tsx → NostoHome.tsx} +17 -34
  9. package/src/components/{Order/index.client.tsx → NostoOrder.tsx} +22 -38
  10. package/src/components/NostoOther.tsx +46 -0
  11. package/src/components/{Placement/index.client.tsx → NostoPlacement.tsx} +5 -8
  12. package/src/components/{Product/index.client.tsx → NostoProduct.tsx} +37 -80
  13. package/src/components/NostoProvider.tsx +220 -0
  14. package/src/components/{Search/index.client.tsx → NostoSearch.tsx} +18 -36
  15. package/src/components/{Session/index.client.tsx → NostoSession.tsx} +14 -17
  16. package/src/components/context.ts +55 -0
  17. package/src/components/index.ts +14 -0
  18. package/src/index.ts +3 -0
  19. package/src/types.ts +112 -97
  20. package/src/utils/compare.ts +9 -9
  21. package/src/utils/hooks.ts +28 -8
  22. package/src/utils/object.ts +10 -11
  23. package/src/utils/snakeize.ts +11 -11
  24. package/dist/index.umd.client.js +0 -9
  25. package/src/components/Checkout/index.client.tsx +0 -64
  26. package/src/components/Fohofo/index.client.tsx +0 -64
  27. package/src/components/Other/index.client.tsx +0 -63
  28. package/src/components/Provider/context.client.ts +0 -45
  29. package/src/components/Provider/index.client.tsx +0 -222
  30. package/src/index.client.ts +0 -33
@@ -1,30 +1,30 @@
1
- import { isPlainObject } from "./object";
1
+ import { isPlainObject } from "./object"
2
2
 
3
3
  export function deepCompare(a: unknown, b: unknown): boolean {
4
4
  if (a === b) {
5
- return true;
5
+ return true
6
6
  }
7
7
 
8
8
  if (a instanceof Date && b instanceof Date) {
9
- return a.getTime() === b.getTime();
9
+ return a.getTime() === b.getTime()
10
10
  }
11
11
 
12
12
  if (a instanceof Array && b instanceof Array) {
13
13
  if (a.length !== b.length) {
14
- return false;
14
+ return false
15
15
  }
16
16
 
17
- return a.every((v, i) => deepCompare(v, b[i]));
17
+ return a.every((v, i) => deepCompare(v, b[i]))
18
18
  }
19
19
 
20
20
  if (isPlainObject(a) && isPlainObject(b)) {
21
- const entriesA = Object.entries(a);
21
+ const entriesA = Object.entries(a)
22
22
 
23
23
  if (entriesA.length !== Object.keys(b).length) {
24
- return false;
24
+ return false
25
25
  }
26
- return entriesA.every(([k, v]) => deepCompare(v, b[k]));
26
+ return entriesA.every(([k, v]) => deepCompare(v, b[k]))
27
27
  }
28
28
 
29
- return false;
29
+ return false
30
30
  }
@@ -1,21 +1,41 @@
1
- import { useEffect, useRef, useMemo } from "react";
2
- import { deepCompare } from "./compare";
1
+ import { useEffect, useRef, useMemo } from "react"
2
+ import { useNostoContext } from "../components/context"
3
+ import { deepCompare } from "./compare"
4
+ import { NostoClient } from "../types"
3
5
 
4
6
  export function useDeepCompareEffect(
5
7
  callback: Parameters<typeof useEffect>[0],
6
8
  dependencies: Parameters<typeof useEffect>[1]
7
9
  ): ReturnType<typeof useEffect> {
8
- return useEffect(callback, useDeepCompareMemoize(dependencies));
10
+ return useEffect(callback, useDeepCompareMemoize(dependencies))
9
11
  }
10
12
 
11
13
  function useDeepCompareMemoize<T>(value: T) {
12
- const ref = useRef<T>(value);
13
- const signalRef = useRef<number>(0);
14
+ const ref = useRef<T>(value)
15
+ const signalRef = useRef<number>(0)
14
16
 
15
17
  if (!deepCompare(value, ref.current)) {
16
- ref.current = value;
17
- signalRef.current += 1;
18
+ ref.current = value
19
+ signalRef.current += 1
18
20
  }
19
21
 
20
- return useMemo(() => ref.current, [signalRef.current]);
22
+ return useMemo(() => ref.current, [signalRef.current])
23
+ }
24
+
25
+ export function useNostoApi(
26
+ cb: (api: NostoClient) => void,
27
+ deps?: React.DependencyList,
28
+ flags?: { deep?: boolean }
29
+ ): void {
30
+ const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
31
+ const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
32
+
33
+ useEffectFn(() => {
34
+ if (clientScriptLoaded) {
35
+ window.nostojs(api => {
36
+ api.defaultSession().setVariation(currentVariation).setResponseMode(responseMode)
37
+ cb(api)
38
+ })
39
+ }
40
+ }, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
21
41
  }
@@ -1,20 +1,19 @@
1
- export function isPlainObject<T = Record<keyof any, unknown>>(
2
- value: unknown
3
- ): value is T {
4
- const isObject = (v: unknown): v is object => String(v) === "[object Object]";
1
+ const isObject = (v: unknown): v is object => String(v) === "[object Object]"
5
2
 
6
- if (!isObject(value)) return false;
3
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
4
+ if (!isObject(value)) return false
7
5
 
8
- const constructor = value.constructor;
9
- if (constructor === undefined) return true;
6
+ const constructor = value.constructor
7
+ if (constructor === undefined) return true
10
8
 
11
- const prototype = constructor.prototype;
12
- if (!isObject(prototype)) return false;
9
+ const prototype = constructor.prototype
10
+ if (!isObject(prototype)) return false
13
11
 
14
12
  // Checks if it is not a class
13
+ // eslint-disable-next-line no-prototype-builtins
15
14
  if (!prototype.hasOwnProperty("isPrototypeOf")) {
16
- return false;
15
+ return false
17
16
  }
18
17
 
19
- return true;
18
+ return true
20
19
  }
@@ -1,28 +1,28 @@
1
1
  export function snakeize<T>(obj: T): T {
2
2
  if (!obj || typeof obj !== "object") {
3
- return obj;
3
+ return obj
4
4
  }
5
5
  if (isDate(obj) || isRegex(obj)) {
6
- return obj;
6
+ return obj
7
7
  }
8
8
  if (Array.isArray(obj)) {
9
- return obj.map(snakeize) as T;
9
+ return obj.map(snakeize) as T
10
10
  }
11
11
  return Object.keys(obj).reduce((acc, key) => {
12
- var camel =
12
+ const camel =
13
13
  key[0].toLowerCase() +
14
14
  key.slice(1).replace(/([A-Z]+)/g, (_, x) => {
15
- return "_" + x.toLowerCase();
16
- });
17
- acc[camel as keyof typeof acc] = snakeize(obj[key as keyof typeof acc]);
18
- return acc;
19
- }, {} as T);
15
+ return "_" + x.toLowerCase()
16
+ })
17
+ acc[camel as keyof typeof acc] = snakeize(obj[key as keyof typeof acc])
18
+ return acc
19
+ }, {}) as T
20
20
  }
21
21
 
22
22
  function isDate(obj: unknown) {
23
- return Object.prototype.toString.call(obj) === "[object Date]";
23
+ return Object.prototype.toString.call(obj) === "[object Date]"
24
24
  }
25
25
 
26
26
  function isRegex(obj: unknown) {
27
- return Object.prototype.toString.call(obj) === "[object RegExp]";
27
+ return Object.prototype.toString.call(obj) === "[object RegExp]"
28
28
  }
@@ -1,9 +0,0 @@
1
- (function(u,m){typeof exports=="object"&&typeof module!="undefined"?m(exports,require("react"),require("react-dom/client")):typeof define=="function"&&define.amd?define(["exports","react","react-dom/client"],m):(u=typeof globalThis!="undefined"?globalThis:u||self,m(u["@nosto/nosto-react"]={},u.React,u.ReactDOM))})(this,function(u,m,x){"use strict";function D(t){return t&&typeof t=="object"&&"default"in t?t:{default:t}}var R=D(m);const V=m.createContext({account:"",currentVariation:"",pageType:"",responseMode:"HTML",clientScriptLoaded:!1,useRenderCampaigns:()=>{}});function h(){const t=m.useContext(V);if(!t)throw new Error("No nosto context found");return t}var E={exports:{}},b={};/**
2
- * @license React
3
- * react-jsx-runtime.production.min.js
4
- *
5
- * Copyright (c) Facebook, Inc. and its affiliates.
6
- *
7
- * This source code is licensed under the MIT license found in the
8
- * LICENSE file in the root directory of this source tree.
9
- */var H=R.default,$=Symbol.for("react.element"),G=Symbol.for("react.fragment"),z=Object.prototype.hasOwnProperty,J=H.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,W={key:!0,ref:!0,__self:!0,__source:!0};function A(t,n,e){var r,c={},d=null,i=null;e!==void 0&&(d=""+e),n.key!==void 0&&(d=""+n.key),n.ref!==void 0&&(i=n.ref);for(r in n)z.call(n,r)&&!W.hasOwnProperty(r)&&(c[r]=n[r]);if(t&&t.defaultProps)for(r in n=t.defaultProps,n)c[r]===void 0&&(c[r]=n[r]);return{$$typeof:$,type:t,key:d,ref:i,props:c,_owner:J.current}}b.Fragment=G,b.jsx=A,b.jsxs=A,E.exports=b;const s=E.exports.jsx,w=E.exports.jsxs,_=E.exports.Fragment;function B(){const{clientScriptLoaded:t,currentVariation:n,responseMode:e,recommendationComponent:r,useRenderCampaigns:c}=h(),{renderCampaigns:d,pageTypeUpdated:i}=c("404");return m.useEffect(()=>{t&&i&&window.nostojs(a=>{a.defaultSession().setVariation(n).setResponseMode(e).viewNotFound().setPlacements(a.placements.getPlacements()).load().then(p=>{d(p,a)})})},[t,n,r,i]),s(_,{children:s("div",{className:"nosto_page_type",style:{display:"none"},children:"notfound"})})}function Y(){const{clientScriptLoaded:t,currentVariation:n,responseMode:e,recommendationComponent:r,useRenderCampaigns:c}=h(),{renderCampaigns:d,pageTypeUpdated:i}=c("other");return m.useEffect(()=>{t&&i&&window.nostojs(a=>{a.defaultSession().setVariation(n).setResponseMode(e).viewOther().setPlacements(a.placements.getPlacements()).load().then(p=>{d(p,a)})})},[t,n,r,i]),s(_,{children:s("div",{className:"nosto_page_type",style:{display:"none"},children:"other"})})}function Z(){const{clientScriptLoaded:t,currentVariation:n,responseMode:e,recommendationComponent:r,useRenderCampaigns:c}=h(),{renderCampaigns:d,pageTypeUpdated:i}=c("checkout");return m.useEffect(()=>{t&&i&&window.nostojs(a=>{a.defaultSession().setVariation(n).setResponseMode(e).viewCart().setPlacements(a.placements.getPlacements()).load().then(p=>{d(p,a)})})},[t,n,r,i]),s(_,{children:s("div",{className:"nosto_page_type",style:{display:"none"},children:"cart"})})}function F(t){const n=c=>String(c)==="[object Object]";if(!n(t))return!1;const e=t.constructor;if(e===void 0)return!0;const r=e.prototype;return!(!n(r)||!r.hasOwnProperty("isPrototypeOf"))}function U(t,n){if(t===n)return!0;if(t instanceof Date&&n instanceof Date)return t.getTime()===n.getTime();if(t instanceof Array&&n instanceof Array)return t.length!==n.length?!1:t.every((e,r)=>U(e,n[r]));if(F(t)&&F(n)){const e=Object.entries(t);return e.length!==Object.keys(n).length?!1:e.every(([r,c])=>U(c,n[r]))}return!1}function I(t,n){return m.useEffect(t,K(n))}function K(t){const n=m.useRef(t),e=m.useRef(0);return U(t,n.current)||(n.current=t,e.current+=1),m.useMemo(()=>n.current,[e.current])}function Q(t){const{product:n,tagging:e}=t,{clientScriptLoaded:r,currentVariation:c,responseMode:d,recommendationComponent:i,useRenderCampaigns:a}=h(),{renderCampaigns:p,pageTypeUpdated:f}=a("product");return I(()=>{r&&f&&window.nostojs(o=>{o.defaultSession().setResponseMode(d).viewProduct(n).setPlacements(o.placements.getPlacements()).load().then(y=>{p(y,o)})})},[r,c,n,i,f]),w(_,{children:[s("div",{className:"nosto_page_type",style:{display:"none"},children:"product"}),w("div",{className:"nosto_product",style:{display:"none"},children:[(e==null?void 0:e.variationId)&&s("span",{className:"variation_id",children:e.variationId}),n&&s("span",{className:"product_id",children:n}),(e==null?void 0:e.name)&&s("span",{className:"name",children:e.name}),(e==null?void 0:e.url)&&s("span",{className:"url",children:e.url.toString()}),(e==null?void 0:e.imageUrl)&&s("span",{className:"image_url",children:e.imageUrl.toString()}),(e==null?void 0:e.availability)&&s("span",{className:"availability",children:e.availability}),(e==null?void 0:e.price)&&s("span",{className:"price",children:e.price}),(e==null?void 0:e.listPrice)&&s("span",{className:"list_price",children:e.listPrice}),(e==null?void 0:e.priceCurrencyCode)&&s("span",{className:"price_currency_code",children:e.priceCurrencyCode}),(e==null?void 0:e.brand)&&s("span",{className:"brand",children:e.brand}),(e==null?void 0:e.description)&&s("span",{className:"description",children:e.description}),(e==null?void 0:e.googleCategory)&&s("span",{className:"description",children:e.googleCategory}),(e==null?void 0:e.condition)&&s("span",{className:"condition",children:e.condition}),(e==null?void 0:e.gender)&&s("span",{className:"gender",children:e.gender}),(e==null?void 0:e.ageGroup)&&s("span",{className:"age_group",children:e.ageGroup}),(e==null?void 0:e.gtin)&&s("span",{className:"gtin",children:e.gtin}),(e==null?void 0:e.category)&&(e==null?void 0:e.category.map((o,y)=>s("span",{className:"category",children:o},y))),(e==null?void 0:e.tags1)&&e.tags1.map((o,y)=>s("span",{className:"tag1",children:o},y)),(e==null?void 0:e.tags2)&&e.tags2.map((o,y)=>s("span",{className:"tag2",children:o},y)),(e==null?void 0:e.tags3)&&e.tags3.map((o,y)=>s("span",{className:"tag3",children:o},y)),(e==null?void 0:e.ratingValue)&&s("span",{className:"rating_value",children:e.ratingValue}),(e==null?void 0:e.reviewCount)&&s("span",{className:"review_count",children:e.reviewCount}),(e==null?void 0:e.alternateImageUrls)&&e.alternateImageUrls.map((o,y)=>s("span",{className:"alternate_image_url",children:o.toString()},y)),(e==null?void 0:e.customFields)&&Object.keys(e.customFields).map((o,y)=>e.customFields&&e.customFields[o]&&s("span",{className:o,children:e.customFields[o]},y)),(e==null?void 0:e.skus)&&e.skus.map((o,y)=>w("span",{className:"nosto_sku",children:[(o==null?void 0:o.id)&&s("span",{className:"product_id",children:o.id}),(o==null?void 0:o.name)&&s("span",{className:"name",children:o.name}),(o==null?void 0:o.price)&&s("span",{className:"price",children:o.price}),(o==null?void 0:o.listPrice)&&s("span",{className:"list_price",children:o.listPrice}),(o==null?void 0:o.url)&&s("span",{className:"url",children:o.url.toString()}),(o==null?void 0:o.imageUrl)&&s("span",{className:"image_url",children:o.imageUrl.toString()}),(o==null?void 0:o.gtin)&&s("span",{className:"gtin",children:o.gtin}),(o==null?void 0:o.availability)&&s("span",{className:"availability",children:o.availability}),(o==null?void 0:o.customFields)&&Object.keys(o.customFields).map((j,O)=>o.customFields&&o.customFields[j]&&s("span",{className:j,children:o.customFields[j]},O))]},y))]})]})}function X(t){const{category:n}=t,{clientScriptLoaded:e,currentVariation:r,responseMode:c,recommendationComponent:d,useRenderCampaigns:i}=h(),{renderCampaigns:a,pageTypeUpdated:p}=i("home");return m.useEffect(()=>{e&&p&&window.nostojs(f=>{f.defaultSession().setVariation(r).setResponseMode(c).viewCategory(n).setPlacements(f.placements.getPlacements()).load().then(o=>{a(o,f)})})},[e,n,r,d,p]),w(_,{children:[s("div",{className:"nosto_page_type",style:{display:"none"},children:"category"}),s("div",{className:"nosto_category",style:{display:"none"},children:n})]})}function g(t){const{query:n}=t,{clientScriptLoaded:e,currentVariation:r,responseMode:c,recommendationComponent:d,useRenderCampaigns:i}=h(),{renderCampaigns:a,pageTypeUpdated:p}=i("search");return m.useEffect(()=>{e&&p&&window.nostojs(f=>{f.defaultSession().setVariation(r).setResponseMode(c).viewSearch(n).setPlacements(f.placements.getPlacements()).load().then(o=>{a(o,f)})})},[e,r,n,d,p]),w(_,{children:[s("div",{className:"nosto_page_type",style:{display:"none"},children:"search"}),s("div",{className:"nosto_search",style:{display:"none"},children:n})]})}function P(t){return!t||typeof t!="object"||k(t)||ee(t)?t:Array.isArray(t)?t.map(P):Object.keys(t).reduce((n,e)=>{var r=e[0].toLowerCase()+e.slice(1).replace(/([A-Z]+)/g,(c,d)=>"_"+d.toLowerCase());return n[r]=P(t[e]),n},{})}function k(t){return Object.prototype.toString.call(t)==="[object Date]"}function ee(t){return Object.prototype.toString.call(t)==="[object RegExp]"}function te(t){const{order:n}=t,{clientScriptLoaded:e,currentVariation:r,responseMode:c,recommendationComponent:d,useRenderCampaigns:i}=h(),{renderCampaigns:a,pageTypeUpdated:p}=i("order");return m.useEffect(()=>{e&&p&&window.nostojs(f=>{f.defaultSession().setVariation(r).setResponseMode(c).addOrder(P(n)).setPlacements(f.placements.getPlacements()).load().then(o=>{a(o,f)})})},[e,r,d,p]),w(_,{children:[s("div",{className:"nosto_page_type",style:{display:"none"},children:"order"}),s("div",{className:"nosto_order",style:{display:"none"},children:n.purchase.number})]})}function ne(){const{clientScriptLoaded:t,currentVariation:n,responseMode:e,recommendationComponent:r,useRenderCampaigns:c}=h(),{renderCampaigns:d,pageTypeUpdated:i}=c("home");return m.useEffect(()=>{t&&i&&window.nostojs(a=>{a.defaultSession().setVariation(n).setResponseMode(e).viewFrontPage().setPlacements(a.placements.getPlacements()).load().then(p=>{d(p,a)})})},[t,n,r,i]),s(_,{children:s("div",{className:"nosto_page_type",style:{display:"none"},children:"front"})})}function oe(t){const{id:n,pageType:e}=t;return s("div",{className:"nosto_element",id:n},n+(e||""))}function se(t){let{account:n,currentVariation:e="",multiCurrency:r=!1,host:c,children:d,recommendationComponent:i,shopifyMarkets:a}=t;const[p,f]=R.default.useState(!1),o=R.default.useMemo(()=>p,[p]);e=r?e:"";const y=m.isValidElement(i)?"JSON_ORIGINAL":"HTML";function j(v){return R.default.cloneElement(i,{nostoRecommendation:v.nostoRecommendation})}const[O,ae]=m.useState(""),ce=function(v=""){const S=m.useRef({});m.useEffect(()=>{O!=v&&ae(v)},[]);const M=v==O;function l(N,C){if(y=="HTML")C.placements.injectCampaigns(N.recommendations);else{const L=N.campaigns.recommendations;for(const T in L){let ie=L[T],de="#"+T,q=()=>document.querySelector(de);q()&&(S.current[T]||(S.current[T]=x.createRoot(q())),S.current[T].render(s(j,{nostoRecommendation:ie})))}}}return{renderCampaigns:l,pageTypeUpdated:M}};return m.useEffect(()=>{var v,S,M;if(window.nostojs||(window.nostojs=l=>{(window.nostojs.q=window.nostojs.q||[]).push(l)},window.nostojs(l=>l.setAutoLoad(!1))),!document.querySelectorAll("[nosto-client-script]").length&&!a){const l=document.createElement("script");l.type="text/javascript",l.src="//"+(c||"connect.nosto.com")+"/include/"+n,l.async=!0,l.setAttribute("nosto-client-script",""),l.onload=()=>{var N;typeof jest!="undefined"&&((N=window.nosto)==null||N.reload({site:"localhost"})),f(!0)},document.body.appendChild(l)}if(a){const l=document.querySelector("[nosto-client-script]"),N=document.querySelector("#nosto-sandbox");if(!l||(l==null?void 0:l.getAttribute("nosto-language"))!=(a==null?void 0:a.language)||(l==null?void 0:l.getAttribute("nosto-market-id"))!=(a==null?void 0:a.marketId)){p&&f(!1),(v=l==null?void 0:l.parentNode)==null||v.removeChild(l),(S=N==null?void 0:N.parentNode)==null||S.removeChild(N);const C=document.createElement("script");C.type="text/javascript",C.src="//"+(c||"connect.nosto.com")+`/script/shopify/market/nosto.js?merchant=${n}&market=${a.marketId||""}&locale=${((M=a==null?void 0:a.language)==null?void 0:M.toLowerCase())||""}`,C.async=!0,C.setAttribute("nosto-client-script",""),C.setAttribute("nosto-language",(a==null?void 0:a.language)||""),C.setAttribute("nosto-market-id",String(a==null?void 0:a.marketId)),C.onload=()=>{var L;typeof jest!="undefined"&&((L=window.nosto)==null||L.reload({site:"localhost"})),f(!0)},document.body.appendChild(C)}}},[p,a]),s(V.Provider,{value:{account:n,clientScriptLoaded:o,currentVariation:e,responseMode:y,recommendationComponent:i,useRenderCampaigns:ce,pageType:O},children:d})}function re(t){const{cart:n,customer:e}=t,{clientScriptLoaded:r}=h();return I(()=>{const c=n?P(n):void 0,d=e?P(e):void 0;r&&window.nostojs(i=>{i.defaultSession().setResponseMode("HTML").setCart(c).setCustomer(d).viewOther().load()})},[r,n,e]),s(_,{})}u.Nosto404=B,u.NostoCategory=X,u.NostoCheckout=Z,u.NostoContext=V,u.NostoHome=ne,u.NostoOrder=te,u.NostoOther=Y,u.NostoPlacement=oe,u.NostoProduct=Q,u.NostoProvider=se,u.NostoSearch=g,u.NostoSession=re,u.useNostoContext=h,Object.defineProperties(u,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
@@ -1,64 +0,0 @@
1
- import { useEffect } from "react";
2
- import { useNostoContext } from "../Provider/context.client";
3
-
4
- /**
5
- * You can personalise your cart and checkout pages by using the NostoCheckout component.
6
- * The component does not require any props.
7
- *
8
- * By default, your account, when created, has two cart-page placements named `categorypage-nosto-1` and `categorypage-nosto-2`.
9
- * You may omit these and use any identifier you need.
10
- * The identifiers used here are simply provided to illustrate the example.
11
- *
12
- * @example
13
- * ```
14
- * <div className="checkout-page">
15
- * <NostoPlacement id="checkout-nosto-1" />
16
- * <NostoPlacement id="checkout-nosto-2" />
17
- * <NostoCheckout />
18
- * </div>
19
- * ```
20
- *
21
- * @group Personalisation Components
22
- */
23
-
24
- export default function NostoCheckout(): JSX.Element {
25
- const {
26
- clientScriptLoaded,
27
- currentVariation,
28
- responseMode,
29
- recommendationComponent,
30
- useRenderCampaigns,
31
- } = useNostoContext();
32
-
33
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("checkout");
34
-
35
- useEffect(() => {
36
- if (clientScriptLoaded && pageTypeUpdated) {
37
- window.nostojs((api) => {
38
- api
39
- .defaultSession()
40
- .setVariation(currentVariation)
41
- .setResponseMode(responseMode)
42
- .viewCart()
43
- .setPlacements(api.placements.getPlacements())
44
- .load()
45
- .then((data) => {
46
- renderCampaigns(data, api);
47
- });
48
- });
49
- }
50
- }, [
51
- clientScriptLoaded,
52
- currentVariation,
53
- recommendationComponent,
54
- pageTypeUpdated,
55
- ]);
56
-
57
- return (
58
- <>
59
- <div className="nosto_page_type" style={{ display: "none" }}>
60
- cart
61
- </div>
62
- </>
63
- );
64
- }
@@ -1,64 +0,0 @@
1
- import { useEffect } from "react";
2
- import { useNostoContext } from "../Provider/context.client";
3
-
4
- /**
5
- * You can personalise your cart and checkout pages by using the `Nosto404` component.
6
- * The component does not require any props.
7
- *
8
- * By default, your account, when created, has three 404-page placements named `notfound-nosto-1`, `notfound-nosto-2` and `notfound-nosto-3`.
9
- * You may omit these and use any identifier you need.
10
- * The identifiers used here are simply provided to illustrate the example.
11
- *
12
- * @example
13
- * ```
14
- * <div className="notfound-page">
15
- * <NostoPlacement id="notfound-nosto-1" />
16
- * <NostoPlacement id="notfound-nosto-2" />
17
- * <NostoPlacement id="notfound-nosto-3" />
18
- * <Nosto404 />
19
- * </div>
20
- * ```
21
- *
22
- * @group Personalisation Components
23
- */
24
- export default function Nosto404(): JSX.Element {
25
- const {
26
- clientScriptLoaded,
27
- currentVariation,
28
- responseMode,
29
- recommendationComponent,
30
- useRenderCampaigns,
31
- } = useNostoContext();
32
-
33
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("404");
34
-
35
- useEffect(() => {
36
- if (clientScriptLoaded && pageTypeUpdated) {
37
- window.nostojs((api) => {
38
- api
39
- .defaultSession()
40
- .setVariation(currentVariation)
41
- .setResponseMode(responseMode)
42
- .viewNotFound()
43
- .setPlacements(api.placements.getPlacements())
44
- .load()
45
- .then((data) => {
46
- renderCampaigns(data, api);
47
- });
48
- });
49
- }
50
- }, [
51
- clientScriptLoaded,
52
- currentVariation,
53
- recommendationComponent,
54
- pageTypeUpdated,
55
- ]);
56
-
57
- return (
58
- <>
59
- <div className="nosto_page_type" style={{ display: "none" }}>
60
- notfound
61
- </div>
62
- </>
63
- );
64
- }
@@ -1,63 +0,0 @@
1
- import React, { useEffect } from "react";
2
- import { useNostoContext } from "../Provider/context.client";
3
-
4
- /**
5
- * You can personalise your miscellaneous pages by using the NostoOther component.
6
- * The component does not require any props.
7
- *
8
- * By default, your account, when created, has two other-page placements named `other-nosto-1` and `other-nosto-2`.
9
- * You may omit these and use any identifier you need.
10
- * The identifiers used here are simply provided to illustrate the example.
11
- *
12
- * @example
13
- * ```
14
- * <div className="other-page">
15
- * <NostoPlacement id="other-nosto-1" />
16
- * <NostoPlacement id="other-nosto-2" />
17
- * <NostoOther />
18
- * </div>;
19
- * ```
20
- *
21
- * @group Personalisation Components
22
- */
23
- export default function NostoOther(): JSX.Element {
24
- const {
25
- clientScriptLoaded,
26
- currentVariation,
27
- responseMode,
28
- recommendationComponent,
29
- useRenderCampaigns,
30
- } = useNostoContext();
31
-
32
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("other");
33
-
34
- useEffect(() => {
35
- if (clientScriptLoaded && pageTypeUpdated) {
36
- window.nostojs((api) => {
37
- api
38
- .defaultSession()
39
- .setVariation(currentVariation)
40
- .setResponseMode(responseMode)
41
- .viewOther()
42
- .setPlacements(api.placements.getPlacements())
43
- .load()
44
- .then((data) => {
45
- renderCampaigns(data, api);
46
- });
47
- });
48
- }
49
- }, [
50
- clientScriptLoaded,
51
- currentVariation,
52
- recommendationComponent,
53
- pageTypeUpdated,
54
- ]);
55
-
56
- return (
57
- <>
58
- <div className="nosto_page_type" style={{ display: "none" }}>
59
- other
60
- </div>
61
- </>
62
- );
63
- }
@@ -1,45 +0,0 @@
1
- import { createContext, useContext } from "react";
2
- import { Recommendation } from "../../types";
3
-
4
- /**
5
- * @group Types
6
- */
7
- export interface NostoContextType {
8
- account: string;
9
- clientScriptLoaded: boolean;
10
- currentVariation: string;
11
- renderFunction?: Function;
12
- responseMode: string;
13
- recommendationComponent?: React.ReactElement<{
14
- nostoRecommendation: Recommendation;
15
- }>;
16
- useRenderCampaigns: Function;
17
- pageType: string;
18
- }
19
-
20
- /**
21
- * @group Essential Functions
22
- */
23
- export const NostoContext = createContext<NostoContextType>({
24
- account: "",
25
- currentVariation: "",
26
- pageType: "",
27
- responseMode: "HTML",
28
- clientScriptLoaded: false,
29
- useRenderCampaigns: () => undefined,
30
- });
31
-
32
- /**
33
- * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
34
- *
35
- * @group Essential Functions
36
- */
37
- export function useNostoContext(): NostoContextType {
38
- const context = useContext(NostoContext);
39
-
40
- if (!context) {
41
- throw new Error("No nosto context found");
42
- }
43
-
44
- return context;
45
- }
@@ -1,222 +0,0 @@
1
- import React, { useEffect, isValidElement, useState, useRef } from "react";
2
- import { NostoContext } from "./context.client";
3
- import { createRoot } from "react-dom/client";
4
- import { Recommendation } from "../../types";
5
-
6
- /**
7
- * This widget is what we call the Nosto root widget, which is responsible for adding the actual Nosto script and the JS API stub.
8
- * This widget wraps all other React Nosto widgets.
9
- *
10
- * ```
11
- * <NostoProvider account="your-nosto-account-id" recommendationComponent={<NostoSlot />}>
12
- * <App />
13
- * </NostoProvider>
14
- * ```
15
- *
16
- * **Note:** the component also accepts a prop to configure the host `host="connect.nosto.com"`.
17
- * In advanced use-cases, the need to configure the host may surface.
18
- *
19
- * In order to implement client-side rendering, the requires a designated component to render the recommendations provided by Nosto.
20
- * This component should be capable of processing the JSON response received from our backend.
21
- * Notice the `recommendationComponent` prop passed to `<NostoProvider>` above.
22
- *
23
- * Learn more [here](https://github.com/Nosto/shopify-hydrogen/blob/main/README.md#client-side-rendering-for-recommendations) and see a [live example](https://github.com/Nosto/shopify-hydrogen-demo) on our demo store.
24
- *
25
- * @group Essential Functions
26
- */
27
- export default function NostoProvider(props: {
28
- /**
29
- * Indicates merchant id
30
- */
31
- account: string;
32
- /**
33
- * Indicates currency
34
- */
35
- currentVariation?: string;
36
- /**
37
- * Indicates an url of a server
38
- */
39
- host?: string;
40
- children: React.ReactElement;
41
- /**
42
- * Indicates if merchant uses multiple currencies
43
- */
44
- multiCurrency?: boolean;
45
- /**
46
- * Recommendation component which holds nostoRecommendation object
47
- */
48
- recommendationComponent?: any;
49
- /**
50
- * Enables Shopify markets with language and market id
51
- */
52
- shopifyMarkets?: {
53
- language?: string;
54
- marketId?: string | number;
55
- }
56
- }): JSX.Element {
57
- let {
58
- account,
59
- currentVariation = "",
60
- multiCurrency = false,
61
- host,
62
- children,
63
- recommendationComponent,
64
- shopifyMarkets
65
- } = props;
66
- const [clientScriptLoadedState, setClientScriptLoadedState] =
67
- React.useState(false);
68
- const clientScriptLoaded = React.useMemo(
69
- () => clientScriptLoadedState,
70
- [clientScriptLoadedState]
71
- );
72
-
73
- //Pass currentVariation as empty string if multiCurrency is disabled
74
- currentVariation = multiCurrency ? currentVariation : "";
75
-
76
- // Set responseMode for loading campaigns:
77
- const responseMode = isValidElement(recommendationComponent)
78
- ? "JSON_ORIGINAL"
79
- : "HTML";
80
-
81
- // RecommendationComponent for client-side rendering:
82
- function RecommendationComponentWrapper(props: {
83
- nostoRecommendation: Recommendation;
84
- }) {
85
- return React.cloneElement(recommendationComponent!, {
86
- nostoRecommendation: props.nostoRecommendation,
87
- });
88
- }
89
-
90
- // custom hook for rendering campaigns (CSR/SSR):
91
- const [pageType, setPageType] = useState("");
92
- const useRenderCampaigns: any = function (type: string = "") {
93
- const placementRefs: any = useRef({});
94
- useEffect(() => {
95
- if (pageType != type) {
96
- setPageType(type);
97
- }
98
- }, []);
99
-
100
- const pageTypeUpdated = type == pageType;
101
-
102
- function renderCampaigns(
103
- data: {
104
- recommendations: any;
105
- campaigns: {
106
- recommendations: {
107
- [key: string]: any;
108
- };
109
- };
110
- },
111
- api: {
112
- placements: {
113
- injectCampaigns: (recommendations: any) => void;
114
- };
115
- }
116
- ) {
117
- if (responseMode == "HTML") {
118
- // inject content campaigns as usual:
119
- api.placements.injectCampaigns(data.recommendations);
120
- } else {
121
- // render recommendation component into placements:
122
- const recommendations = data.campaigns.recommendations;
123
- for (const key in recommendations) {
124
- let recommendation = recommendations[key];
125
- let placementSelector = "#" + key;
126
- let placement: Function = () =>
127
- document.querySelector(placementSelector);
128
-
129
- if (!!placement()) {
130
- if (!placementRefs.current[key])
131
- placementRefs.current[key] = createRoot(placement());
132
- const root = placementRefs.current[key];
133
- root.render(
134
- <RecommendationComponentWrapper
135
- nostoRecommendation={recommendation}
136
- ></RecommendationComponentWrapper>
137
- );
138
- }
139
- }
140
- }
141
- }
142
- return { renderCampaigns, pageTypeUpdated };
143
- };
144
-
145
-
146
- useEffect(() => {
147
- if (!window.nostojs) {
148
- window.nostojs = (cb: Function) => {
149
- (window.nostojs.q = window.nostojs.q || []).push(cb);
150
- };
151
- window.nostojs((api) => api.setAutoLoad(false));
152
- }
153
-
154
- if (!document.querySelectorAll("[nosto-client-script]").length && !shopifyMarkets) {
155
- const script = document.createElement("script");
156
- script.type = "text/javascript";
157
- script.src = "//" + (host || "connect.nosto.com") + "/include/" + account;
158
- script.async = true;
159
- script.setAttribute("nosto-client-script", "");
160
-
161
- script.onload = () => {
162
- if (typeof jest !== "undefined") {
163
- window.nosto?.reload({
164
- site: "localhost",
165
- });
166
- }
167
- setClientScriptLoadedState(true);
168
- };
169
- document.body.appendChild(script);
170
- }
171
-
172
- //Enable Shopify markets functionality:
173
- if (!!shopifyMarkets) {
174
-
175
- const existingScript = document.querySelector("[nosto-client-script]");
176
- const nostoSandbox = document.querySelector('#nosto-sandbox');
177
-
178
- if (!existingScript || existingScript?.getAttribute('nosto-language') != shopifyMarkets?.language || existingScript?.getAttribute('nosto-market-id') != shopifyMarkets?.marketId) {
179
- if (clientScriptLoadedState) { setClientScriptLoadedState(false) };
180
-
181
- existingScript?.parentNode?.removeChild(existingScript)
182
- nostoSandbox?.parentNode?.removeChild(nostoSandbox)
183
-
184
- const script = document.createElement("script");
185
- script.type = "text/javascript";
186
- script.src = "//" + (host || "connect.nosto.com") + `/script/shopify/market/nosto.js?merchant=${account}&market=${shopifyMarkets.marketId || ''}&locale=${shopifyMarkets?.language?.toLowerCase() || ''}`
187
- script.async = true;
188
- script.setAttribute("nosto-client-script", "");
189
- script.setAttribute("nosto-language", shopifyMarkets?.language || '');
190
- script.setAttribute("nosto-market-id", String(shopifyMarkets?.marketId));
191
-
192
- script.onload = () => {
193
- if (typeof jest !== "undefined") {
194
- window.nosto?.reload({
195
- site: "localhost",
196
- });
197
- }
198
- setClientScriptLoadedState(true);
199
- };
200
- document.body.appendChild(script);
201
- }
202
-
203
- }
204
-
205
- }, [clientScriptLoadedState, shopifyMarkets]);
206
-
207
- return (
208
- <NostoContext.Provider
209
- value={{
210
- account,
211
- clientScriptLoaded,
212
- currentVariation,
213
- responseMode,
214
- recommendationComponent,
215
- useRenderCampaigns,
216
- pageType,
217
- }}
218
- >
219
- {children}
220
- </NostoContext.Provider>
221
- );
222
- }