@nosto/nosto-react 1.0.0 → 2.1.0
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.
- package/dist/index.d.ts +103 -755
- package/dist/index.es.js +563 -598
- package/dist/index.umd.js +9 -9
- package/package.json +10 -19
- package/src/components/Nosto404.tsx +4 -9
- package/src/components/NostoCategory.tsx +4 -7
- package/src/components/NostoCheckout.tsx +4 -9
- package/src/components/NostoHome.tsx +4 -8
- package/src/components/NostoOrder.tsx +5 -7
- package/src/components/NostoOther.tsx +4 -9
- package/src/components/NostoPlacement.tsx +0 -2
- package/src/components/NostoProduct.tsx +4 -8
- package/src/components/NostoProvider.tsx +26 -138
- package/src/components/NostoSearch.tsx +4 -7
- package/src/components/NostoSession.tsx +1 -2
- package/src/components/helpers.ts +3 -0
- package/src/components/index.ts +1 -4
- package/src/context.ts +31 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/scriptLoader.ts +30 -0
- package/src/hooks/useDeepCompareEffect.ts +21 -0
- package/src/hooks/useLoadClientScript.ts +84 -0
- package/src/hooks/useNostoApi.ts +22 -0
- package/src/hooks/useNostoContext.ts +12 -0
- package/src/hooks/useRenderCampaigns.tsx +59 -0
- package/src/index.ts +3 -0
- package/src/types.ts +73 -1
- package/src/components/context.ts +0 -55
- package/src/utils/hooks.ts +0 -41
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { NostoContext } from "
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { isValidElement } from "react"
|
|
2
|
+
import { NostoContext, RecommendationComponent } from "../context"
|
|
3
|
+
import { useLoadClientScript } from "../hooks"
|
|
4
|
+
import type { ReactElement } from "react"
|
|
5
|
+
import { ScriptLoadOptions } from "../hooks/scriptLoader"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @group Components
|
|
@@ -19,7 +20,10 @@ export interface NostoProviderProps {
|
|
|
19
20
|
* Indicates an url of a server
|
|
20
21
|
*/
|
|
21
22
|
host?: string
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* children
|
|
25
|
+
*/
|
|
26
|
+
children: ReactElement | ReactElement[]
|
|
23
27
|
/**
|
|
24
28
|
* Indicates if merchant uses multiple currencies
|
|
25
29
|
*/
|
|
@@ -27,9 +31,7 @@ export interface NostoProviderProps {
|
|
|
27
31
|
/**
|
|
28
32
|
* Recommendation component which holds nostoRecommendation object
|
|
29
33
|
*/
|
|
30
|
-
recommendationComponent?:
|
|
31
|
-
nostoRecommendation: Recommendation
|
|
32
|
-
}>
|
|
34
|
+
recommendationComponent?: RecommendationComponent
|
|
33
35
|
/**
|
|
34
36
|
* Enables Shopify markets with language and market id
|
|
35
37
|
*/
|
|
@@ -37,6 +39,14 @@ export interface NostoProviderProps {
|
|
|
37
39
|
language?: string
|
|
38
40
|
marketId?: string | number
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Load nosto script (should be false if loading the script outside of nosto-react)
|
|
44
|
+
*/
|
|
45
|
+
loadScript?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Custom script loader
|
|
48
|
+
*/
|
|
49
|
+
scriptLoader?: (scriptSrc: string, options?: ScriptLoadOptions) => Promise<void>
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/**
|
|
@@ -64,143 +74,23 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
64
74
|
const {
|
|
65
75
|
account,
|
|
66
76
|
multiCurrency = false,
|
|
67
|
-
host,
|
|
68
77
|
children,
|
|
69
78
|
recommendationComponent,
|
|
70
|
-
shopifyMarkets,
|
|
71
79
|
} = props
|
|
72
|
-
const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
|
|
73
|
-
const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
|
|
74
80
|
|
|
75
81
|
// Pass currentVariation as empty string if multiCurrency is disabled
|
|
76
82
|
const currentVariation = multiCurrency ? props.currentVariation : ""
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
function RecommendationComponentWrapper(props: { nostoRecommendation: Recommendation }) {
|
|
83
|
-
return React.cloneElement(recommendationComponent!, {
|
|
84
|
-
// eslint-disable-next-line react/prop-types
|
|
85
|
-
nostoRecommendation: props.nostoRecommendation,
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// custom hook for rendering campaigns (CSR/SSR):
|
|
90
|
-
const [pageType, setPageType] = useState("")
|
|
91
|
-
function useRenderCampaigns(type: string = "") {
|
|
92
|
-
const placementRefs = useRef<Record<string, Root>>({})
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
if (pageType !== type) {
|
|
95
|
-
setPageType(type)
|
|
96
|
-
}
|
|
97
|
-
}, [])
|
|
98
|
-
|
|
99
|
-
const pageTypeUpdated = type === pageType
|
|
100
|
-
|
|
101
|
-
function renderCampaigns(
|
|
102
|
-
data: {
|
|
103
|
-
recommendations: Record<string, Recommendation>
|
|
104
|
-
campaigns: {
|
|
105
|
-
recommendations: Record<string, Recommendation>
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
api: NostoClient
|
|
109
|
-
) {
|
|
110
|
-
if (responseMode == "HTML") {
|
|
111
|
-
// inject content campaigns as usual:
|
|
112
|
-
api.placements.injectCampaigns(data.recommendations)
|
|
113
|
-
} else {
|
|
114
|
-
// render recommendation component into placements:
|
|
115
|
-
const recommendations = data.campaigns.recommendations
|
|
116
|
-
for (const key in recommendations) {
|
|
117
|
-
const recommendation = recommendations[key]
|
|
118
|
-
const placementSelector = "#" + key
|
|
119
|
-
const placement = () => document.querySelector(placementSelector)
|
|
120
|
-
|
|
121
|
-
if (placement()) {
|
|
122
|
-
if (!placementRefs.current[key]) placementRefs.current[key] = createRoot(placement()!)
|
|
123
|
-
const root = placementRefs.current[key]!
|
|
124
|
-
root.render(
|
|
125
|
-
<RecommendationComponentWrapper
|
|
126
|
-
nostoRecommendation={recommendation}
|
|
127
|
-
></RecommendationComponentWrapper>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return { renderCampaigns, pageTypeUpdated }
|
|
84
|
+
if (recommendationComponent && !isValidElement(recommendationComponent)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"The recommendationComponent prop must be a valid React element. Please provide a valid React element."
|
|
87
|
+
)
|
|
134
88
|
}
|
|
135
89
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
window.nostojs = (cb: (api: NostoClient) => void) => {
|
|
139
|
-
(window.nostojs.q = window.nostojs.q || []).push(cb)
|
|
140
|
-
}
|
|
141
|
-
window.nostojs(api => api.setAutoLoad(false))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!document.querySelectorAll("[nosto-client-script]").length && !shopifyMarkets) {
|
|
145
|
-
const script = document.createElement("script")
|
|
146
|
-
script.type = "text/javascript"
|
|
147
|
-
script.src = "//" + (host || "connect.nosto.com") + "/include/" + account
|
|
148
|
-
script.async = true
|
|
149
|
-
script.setAttribute("nosto-client-script", "")
|
|
150
|
-
|
|
151
|
-
script.onload = () => {
|
|
152
|
-
if (typeof jest !== "undefined") {
|
|
153
|
-
window.nosto?.reload({
|
|
154
|
-
site: "localhost",
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
setClientScriptLoadedState(true)
|
|
158
|
-
}
|
|
159
|
-
document.body.appendChild(script)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Enable Shopify markets functionality:
|
|
163
|
-
if (shopifyMarkets) {
|
|
164
|
-
const existingScript = document.querySelector("[nosto-client-script]")
|
|
165
|
-
const nostoSandbox = document.querySelector("#nosto-sandbox")
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
!existingScript ||
|
|
169
|
-
existingScript?.getAttribute("nosto-language") !== shopifyMarkets?.language ||
|
|
170
|
-
existingScript?.getAttribute("nosto-market-id") !== shopifyMarkets?.marketId
|
|
171
|
-
) {
|
|
172
|
-
if (clientScriptLoadedState) {
|
|
173
|
-
setClientScriptLoadedState(false)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
existingScript?.parentNode?.removeChild(existingScript)
|
|
177
|
-
nostoSandbox?.parentNode?.removeChild(nostoSandbox)
|
|
178
|
-
|
|
179
|
-
const script = document.createElement("script")
|
|
180
|
-
script.type = "text/javascript"
|
|
181
|
-
script.src =
|
|
182
|
-
"//" +
|
|
183
|
-
(host || "connect.nosto.com") +
|
|
184
|
-
`/script/shopify/market/nosto.js?merchant=${account}&market=${
|
|
185
|
-
shopifyMarkets.marketId || ""
|
|
186
|
-
}&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))
|
|
90
|
+
// Set responseMode for loading campaigns:
|
|
91
|
+
const responseMode = recommendationComponent ? "JSON_ORIGINAL" : "HTML"
|
|
191
92
|
|
|
192
|
-
|
|
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
|
-
}, [clientScriptLoadedState, shopifyMarkets])
|
|
93
|
+
const { clientScriptLoaded } = useLoadClientScript(props)
|
|
204
94
|
|
|
205
95
|
return (
|
|
206
96
|
<NostoContext.Provider
|
|
@@ -209,9 +99,7 @@ export default function NostoProvider(props: NostoProviderProps) {
|
|
|
209
99
|
clientScriptLoaded,
|
|
210
100
|
currentVariation,
|
|
211
101
|
responseMode,
|
|
212
|
-
recommendationComponent
|
|
213
|
-
useRenderCampaigns,
|
|
214
|
-
pageType,
|
|
102
|
+
recommendationComponent
|
|
215
103
|
}}
|
|
216
104
|
>
|
|
217
105
|
{children}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useNostoApi } from "../utils/hooks"
|
|
1
|
+
import { useRenderCampaigns, useNostoApi } from "../hooks"
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* You can personalise your search pages by using the NostoSearch component.
|
|
@@ -26,9 +25,7 @@ import { useNostoApi } from "../utils/hooks"
|
|
|
26
25
|
*/
|
|
27
26
|
export default function NostoSearch(props: { query: string; placements?: string[] }) {
|
|
28
27
|
const { query, placements } = props
|
|
29
|
-
const {
|
|
30
|
-
|
|
31
|
-
const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search")
|
|
28
|
+
const { renderCampaigns } = useRenderCampaigns()
|
|
32
29
|
|
|
33
30
|
useNostoApi(
|
|
34
31
|
async (api) => {
|
|
@@ -36,9 +33,9 @@ export default function NostoSearch(props: { query: string; placements?: string[
|
|
|
36
33
|
.viewSearch(query)
|
|
37
34
|
.setPlacements(placements || api.placements.getPlacements())
|
|
38
35
|
.load()
|
|
39
|
-
renderCampaigns(data
|
|
36
|
+
renderCampaigns(data)
|
|
40
37
|
},
|
|
41
|
-
[query
|
|
38
|
+
[query]
|
|
42
39
|
)
|
|
43
40
|
return null
|
|
44
41
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useNostoContext } from "
|
|
1
|
+
import { useNostoContext, useDeepCompareEffect } from "../hooks"
|
|
2
2
|
import { Cart, Customer } from "../types"
|
|
3
3
|
import { snakeize } from "../utils/snakeize"
|
|
4
|
-
import { useDeepCompareEffect } from "../utils/hooks"
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Nosto React requires that you pass it the details of current cart contents and the details of the currently logged-in customer, if any, on every route change.
|
package/src/components/index.ts
CHANGED
|
@@ -7,8 +7,5 @@ export { default as NostoSearch } from "./NostoSearch"
|
|
|
7
7
|
export { default as NostoOrder } from "./NostoOrder"
|
|
8
8
|
export { default as NostoHome } from "./NostoHome"
|
|
9
9
|
export { default as NostoPlacement } from "./NostoPlacement"
|
|
10
|
-
export { default as NostoProvider } from "./NostoProvider"
|
|
11
|
-
|
|
12
|
-
export { NostoContext, useNostoContext } from "./context"
|
|
13
|
-
export type { NostoContextType } from "./context"
|
|
10
|
+
export { default as NostoProvider, type NostoProviderProps } from "./NostoProvider"
|
|
14
11
|
export { default as NostoSession } from "./NostoSession"
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, ReactElement } from "react"
|
|
2
|
+
import { Recommendation, RenderMode } from "./types"
|
|
3
|
+
|
|
4
|
+
type AnyFunction = (...args: unknown[]) => unknown
|
|
5
|
+
|
|
6
|
+
export type RecommendationComponent = ReactElement<{
|
|
7
|
+
nostoRecommendation: Recommendation
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @group Types
|
|
12
|
+
*/
|
|
13
|
+
export interface NostoContextType {
|
|
14
|
+
account: string
|
|
15
|
+
clientScriptLoaded: boolean
|
|
16
|
+
currentVariation?: string
|
|
17
|
+
renderFunction?: AnyFunction
|
|
18
|
+
responseMode: RenderMode
|
|
19
|
+
recommendationComponent?: RecommendationComponent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @group Essential Functions
|
|
24
|
+
*/
|
|
25
|
+
export const NostoContext = createContext<NostoContextType>({
|
|
26
|
+
account: "",
|
|
27
|
+
currentVariation: "",
|
|
28
|
+
responseMode: "HTML",
|
|
29
|
+
clientScriptLoaded: false
|
|
30
|
+
})
|
|
31
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useDeepCompareEffect } from "./useDeepCompareEffect"
|
|
2
|
+
export { useNostoApi } from "./useNostoApi"
|
|
3
|
+
export { useNostoContext } from "./useNostoContext"
|
|
4
|
+
export { useRenderCampaigns } from "./useRenderCampaigns"
|
|
5
|
+
export { useLoadClientScript } from "./useLoadClientScript"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default function scriptLoader(scriptSrc: string, options?: ScriptLoadOptions): Promise<void> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const script = document.createElement("script")
|
|
4
|
+
script.type = "text/javascript"
|
|
5
|
+
script.src = scriptSrc
|
|
6
|
+
script.async = true
|
|
7
|
+
script.onload = () => resolve()
|
|
8
|
+
script.onerror = () => reject()
|
|
9
|
+
Object.entries(options?.attributes ?? {}).forEach(([k, v]) => script.setAttribute(k, v))
|
|
10
|
+
if (options?.position === "head") {
|
|
11
|
+
document.head.appendChild(script)
|
|
12
|
+
} else {
|
|
13
|
+
document.body.appendChild(script)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @group Types
|
|
20
|
+
*/
|
|
21
|
+
export type ScriptLoadOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Indicates the position of the script, default is "body"
|
|
24
|
+
*/
|
|
25
|
+
position?: "head" | "body"
|
|
26
|
+
/**
|
|
27
|
+
* Indicates the attributes of the script element
|
|
28
|
+
*/
|
|
29
|
+
attributes?: Record<string, string>
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useRef, useMemo } from "react"
|
|
2
|
+
import { deepCompare } from "../utils/compare"
|
|
3
|
+
|
|
4
|
+
export function useDeepCompareEffect(
|
|
5
|
+
callback: Parameters<typeof useEffect>[0],
|
|
6
|
+
dependencies: Parameters<typeof useEffect>[1]
|
|
7
|
+
): ReturnType<typeof useEffect> {
|
|
8
|
+
return useEffect(callback, useDeepCompareMemoize(dependencies))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function useDeepCompareMemoize<T>(value: T) {
|
|
12
|
+
const ref = useRef<T>(value);
|
|
13
|
+
const signalRef = useRef<number>(0)
|
|
14
|
+
|
|
15
|
+
if (!deepCompare(value, ref.current)) {
|
|
16
|
+
ref.current = value
|
|
17
|
+
signalRef.current += 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return useMemo(() => ref.current, [signalRef.current])
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect } from "react"
|
|
2
|
+
import { isNostoLoaded } from "../components/helpers"
|
|
3
|
+
import type { NostoClient } from "../types"
|
|
4
|
+
import type { NostoProviderProps } from "../components/NostoProvider"
|
|
5
|
+
import scriptLoaderFn from "./scriptLoader"
|
|
6
|
+
|
|
7
|
+
type NostoScriptProps = Pick<NostoProviderProps, "account" | "host" | "shopifyMarkets" | "loadScript" | "scriptLoader">
|
|
8
|
+
|
|
9
|
+
export function useLoadClientScript(props: NostoScriptProps) {
|
|
10
|
+
const { host = "connect.nosto.com", scriptLoader = scriptLoaderFn, account, shopifyMarkets, loadScript = true } = props
|
|
11
|
+
const [clientScriptLoaded, setClientScriptLoaded] = useState(false)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
function scriptOnload() {
|
|
15
|
+
// Override for production scripts to work in unit tests
|
|
16
|
+
if ("nostoReactTest" in window) {
|
|
17
|
+
window.nosto?.reload({
|
|
18
|
+
site: "localhost"
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
setClientScriptLoaded(true)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create and append script element
|
|
25
|
+
async function injectScriptElement(urlPartial: string, extraAttributes: Record<string, string> = {}) {
|
|
26
|
+
const scriptSrc = `//${host}${urlPartial}`
|
|
27
|
+
const attributes = { "nosto-client-script": "", ...extraAttributes }
|
|
28
|
+
await scriptLoader(scriptSrc, { attributes })
|
|
29
|
+
scriptOnload()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function prepareShopifyMarketsScript() {
|
|
33
|
+
const existingScript = document.querySelector("[nosto-client-script]")
|
|
34
|
+
|
|
35
|
+
const marketId = String(shopifyMarkets?.marketId || "")
|
|
36
|
+
const language = shopifyMarkets?.language || ""
|
|
37
|
+
|
|
38
|
+
const attributeMismatch =
|
|
39
|
+
existingScript?.getAttribute("nosto-language") !== language ||
|
|
40
|
+
existingScript?.getAttribute("nosto-market-id") !== marketId
|
|
41
|
+
|
|
42
|
+
if (!existingScript || attributeMismatch) {
|
|
43
|
+
if (clientScriptLoaded) {
|
|
44
|
+
setClientScriptLoaded(false)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nostoSandbox = document.querySelector("#nosto-sandbox")
|
|
48
|
+
|
|
49
|
+
existingScript?.parentNode?.removeChild(existingScript)
|
|
50
|
+
nostoSandbox?.parentNode?.removeChild(nostoSandbox)
|
|
51
|
+
|
|
52
|
+
const urlPartial =
|
|
53
|
+
`/script/shopify/market/nosto.js?merchant=${account}&market=${marketId}&locale=${language.toLowerCase()}`
|
|
54
|
+
injectScriptElement(urlPartial, { "nosto-language": language, "nosto-market-id": marketId })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load Nosto API stub
|
|
59
|
+
if (!window.nostojs) {
|
|
60
|
+
window.nostojs = (cb: (api: NostoClient) => void) => {
|
|
61
|
+
(window.nostojs.q = window.nostojs.q || []).push(cb)
|
|
62
|
+
}
|
|
63
|
+
window.nostojs(api => api.setAutoLoad(false))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!loadScript) {
|
|
67
|
+
window.nosto ? scriptOnload() : window.nostojs(scriptOnload)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load Nosto client script if not already loaded externally
|
|
72
|
+
if (!isNostoLoaded() && !shopifyMarkets) {
|
|
73
|
+
const urlPartial = `/include/${account}`
|
|
74
|
+
injectScriptElement(urlPartial)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Load Shopify Markets scripts
|
|
78
|
+
if (shopifyMarkets) {
|
|
79
|
+
prepareShopifyMarketsScript()
|
|
80
|
+
}
|
|
81
|
+
}, [shopifyMarkets?.marketId, shopifyMarkets?.language])
|
|
82
|
+
|
|
83
|
+
return { clientScriptLoaded }
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DependencyList, useEffect } from "react"
|
|
2
|
+
import { useNostoContext } from "./useNostoContext"
|
|
3
|
+
import { NostoClient } from "../types"
|
|
4
|
+
import { useDeepCompareEffect } from "./useDeepCompareEffect"
|
|
5
|
+
|
|
6
|
+
export function useNostoApi(
|
|
7
|
+
cb: (api: NostoClient) => void,
|
|
8
|
+
deps?: DependencyList,
|
|
9
|
+
flags?: { deep?: boolean }
|
|
10
|
+
): void {
|
|
11
|
+
const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
|
|
12
|
+
const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
|
|
13
|
+
|
|
14
|
+
useEffectFn(() => {
|
|
15
|
+
if (clientScriptLoaded) {
|
|
16
|
+
window.nostojs(api => {
|
|
17
|
+
api.defaultSession().setVariation(currentVariation!).setResponseMode(responseMode)
|
|
18
|
+
cb(api)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from "react"
|
|
2
|
+
import { NostoContext, NostoContextType } from "../context"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
|
|
6
|
+
*
|
|
7
|
+
* @group Essential Functions
|
|
8
|
+
*/
|
|
9
|
+
export function useNostoContext(): NostoContextType {
|
|
10
|
+
return useContext(NostoContext)
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cloneElement, useRef } from "react"
|
|
2
|
+
import { createRoot, Root } from "react-dom/client"
|
|
3
|
+
import { ActionResponse, Recommendation } from "../types"
|
|
4
|
+
import { useNostoContext } from "./useNostoContext"
|
|
5
|
+
import { RecommendationComponent } from "../context"
|
|
6
|
+
|
|
7
|
+
// RecommendationComponent for client-side rendering:
|
|
8
|
+
function RecommendationComponentWrapper(props: {
|
|
9
|
+
recommendationComponent: RecommendationComponent,
|
|
10
|
+
nostoRecommendation: Recommendation }) {
|
|
11
|
+
|
|
12
|
+
return cloneElement(props.recommendationComponent, {
|
|
13
|
+
// eslint-disable-next-line react/prop-types
|
|
14
|
+
nostoRecommendation: props.nostoRecommendation,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function injectCampaigns(data: ActionResponse) {
|
|
19
|
+
if (!window.nostojs) {
|
|
20
|
+
throw new Error("Nosto has not yet been initialized")
|
|
21
|
+
}
|
|
22
|
+
window.nostojs(api => {
|
|
23
|
+
api.placements.injectCampaigns(data.recommendations)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useRenderCampaigns() {
|
|
28
|
+
const { responseMode, recommendationComponent } = useNostoContext()
|
|
29
|
+
const placementRefs = useRef<Record<string, Root>>({})
|
|
30
|
+
|
|
31
|
+
if (responseMode == "HTML") {
|
|
32
|
+
return { renderCampaigns: injectCampaigns }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderCampaigns(data: ActionResponse) {
|
|
36
|
+
// render recommendation component into placements:
|
|
37
|
+
const recommendations = data.campaigns?.recommendations ?? {}
|
|
38
|
+
for (const key in recommendations) {
|
|
39
|
+
const recommendation = recommendations[key] as Recommendation
|
|
40
|
+
const placementSelector = "#" + key
|
|
41
|
+
const placementElement = document.querySelector(placementSelector)
|
|
42
|
+
|
|
43
|
+
if (placementElement) {
|
|
44
|
+
if (!placementRefs.current[key]) {
|
|
45
|
+
placementRefs.current[key] = createRoot(placementElement)
|
|
46
|
+
}
|
|
47
|
+
const root = placementRefs.current[key]!
|
|
48
|
+
root.render(
|
|
49
|
+
<RecommendationComponentWrapper
|
|
50
|
+
recommendationComponent={recommendationComponent!}
|
|
51
|
+
nostoRecommendation={recommendation}
|
|
52
|
+
></RecommendationComponentWrapper>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { renderCampaigns }
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export type { Cart, Customer, Product, Order, Recommendation } from "./types"
|
|
2
2
|
export * from "./components"
|
|
3
|
+
export { type ScriptLoadOptions } from "./hooks/scriptLoader"
|
|
4
|
+
export { NostoContext, type NostoContextType } from "./context"
|
|
5
|
+
export { useNostoContext } from "./hooks/useNostoContext"
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ declare global {
|
|
|
17
17
|
export interface NostoClient {
|
|
18
18
|
setAutoLoad(autoload: boolean): void
|
|
19
19
|
defaultSession(): Session
|
|
20
|
+
listen(event: string, callback: (data: unknown) => void): void
|
|
20
21
|
placements: {
|
|
21
22
|
getPlacements(): string[]
|
|
22
23
|
injectCampaigns(recommendations: Record<string, unknown>): void
|
|
@@ -28,7 +29,7 @@ export interface NostoClient {
|
|
|
28
29
|
*/
|
|
29
30
|
export interface Recommendation {
|
|
30
31
|
result_id: string
|
|
31
|
-
products:
|
|
32
|
+
products: PushedProduct[]
|
|
32
33
|
result_type: string
|
|
33
34
|
title: string
|
|
34
35
|
div_id: string
|
|
@@ -36,6 +37,77 @@ export interface Recommendation {
|
|
|
36
37
|
params: unknown
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface PushedProduct {
|
|
41
|
+
age_group?: string
|
|
42
|
+
alternate_image_urls: string[]
|
|
43
|
+
availability: string
|
|
44
|
+
brand?: string
|
|
45
|
+
category: string[]
|
|
46
|
+
category_id: string[]
|
|
47
|
+
condition?: string
|
|
48
|
+
custom_fields: { [index: string]: string }
|
|
49
|
+
date_published?: Date
|
|
50
|
+
description?: string
|
|
51
|
+
gender?: string
|
|
52
|
+
google_category?: string
|
|
53
|
+
gtin?: string
|
|
54
|
+
image_url?: string
|
|
55
|
+
inventory_level?: number
|
|
56
|
+
list_price?: number
|
|
57
|
+
name: string
|
|
58
|
+
parent_category_id: string[]
|
|
59
|
+
price: number
|
|
60
|
+
price_currency_code: string
|
|
61
|
+
product_id: string
|
|
62
|
+
rating_value?: number
|
|
63
|
+
review_count?: number
|
|
64
|
+
skus: PushedProductSKU[]
|
|
65
|
+
source_updated?: Date
|
|
66
|
+
supplier_cost?: number
|
|
67
|
+
tags1: string[]
|
|
68
|
+
tags2: string[]
|
|
69
|
+
tags3: string[]
|
|
70
|
+
thumb_url?: string
|
|
71
|
+
unit_pricing_base_measure?: number
|
|
72
|
+
unit_pricing_measure?: number
|
|
73
|
+
unit_pricing_unit?: string
|
|
74
|
+
update_received?: Date
|
|
75
|
+
url: string
|
|
76
|
+
variation_id?: string
|
|
77
|
+
variations: { [index: string]: PushedVariation }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface PushedProductSKU extends NostoSku { }
|
|
81
|
+
|
|
82
|
+
export interface PushedVariation extends NostoVariant { }
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export interface NostoSku extends Sku {
|
|
86
|
+
inventory_level?: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NostoVariant {
|
|
90
|
+
availability: string
|
|
91
|
+
available: boolean
|
|
92
|
+
discounted: boolean
|
|
93
|
+
list_price?: number
|
|
94
|
+
price: number
|
|
95
|
+
price_currency_code: string
|
|
96
|
+
price_text?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Sku {
|
|
100
|
+
availability: string
|
|
101
|
+
custom_fields: { [index: string]: string }
|
|
102
|
+
gtin?: string
|
|
103
|
+
id: string
|
|
104
|
+
image_url?: string
|
|
105
|
+
list_price?: number
|
|
106
|
+
name: string
|
|
107
|
+
price: number
|
|
108
|
+
url?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
39
111
|
// copied from client script d.ts export
|
|
40
112
|
declare const eventTypes: readonly ["vp", "lp", "dp", "rp", "bp", "vc", "or", "is", "cp", "ec", "es", "gc", "src", "cpr", "pl", "cc", "con"]
|
|
41
113
|
declare type EventType = typeof eventTypes[number]
|