@nosto/nosto-react 2.6.0 → 2.7.1

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/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Nosto React
2
2
 
3
+ [![](https://img.shields.io/github/package-json/v/nosto/nosto-react)](https://github.com/Nosto/nosto-react/releases)
4
+ [![](https://github.com/Nosto/nosto-react/actions/workflows/ci.yml/badge.svg)](https://github.com/Nosto/nosto-react/actions/workflows/ci.yml)
5
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
6
+ [![npm license](https://img.shields.io/npm/l/@nosto/nosto-react.svg)](https://github.com/Nosto/nosto-react/blob/master/LICENSE)
7
+
3
8
  Nosto React is a React component library to make it even easier to implement Nosto.
4
9
 
5
10
  The library provides you everything to get started with personalisation on your React site. It's dead simple, and beginner friendly.
6
11
 
7
- ### Why?
12
+ ## Why?
8
13
 
9
14
  You should be using Nosto React if you want to:
10
15
 
@@ -12,284 +17,14 @@ You should be using Nosto React if you want to:
12
17
  - Customize your components at will and with ease
13
18
  - Follow React principles
14
19
 
15
- ## Feature list
16
-
17
- Our React component library includes the following features:
18
-
19
- - Recommendations, including client-side rendering
20
- - Onsite content personalisation
21
- - Dynamic bundles
22
- - Debug toolbar\* (excluding advanced use cases)
23
- - Pop-ups & personalised emails
24
- - A/B testing
25
- - Segmentation and Insights
26
- - Analytics
27
- - Search\*\* (when implemented via our code editor)
28
-
29
- _\*Note: Our React component library currently does not support advanced use cases of the debug toolbar, but we are constantly working to improve our library and provide you with the best possible integration options. We support most use-cases with page tagging and debug workflows._
30
-
31
- _\*\*Note: The search feature is available when implemented via our code editor._
32
-
33
- ### Additional features
34
-
35
- - ✓ Lightweight. Almost zero bloat.
36
- - ✓ Full support for the Facebook Pixel and Google Analytics.
37
- - ✓ Full support for leveraging overlays.
38
- - ✓ Full support for the JS API.
39
- - ✓ Full support for placements.
40
-
41
- ### Building
42
-
43
- #### Required versions
44
-
45
- - npm: 10.9.0
46
- - node: v22.12.0
47
-
48
- ### Installation
49
-
50
- ##### Yarn:
51
-
52
- yarn add @nosto/nosto-react
53
-
54
- ##### NPM:
55
-
56
- npm install @nosto/nosto-react
57
-
58
- ### Getting Started
59
-
60
- ##### The root widget
61
-
62
- There’s one very specific widget in Nosto React and it is the `NostoProvider` one.
63
-
64
- This widget is what we call the Nosto root widget, which is responsible for adding the actual Nosto script and the JS API stub. This widget wraps all other React Nosto widgets. Here’s how:
65
-
66
- ```jsx
67
- import { NostoProvider } from "@nosto/nosto-react";
68
-
69
- <NostoProvider
70
- account="your-nosto-account-id"
71
- recommendationComponent={<NostoSlot />}
72
- >
73
- <App />
74
- </NostoProvider>;
75
- ```
76
-
77
- **Note:** the component also accepts a prop to configure the host `host="connect.nosto.com"`. In advanced use-cases, the need to configure the host may surface.
78
-
79
- #### Client side rendering for recommendations
80
-
81
- In order to implement client-side rendering, the <NostoProvider> requires a designated component to render the recommendations provided by Nosto. This component should be capable of processing the JSON response received from our backend. Notice the `recommendationComponent={<NostoSlot />}` prop passed to `<NostoProvider>` above.
82
-
83
- 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.
84
-
85
- ##### Understanding Placements
86
-
87
- Nosto React has a special component called `NostoPlacement`. The component is a simply a <u>hidden</u> `<div>` placeholder into which Nosto injects recommendations or personalises the content between the tags.
88
-
89
- We recommend adding as many placements across your views as needed as these are hidden and only populated when a corresponding campaign (targeting that placement) is configured.
90
-
91
- ##### Managing the session
92
-
93
- 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. This makes it easier to add attribution.
94
-
95
- The `NostoSession` component makes it very easy to keep the session up to date so long as the cart and the customer are provided.
96
-
97
- The `cart` prop requires a value that adheres to the type `Cart`, while the `customer` prop requires a value that adheres to the type `Customer`.
98
-
99
- ```jsx
100
- import { NostoSession } from "@nosto/nosto-react";
101
-
102
- <>
103
- <Meta />
104
- <header>
105
- <MainMenu />
106
- <NostoSession cart={currentCart} customer={currentUser} />
107
- </header>
108
- <Routes />
109
- <Footer />
110
- </>;
111
- ```
112
-
113
- ### Adding Personalisation
114
-
115
- Nosto React ships with canned components for the different page types. Each component contains all lifecycle methods to dispatch the necessary events.
116
-
117
- ##### Personalising your home page
118
-
119
- The `NostoHome` component must be used to personalise the home page. The component does not require any props.
120
-
121
- By default, your account, when created, has <u>four</u> front-page placements named `frontpage-nosto-1`, `frontpage-nosto-2`, `frontpage-nosto-3` and `frontpage-nosto-4`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
122
-
123
- The `<NostoHome \>` component needs to be added after the placements. Content and recommendations will be rendered through this component.
124
-
125
- ```jsx
126
- import { NostoHome, NostoPlacement } from "@nosto/nosto-react";
127
-
128
- <div className="front-page">
129
- ... ... ...
130
- <NostoPlacement id="frontpage-nosto-1" />
131
- <NostoPlacement id="frontpage-nosto-2" />
132
- <NostoPlacement id="frontpage-nosto-3" />
133
- <NostoPlacement id="frontpage-nosto-4" />
134
- <NostoHome />
135
- </div>;
136
- ```
137
-
138
- ##### Personalising your product pages
139
-
140
- The `NostoProduct` component must be used to personalise the product page. The component requires that you provide it the identifier of the current product being viewed.
141
-
142
- By default, your account, when created, has <u>three</u> product-page placements named `productpage-nosto-1`, `productpage-nosto-2` and `productpage-nosto-3`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
143
-
144
- The `<NostoProduct \>` component needs to be added after the placements. Content and recommendations will be rendered through this component. Pass in the product ID via the `product` prop to pass this information back to Nosto.
145
-
146
- ```jsx
147
- import { NostoPlacement, NostoProduct } from "@nosto/nosto-react";
148
-
149
- <div className="product-page">
150
- ... ... ...
151
- <NostoPlacement id="productpage-nosto-1" />
152
- <NostoPlacement id="productpage-nosto-2" />
153
- <NostoPlacement id="productpage-nosto-3" />
154
- <NostoProduct product={product.id} />
155
- </div>;
156
- ```
157
-
158
- ##### Personalising your search result pages
159
-
160
- You can personalise your search pages by using the `NostoSearch` component. The component requires that you provide it the current search term.
161
-
162
- By default, your account, when created, has <u>two</u> search-page placements named `searchpage-nosto-1` and `searchpage-nosto-2`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
163
-
164
- ```jsx
165
- import { NostoPlacement, NostoSearch } from "@nosto/nosto-react";
166
-
167
- <div className="search-page">
168
- ... ... ...
169
- <NostoPlacement id="searchpage-nosto-1" />
170
- <NostoPlacement id="searchpage-nosto-2" />
171
- <NostoSearch query={search} />
172
- </div>;
173
- ```
174
-
175
- **Note:** Do not encode the search term in any way. It should be provided an unencoded string. A query for "black shoes" must be provided as-is and not as "black+shoes". Doing so will lead to invalid results.
176
-
177
- ##### Personalising your category list pages
178
-
179
- You can personalise your category and collection pages by using the `NostoCategory` component. The component requires that you provide it the the slash-delimited slug representation of the current category.
180
-
181
- By default, your account, when created, has <u>two</u> category placements named `categorypage-nosto-1` and `categorypage-nosto-2`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
182
-
183
- ```jsx
184
- import { NostoCategory, NostoPlacement } from "@nosto/nosto-react";
185
-
186
- <div className="category-page">
187
- ... ... ...
188
- <NostoPlacement id="categorypage-nosto-1" />
189
- <NostoPlacement id="categorypage-nosto-2" />
190
- <NostoCategory category={category.name} />
191
- </div>;
192
- ```
193
-
194
- **Note:** Be sure to pass in the correct category representation. If the category being viewed is Mens >> Jackets, you must provide the name as `/Mens/Jackets` . You must ensure that the category path provided here matches that of the categories tagged in your products.
195
-
196
- ##### Personalising your cart checkout pages
197
-
198
- You can personalise your cart and checkout pages by using the `NostoCheckout` component. The component does not require any props.
199
-
200
- By default, your account, when created, has <u>two</u> cart-page placements named `categorypage-nosto-1` and `categorypage-nosto-2`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
201
-
202
- ```jsx
203
- import { NostoCheckout, NostoPlacement } from "@nosto/nosto-react";
204
-
205
- <div className="checkout-page">
206
- ... ... ...
207
- <NostoPlacement id="checkout-nosto-1" />
208
- <NostoPlacement id="checkout-nosto-2" />
209
- <NostoCheckout />
210
- </div>;
211
- ```
212
-
213
- ##### Personalising your 404 error pages
214
-
215
- You can personalise not found pages by using the `Nosto404` component. The component does not require any props.
216
-
217
- By default, your account, when created, has three 404-page placements named `notfound-nosto-1`, `notfound-nosto-2` and `notfound-nosto-2`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
218
-
219
- ```jsx
220
- import { Nosto404, NostoPlacement } from "@nosto/nosto-react";
221
-
222
- <div className="notfound-page">
223
- ... ... ...
224
- <NostoPlacement id="notfound-nosto-1" />
225
- <NostoPlacement id="notfound-nosto-2" />
226
- <NostoPlacement id="notfound-nosto-3" />
227
- <Nosto404 />
228
- </div>;
229
- ```
230
-
231
- ##### Personalising your miscellaneous pages
232
-
233
- You can personalise your miscellaneous pages by using the `NostoOther` component. The component does not require any props.
234
-
235
- By default, your account, when created, has two other-page placements named `other-nosto-1` and `other-nosto-2`. You may omit these and use any identifier you need. The identifiers used here are simply provided to illustrate the example.
236
-
237
- ```jsx
238
- import { NostoOther, NostoPlacement } from "@nosto/nosto-react";
239
-
240
- <div className="other-page">
241
- ... ... ...
242
- <NostoPlacement id="other-nosto-1" />
243
- <NostoPlacement id="other-nosto-2" />
244
- <NostoOther />
245
- </div>;
246
- ```
247
-
248
- ##### Personalising your order confirmation page
249
-
250
- You can personalise your order-confirmation/thank-you page by using the `NostoOrder` component. The component requires that you provide it with the details of the order.
251
-
252
- By default, your account, when created, has one other-page placement named `thankyou-nosto-1`. You may omit this and use any identifier you need. The identifier used here is simply provided to illustrate the example.
253
-
254
- ```jsx
255
- import {
256
- NostoOrder,
257
- NostoPlacement,
258
- } from "@nosto/nosto-react";
259
-
260
- <div className="thankyou-page">
261
- ... ... ...
262
- <NostoPlacement id="thankyou-nosto-1" />
263
- <NostoOrder order={ order } />
264
- </div>;
265
- ```
266
-
267
- ### Hook alternatives
268
-
269
- For all the page type specific components hooks are also provided with the same props
270
-
271
- - useNosto404
272
- - useNostoOther
273
- - useNostoCheckout
274
- - useNostoProduct
275
- - useNostoCategory
276
- - useNostoSearch
277
- - useNostOrder
278
- - useNostoHome
279
-
280
- ### Detailed technical documentation
281
- Find our latest technical specs and documentation hosted [here](https://nosto.github.io/nosto-react).
282
-
283
- ### Feedback
284
-
285
- If you've found a feature missing or you would like to report an issue, simply [open up an issue](https://github.com/nosto/nosto-react/issues/new) and let us know.
20
+ ## Installation
286
21
 
287
- We're always collecting feedback and learning from your use-cases. If you find your self customising widgets and forking the repo to make patches - do drop a message. We'd love to know more and understand how we can make React Nosto an even slicker library for you.
22
+ yarn add @nosto/nosto-react
288
23
 
289
- ### Contributing
24
+ npm install @nosto/nosto-react
290
25
 
291
- Please take a moment to review the guidelines for contributing.
26
+ ## Documentation
292
27
 
293
- ### License
28
+ Read [Nosto Techdocs](https://docs.nosto.com/techdocs/apis/frontend/oss/react-support) for more information on how to use the library.
294
29
 
295
- MIT
30
+ [Library TypeDoc page](https://nosto.github.io/nosto-react/) includes detailed library helpers documentation and examples.
package/dist/index.d.ts CHANGED
@@ -9,8 +9,6 @@ import { ReactElement } from 'react';
9
9
  import { ReactNode } from 'react';
10
10
  import { RenderMode } from '@nosto/nosto-js/client';
11
11
 
12
- declare type AnyFunction = (...args: unknown[]) => unknown;
13
-
14
12
  export { Cart }
15
13
 
16
14
  export { Customer }
@@ -116,7 +114,7 @@ export declare interface NostoContextType {
116
114
  account: string;
117
115
  clientScriptLoaded: boolean;
118
116
  currentVariation?: string;
119
- renderFunction?: AnyFunction;
117
+ renderFunction?: (...args: unknown[]) => unknown;
120
118
  responseMode: RenderMode;
121
119
  recommendationComponent?: RecommendationComponent;
122
120
  }
@@ -223,7 +221,7 @@ export declare type NostoOtherProps = {
223
221
  *
224
222
  * @group Components
225
223
  */
226
- export declare function NostoPlacement({ id, pageType }: NostoPlacementProps): JSX.Element;
224
+ export declare function NostoPlacement({ id, pageType, children }: NostoPlacementProps): JSX.Element;
227
225
 
228
226
  /**
229
227
  * @group Components
@@ -231,6 +229,7 @@ export declare function NostoPlacement({ id, pageType }: NostoPlacementProps): J
231
229
  export declare type NostoPlacementProps = {
232
230
  id: string;
233
231
  pageType?: string;
232
+ children?: React.ReactNode;
234
233
  };
235
234
 
236
235
  /**
@@ -320,6 +319,10 @@ export declare interface NostoProviderProps {
320
319
  * Recommendation component which holds nostoRecommendation object
321
320
  */
322
321
  recommendationComponent?: RecommendationComponent;
322
+ /**
323
+ * Recommendation render mode. See {@link https://nosto.github.io/nosto-js/types/client.RenderMode.html}
324
+ */
325
+ renderMode?: RenderMode;
323
326
  /**
324
327
  * Enables Shopify markets with language and market id
325
328
  */
@@ -407,10 +410,12 @@ export declare interface Recommendation {
407
410
  params: unknown;
408
411
  }
409
412
 
410
- declare type RecommendationComponent = ReactElement<{
413
+ export declare type RecommendationComponent = ReactElement<{
411
414
  nostoRecommendation: Recommendation;
412
415
  }>;
413
416
 
417
+ export { RenderMode }
418
+
414
419
  /**
415
420
  * @group Types
416
421
  */
@@ -425,9 +430,9 @@ export declare type ScriptLoadOptions = {
425
430
  attributes?: Record<string, string>;
426
431
  };
427
432
 
428
- declare type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<SnakeToCamelCase<U>>}` : S;
433
+ export declare type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<SnakeToCamelCase<U>>}` : S;
429
434
 
430
- declare type ToCamelCase<T> = T extends (infer U)[] ? ToCamelCase<U>[] : T extends Date ? T : T extends object ? {
435
+ export declare type ToCamelCase<T> = T extends (infer U)[] ? ToCamelCase<U>[] : T extends Date ? T : T extends object ? {
431
436
  [K in keyof T as SnakeToCamelCase<K & string>]: ToCamelCase<T[K]>;
432
437
  } : T;
433
438
 
package/dist/index.es.js CHANGED
@@ -1,6 +1,6 @@
1
- import { createContext as k, useContext as x, useEffect as S, useRef as h, useMemo as I, cloneElement as _, useState as T, isValidElement as D } from "react";
1
+ import { createContext as k, useContext as x, useEffect as S, useRef as h, useMemo as I, cloneElement as _, useState as M, isValidElement as T } from "react";
2
2
  import { jsx as v } from "react/jsx-runtime";
3
- import { createRoot as M } from "react-dom/client";
3
+ import { createRoot as D } from "react-dom/client";
4
4
  const O = k({
5
5
  account: "",
6
6
  currentVariation: "",
@@ -10,13 +10,13 @@ const O = k({
10
10
  function P() {
11
11
  return x(O);
12
12
  }
13
- const N = (e) => String(e) === "[object Object]";
13
+ const g = (e) => String(e) === "[object Object]";
14
14
  function L(e) {
15
- if (!N(e)) return !1;
15
+ if (!g(e)) return !1;
16
16
  const t = e.constructor;
17
17
  if (t === void 0) return !0;
18
18
  const n = t.prototype;
19
- return !(!N(n) || !n.hasOwnProperty("isPrototypeOf"));
19
+ return !(!g(n) || !n.hasOwnProperty("isPrototypeOf"));
20
20
  }
21
21
  function y(e, t) {
22
22
  if (e === t)
@@ -31,66 +31,69 @@ function y(e, t) {
31
31
  }
32
32
  return !1;
33
33
  }
34
- function g(e, t) {
34
+ function j(e, t) {
35
35
  return S(e, V(t));
36
36
  }
37
37
  function V(e) {
38
38
  const t = h(e), n = h(0);
39
39
  return y(e, t.current) || (t.current = e, n.current += 1), I(() => t.current, [n.current]);
40
40
  }
41
- function j() {
41
+ function N() {
42
42
  window.nostojs = window.nostojs ?? function(e) {
43
43
  (window.nostojs.q = window.nostojs.q ?? []).push(e);
44
44
  };
45
45
  }
46
- function H() {
46
+ async function m(e) {
47
+ return window.nostojs(e);
48
+ }
49
+ typeof window < "u" && (N(), m((e) => {
50
+ e.internal.getSettings();
51
+ }));
52
+ function q() {
47
53
  return typeof window.nosto < "u";
48
54
  }
49
- const q = {
55
+ const H = {
50
56
  production: "https://connect.nosto.com/",
51
57
  staging: "https://connect.staging.nosto.com/",
52
58
  local: "https://connect.nosto.com/"
53
59
  };
54
60
  function R(e) {
55
- return q[e ?? "production"];
61
+ return H[e ?? "production"];
56
62
  }
57
63
  function F({ merchantId: e, env: t, options: n, shopifyInternational: o, scriptLoader: r }) {
58
- var c, a;
59
- const s = document.querySelector("script[nosto-language], script[nosto-market-id]"), i = String((o == null ? void 0 : o.marketId) || ""), l = (o == null ? void 0 : o.language) || "", C = (s == null ? void 0 : s.getAttribute("nosto-language")) !== l || (s == null ? void 0 : s.getAttribute("nosto-market-id")) !== i;
64
+ var c, i;
65
+ const s = document.querySelector("script[nosto-language], script[nosto-market-id]"), a = String((o == null ? void 0 : o.marketId) || ""), u = (o == null ? void 0 : o.language) || "", C = (s == null ? void 0 : s.getAttribute("nosto-language")) !== u || (s == null ? void 0 : s.getAttribute("nosto-market-id")) !== a;
60
66
  if (!s || C) {
61
- const m = document.querySelector("#nosto-sandbox");
62
- (c = s == null ? void 0 : s.parentNode) == null || c.removeChild(s), (a = m == null ? void 0 : m.parentNode) == null || a.removeChild(m);
67
+ const f = document.querySelector("#nosto-sandbox");
68
+ (c = s == null ? void 0 : s.parentNode) == null || c.removeChild(s), (i = f == null ? void 0 : f.parentNode) == null || i.removeChild(f);
63
69
  const p = new URL("/script/shopify/market/nosto.js", R(t));
64
- p.searchParams.append("merchant", e), p.searchParams.append("market", i), p.searchParams.append("locale", l.toLowerCase());
65
- const A = {
70
+ p.searchParams.append("merchant", e), p.searchParams.append("market", a), p.searchParams.append("locale", u.toLowerCase());
71
+ const b = {
66
72
  ...n == null ? void 0 : n.attributes,
67
- "nosto-language": l,
68
- "nosto-market-id": i
73
+ "nosto-language": u,
74
+ "nosto-market-id": a
69
75
  };
70
- return (r ?? E)(p.toString(), { ...n, attributes: A });
76
+ return (r ?? E)(p.toString(), { ...n, attributes: b });
71
77
  }
72
78
  return Promise.resolve();
73
79
  }
74
80
  function z(e) {
75
81
  if (e.shopifyInternational)
76
82
  return F(e);
77
- const { merchantId: t, env: n, options: o, scriptLoader: r } = e, c = r ?? E, a = new URL(`/include/${t}`, R(n));
78
- return c(a.toString(), o);
83
+ const { merchantId: t, env: n, options: o, scriptLoader: r } = e, c = r ?? E, i = new URL(`/include/${t}`, R(n));
84
+ return c(i.toString(), o);
79
85
  }
80
86
  function E(e, t) {
81
87
  return new Promise((n, o) => {
82
88
  const r = document.createElement("script");
83
- r.src = e, r.async = !0, r.type = "text/javascript", r.onload = () => n(), r.onerror = () => o(), Object.entries((t == null ? void 0 : t.attributes) ?? {}).forEach(([c, a]) => r.setAttribute(c, a)), (t == null ? void 0 : t.position) === "head" ? document.head.appendChild(r) : document.body.appendChild(r);
89
+ r.src = e, r.async = !0, r.type = "text/javascript", r.onload = () => n(), r.onerror = () => o(), Object.entries((t == null ? void 0 : t.attributes) ?? {}).forEach(([c, i]) => r.setAttribute(c, i)), (t == null ? void 0 : t.position) === "head" ? document.head.appendChild(r) : document.body.appendChild(r);
84
90
  });
85
91
  }
86
- async function f(e) {
87
- return window.nostojs(e);
88
- }
89
- typeof window < "u" && j();
90
- function u(e, t, n) {
92
+ typeof window < "u" && N();
93
+ function d(e, t, n) {
91
94
  const { clientScriptLoaded: o } = P();
92
- (n != null && n.deep ? g : S)(() => {
93
- o && f(e);
95
+ (n != null && n.deep ? j : S)(() => {
96
+ o && m(e);
94
97
  }, [o, ...t ?? []]);
95
98
  }
96
99
  function U(e) {
@@ -99,30 +102,32 @@ function U(e) {
99
102
  nostoRecommendation: e.nostoRecommendation
100
103
  });
101
104
  }
102
- function b(e) {
103
- f((t) => t.placements.injectCampaigns(e));
105
+ function A(e) {
106
+ m((t) => t.placements.injectCampaigns(e));
104
107
  }
105
108
  function $(e) {
106
109
  if (!window.nostojs)
107
110
  throw new Error("Nosto has not yet been initialized");
108
- b(e.recommendations);
111
+ A(e.recommendations);
109
112
  }
110
- function d() {
113
+ function l() {
111
114
  const { responseMode: e, recommendationComponent: t } = P(), n = h({});
112
115
  if (e == "HTML")
113
116
  return { renderCampaigns: $ };
117
+ if (!t)
118
+ throw new Error("recommendationComponent is required for client-side rendering using hook");
114
119
  function o(r) {
115
- var a, s;
116
- b(((a = r.campaigns) == null ? void 0 : a.content) ?? {});
120
+ var i, s;
121
+ A(((i = r.campaigns) == null ? void 0 : i.content) ?? {});
117
122
  const c = ((s = r.campaigns) == null ? void 0 : s.recommendations) ?? {};
118
- for (const i in c) {
119
- const l = c[i], C = "#" + i, m = document.querySelector(C);
120
- m && (n.current[i] || (n.current[i] = M(m)), n.current[i].render(
123
+ for (const a in c) {
124
+ const u = c[a], C = "#" + a, f = document.querySelector(C);
125
+ f && (n.current[a] || (n.current[a] = D(f)), n.current[a].render(
121
126
  /* @__PURE__ */ v(
122
127
  U,
123
128
  {
124
129
  recommendationComponent: t,
125
- nostoRecommendation: l
130
+ nostoRecommendation: u
126
131
  }
127
132
  )
128
133
  ));
@@ -131,18 +136,18 @@ function d() {
131
136
  return { renderCampaigns: o };
132
137
  }
133
138
  function G(e) {
134
- const { renderCampaigns: t } = d();
135
- u(async (n) => {
139
+ const { renderCampaigns: t } = l();
140
+ d(async (n) => {
136
141
  const o = await n.defaultSession().viewNotFound().setPlacements((e == null ? void 0 : e.placements) || n.placements.getPlacements()).load();
137
142
  t(o);
138
143
  });
139
144
  }
140
- function ie(e) {
145
+ function ae(e) {
141
146
  return G(e), null;
142
147
  }
143
148
  function J({ category: e, placements: t }) {
144
- const { renderCampaigns: n } = d();
145
- u(
149
+ const { renderCampaigns: n } = l();
150
+ d(
146
151
  async (o) => {
147
152
  const r = await o.defaultSession().viewCategory(e).setPlacements(t || o.placements.getPlacements()).load();
148
153
  n(r);
@@ -154,8 +159,8 @@ function ue(e) {
154
159
  return J(e), null;
155
160
  }
156
161
  function W(e) {
157
- const { renderCampaigns: t } = d();
158
- u(async (n) => {
162
+ const { renderCampaigns: t } = l();
163
+ d(async (n) => {
159
164
  const o = await n.defaultSession().viewCart().setPlacements((e == null ? void 0 : e.placements) || n.placements.getPlacements()).load();
160
165
  t(o);
161
166
  });
@@ -164,8 +169,8 @@ function de(e) {
164
169
  return W(e), null;
165
170
  }
166
171
  function Z(e) {
167
- const { renderCampaigns: t } = d();
168
- u(async (n) => {
172
+ const { renderCampaigns: t } = l();
173
+ d(async (n) => {
169
174
  const o = await n.defaultSession().viewFrontPage().setPlacements((e == null ? void 0 : e.placements) || n.placements.getPlacements()).load();
170
175
  t(o);
171
176
  });
@@ -186,8 +191,8 @@ function K(e) {
186
191
  return Object.prototype.toString.call(e) === "[object RegExp]";
187
192
  }
188
193
  function Q({ order: e, placements: t }) {
189
- const { renderCampaigns: n } = d();
190
- u(
194
+ const { renderCampaigns: n } = l();
195
+ d(
191
196
  async (o) => {
192
197
  const r = await o.defaultSession().addOrder(w(e)).setPlacements(t || o.placements.getPlacements()).load();
193
198
  n(r);
@@ -200,8 +205,8 @@ function me(e) {
200
205
  return Q(e), null;
201
206
  }
202
207
  function X(e) {
203
- const { renderCampaigns: t } = d();
204
- u(async (n) => {
208
+ const { renderCampaigns: t } = l();
209
+ d(async (n) => {
205
210
  const o = await n.defaultSession().viewOther().setPlacements((e == null ? void 0 : e.placements) || n.placements.getPlacements()).load();
206
211
  t(o);
207
212
  });
@@ -209,20 +214,20 @@ function X(e) {
209
214
  function fe(e) {
210
215
  return X(e), null;
211
216
  }
212
- function pe({ id: e, pageType: t }) {
213
- return /* @__PURE__ */ v("div", { className: "nosto_element", id: e }, e + (t || ""));
217
+ function pe({ id: e, pageType: t, children: n }) {
218
+ return /* @__PURE__ */ v("div", { className: "nosto_element", id: e, children: n }, e + (t || ""));
214
219
  }
215
220
  function Y({ product: e, tagging: t, placements: n, reference: o }) {
216
- const { renderCampaigns: r } = d();
221
+ const { renderCampaigns: r } = l();
217
222
  if (t && !t.product_id)
218
223
  throw new Error("The product object must contain a product_id property");
219
224
  const c = (t == null ? void 0 : t.product_id) ?? e;
220
- u(
221
- async (a) => {
222
- const s = a.defaultSession().viewProduct(t ?? e).setPlacements(n || a.placements.getPlacements());
225
+ d(
226
+ async (i) => {
227
+ const s = i.defaultSession().viewProduct(t ?? e).setPlacements(n || i.placements.getPlacements());
223
228
  o && s.setRef(c, o);
224
- const i = await s.load();
225
- r(i);
229
+ const a = await s.load();
230
+ r(a);
226
231
  },
227
232
  [c, t == null ? void 0 : t.selected_sku_id]
228
233
  );
@@ -233,7 +238,7 @@ function we(e) {
233
238
  function ee(e, t) {
234
239
  return new Promise((n, o) => {
235
240
  const r = document.createElement("script");
236
- r.type = "text/javascript", r.src = e, r.async = !0, r.onload = () => n(), r.onerror = () => o(), Object.entries((t == null ? void 0 : t.attributes) ?? {}).forEach(([c, a]) => r.setAttribute(c, a)), (t == null ? void 0 : t.position) === "head" ? document.head.appendChild(r) : document.body.appendChild(r);
241
+ r.type = "text/javascript", r.src = e, r.async = !0, r.onload = () => n(), r.onerror = () => o(), Object.entries((t == null ? void 0 : t.attributes) ?? {}).forEach(([c, i]) => r.setAttribute(c, i)), (t == null ? void 0 : t.position) === "head" ? document.head.appendChild(r) : document.body.appendChild(r);
237
242
  });
238
243
  }
239
244
  const te = { "nosto-client-script": "" };
@@ -243,16 +248,16 @@ function ne(e) {
243
248
  account: n,
244
249
  shopifyMarkets: o,
245
250
  loadScript: r = !0
246
- } = e, [c, a] = T(!1);
251
+ } = e, [c, i] = M(!1);
247
252
  return S(() => {
248
253
  function s() {
249
- a(!0);
254
+ i(!0);
250
255
  }
251
- if (j(), f((l) => l.setAutoLoad(!1)), !r) {
252
- f(s);
256
+ if (N(), m((u) => u.setAutoLoad(!1)), !r) {
257
+ m(s);
253
258
  return;
254
259
  }
255
- async function i() {
260
+ async function a() {
256
261
  await z({
257
262
  merchantId: n,
258
263
  shopifyInternational: o,
@@ -262,26 +267,26 @@ function ne(e) {
262
267
  scriptLoader: t
263
268
  }), s();
264
269
  }
265
- (!H() || o) && i();
270
+ (!q() || o) && a();
266
271
  }, [o == null ? void 0 : o.marketId, o == null ? void 0 : o.language]), { clientScriptLoaded: c };
267
272
  }
268
273
  function Ce(e) {
269
- const { account: t, multiCurrency: n = !1, children: o, recommendationComponent: r } = e, c = n ? e.currentVariation : "";
270
- if (r && !D(r))
274
+ const { account: t, multiCurrency: n = !1, children: o, recommendationComponent: r, renderMode: c } = e, i = n ? e.currentVariation : "";
275
+ if (r && !T(r))
271
276
  throw new Error(
272
277
  "The recommendationComponent prop must be a valid React element. Please provide a valid React element."
273
278
  );
274
- const a = r ? "JSON_ORIGINAL" : "HTML", { clientScriptLoaded: s } = ne(e);
275
- return s && f((i) => {
276
- i.defaultSession().setVariation(c).setResponseMode(a);
279
+ const s = c || (r ? "JSON_ORIGINAL" : "HTML"), { clientScriptLoaded: a } = ne(e);
280
+ return a && m((u) => {
281
+ u.defaultSession().setVariation(i).setResponseMode(s);
277
282
  }), /* @__PURE__ */ v(
278
283
  O.Provider,
279
284
  {
280
285
  value: {
281
286
  account: t,
282
- clientScriptLoaded: s,
283
- currentVariation: c,
284
- responseMode: a,
287
+ clientScriptLoaded: a,
288
+ currentVariation: i,
289
+ responseMode: s,
285
290
  recommendationComponent: r
286
291
  },
287
292
  children: o
@@ -289,8 +294,8 @@ function Ce(e) {
289
294
  );
290
295
  }
291
296
  function oe({ query: e, placements: t }) {
292
- const { renderCampaigns: n } = d();
293
- u(
297
+ const { renderCampaigns: n } = l();
298
+ d(
294
299
  async (o) => {
295
300
  const r = await o.defaultSession().viewSearch(e).setPlacements(t || o.placements.getPlacements()).load();
296
301
  n(r);
@@ -303,9 +308,9 @@ function he(e) {
303
308
  }
304
309
  function re({ cart: e, customer: t } = {}) {
305
310
  const { clientScriptLoaded: n } = P();
306
- g(() => {
311
+ j(() => {
307
312
  const o = e ? w(e) : void 0, r = t ? w(t) : void 0;
308
- n && f((c) => {
313
+ n && m((c) => {
309
314
  c.defaultSession().setCart(o).setCustomer(r).viewOther().load({ skipPageViews: !0 });
310
315
  });
311
316
  }, [n, e, t]);
@@ -314,7 +319,7 @@ function ye(e) {
314
319
  return re(e), null;
315
320
  }
316
321
  export {
317
- ie as Nosto404,
322
+ ae as Nosto404,
318
323
  ue as NostoCategory,
319
324
  de as NostoCheckout,
320
325
  O as NostoContext,
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- (function(r,u){typeof exports=="object"&&typeof module<"u"?u(exports,require("react"),require("react/jsx-runtime"),require("react-dom/client")):typeof define=="function"&&define.amd?define(["exports","react","react/jsx-runtime","react-dom/client"],u):(r=typeof globalThis<"u"?globalThis:r||self,u(r["@nosto/nosto-react"]={},r.React,r["react/jsx-runtime"],r.client))})(this,function(r,u,y,V){"use strict";const S=u.createContext({account:"",currentVariation:"",responseMode:"HTML",clientScriptLoaded:!1});function w(){return u.useContext(S)}const O=e=>String(e)==="[object Object]";function j(e){if(!O(e))return!1;const t=e.constructor;if(t===void 0)return!0;const n=t.prototype;return!(!O(n)||!n.hasOwnProperty("isPrototypeOf"))}function P(e,t){if(e===t)return!0;if(e instanceof Date&&t instanceof Date)return e.getTime()===t.getTime();if(e instanceof Array&&t instanceof Array)return e.length!==t.length?!1:e.every((n,o)=>P(n,t[o]));if(j(e)&&j(t)){const n=Object.entries(e);return n.length!==Object.keys(t).length?!1:n.every(([o,s])=>P(s,t[o]))}return!1}function L(e,t){return u.useEffect(e,F(t))}function F(e){const t=u.useRef(e),n=u.useRef(0);return P(e,t.current)||(t.current=e,n.current+=1),u.useMemo(()=>t.current,[n.current])}function g(){window.nostojs=window.nostojs??function(e){(window.nostojs.q=window.nostojs.q??[]).push(e)}}function z(){return typeof window.nosto<"u"}const U={production:"https://connect.nosto.com/",staging:"https://connect.staging.nosto.com/",local:"https://connect.nosto.com/"};function R(e){return U[e??"production"]}function $({merchantId:e,env:t,options:n,shopifyInternational:o,scriptLoader:s}){var c,a;const i=document.querySelector("script[nosto-language], script[nosto-market-id]"),d=String((o==null?void 0:o.marketId)||""),f=(o==null?void 0:o.language)||"",v=(i==null?void 0:i.getAttribute("nosto-language"))!==f||(i==null?void 0:i.getAttribute("nosto-market-id"))!==d;if(!i||v){const h=document.querySelector("#nosto-sandbox");(c=i==null?void 0:i.parentNode)==null||c.removeChild(i),(a=h==null?void 0:h.parentNode)==null||a.removeChild(h);const N=new URL("/script/shopify/market/nosto.js",R(t));N.searchParams.append("merchant",e),N.searchParams.append("market",d),N.searchParams.append("locale",f.toLowerCase());const ue={...n==null?void 0:n.attributes,"nosto-language":f,"nosto-market-id":d};return(s??E)(N.toString(),{...n,attributes:ue})}return Promise.resolve()}function G(e){if(e.shopifyInternational)return $(e);const{merchantId:t,env:n,options:o,scriptLoader:s}=e,c=s??E,a=new URL(`/include/${t}`,R(n));return c(a.toString(),o)}function E(e,t){return new Promise((n,o)=>{const s=document.createElement("script");s.src=e,s.async=!0,s.type="text/javascript",s.onload=()=>n(),s.onerror=()=>o(),Object.entries((t==null?void 0:t.attributes)??{}).forEach(([c,a])=>s.setAttribute(c,a)),(t==null?void 0:t.position)==="head"?document.head.appendChild(s):document.body.appendChild(s)})}async function p(e){return window.nostojs(e)}typeof window<"u"&&g();function l(e,t,n){const{clientScriptLoaded:o}=w();(n!=null&&n.deep?L:u.useEffect)(()=>{o&&p(e)},[o,...t??[]])}function J(e){return u.cloneElement(e.recommendationComponent,{nostoRecommendation:e.nostoRecommendation})}function b(e){p(t=>t.placements.injectCampaigns(e))}function W(e){if(!window.nostojs)throw new Error("Nosto has not yet been initialized");b(e.recommendations)}function m(){const{responseMode:e,recommendationComponent:t}=w(),n=u.useRef({});if(e=="HTML")return{renderCampaigns:W};function o(s){var a,i;b(((a=s.campaigns)==null?void 0:a.content)??{});const c=((i=s.campaigns)==null?void 0:i.recommendations)??{};for(const d in c){const f=c[d],v="#"+d,h=document.querySelector(v);h&&(n.current[d]||(n.current[d]=V.createRoot(h)),n.current[d].render(y.jsx(J,{recommendationComponent:t,nostoRecommendation:f})))}}return{renderCampaigns:o}}function A(e){const{renderCampaigns:t}=m();l(async n=>{const o=await n.defaultSession().viewNotFound().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function Z(e){return A(e),null}function k({category:e,placements:t}){const{renderCampaigns:n}=m();l(async o=>{const s=await o.defaultSession().viewCategory(e).setPlacements(t||o.placements.getPlacements()).load();n(s)},[e])}function B(e){return k(e),null}function I(e){const{renderCampaigns:t}=m();l(async n=>{const o=await n.defaultSession().viewCart().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function K(e){return I(e),null}function T(e){const{renderCampaigns:t}=m();l(async n=>{const o=await n.defaultSession().viewFrontPage().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function Q(e){return T(e),null}function C(e){return!e||typeof e!="object"||X(e)||Y(e)?e:Array.isArray(e)?e.map(C):Object.keys(e).reduce((t,n)=>{const o=n[0].toLowerCase()+n.slice(1).replace(/([A-Z]+)/g,(s,c)=>"_"+c.toLowerCase());return t[o]=C(e[n]),t},{})}function X(e){return Object.prototype.toString.call(e)==="[object Date]"}function Y(e){return Object.prototype.toString.call(e)==="[object RegExp]"}function _({order:e,placements:t}){const{renderCampaigns:n}=m();l(async o=>{const s=await o.defaultSession().addOrder(C(e)).setPlacements(t||o.placements.getPlacements()).load();n(s)},[e],{deep:!0})}function x(e){return _(e),null}function q(e){const{renderCampaigns:t}=m();l(async n=>{const o=await n.defaultSession().viewOther().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function ee(e){return q(e),null}function te({id:e,pageType:t}){return y.jsx("div",{className:"nosto_element",id:e},e+(t||""))}function H({product:e,tagging:t,placements:n,reference:o}){const{renderCampaigns:s}=m();if(t&&!t.product_id)throw new Error("The product object must contain a product_id property");const c=(t==null?void 0:t.product_id)??e;l(async a=>{const i=a.defaultSession().viewProduct(t??e).setPlacements(n||a.placements.getPlacements());o&&i.setRef(c,o);const d=await i.load();s(d)},[c,t==null?void 0:t.selected_sku_id])}function ne(e){return H(e),null}function oe(e,t){return new Promise((n,o)=>{const s=document.createElement("script");s.type="text/javascript",s.src=e,s.async=!0,s.onload=()=>n(),s.onerror=()=>o(),Object.entries((t==null?void 0:t.attributes)??{}).forEach(([c,a])=>s.setAttribute(c,a)),(t==null?void 0:t.position)==="head"?document.head.appendChild(s):document.body.appendChild(s)})}const se={"nosto-client-script":""};function re(e){const{scriptLoader:t=oe,account:n,shopifyMarkets:o,loadScript:s=!0}=e,[c,a]=u.useState(!1);return u.useEffect(()=>{function i(){a(!0)}if(g(),p(f=>f.setAutoLoad(!1)),!s){p(i);return}async function d(){await G({merchantId:n,shopifyInternational:o,options:{attributes:se},scriptLoader:t}),i()}(!z()||o)&&d()},[o==null?void 0:o.marketId,o==null?void 0:o.language]),{clientScriptLoaded:c}}function ce(e){const{account:t,multiCurrency:n=!1,children:o,recommendationComponent:s}=e,c=n?e.currentVariation:"";if(s&&!u.isValidElement(s))throw new Error("The recommendationComponent prop must be a valid React element. Please provide a valid React element.");const a=s?"JSON_ORIGINAL":"HTML",{clientScriptLoaded:i}=re(e);return i&&p(d=>{d.defaultSession().setVariation(c).setResponseMode(a)}),y.jsx(S.Provider,{value:{account:t,clientScriptLoaded:i,currentVariation:c,responseMode:a,recommendationComponent:s},children:o})}function M({query:e,placements:t}){const{renderCampaigns:n}=m();l(async o=>{const s=await o.defaultSession().viewSearch(e).setPlacements(t||o.placements.getPlacements()).load();n(s)},[e])}function ie(e){return M(e),null}function D({cart:e,customer:t}={}){const{clientScriptLoaded:n}=w();L(()=>{const o=e?C(e):void 0,s=t?C(t):void 0;n&&p(c=>{c.defaultSession().setCart(o).setCustomer(s).viewOther().load({skipPageViews:!0})})},[n,e,t])}function ae(e){return D(e),null}r.Nosto404=Z,r.NostoCategory=B,r.NostoCheckout=K,r.NostoContext=S,r.NostoHome=Q,r.NostoOrder=x,r.NostoOther=ee,r.NostoPlacement=te,r.NostoProduct=ne,r.NostoProvider=ce,r.NostoSearch=ie,r.NostoSession=ae,r.useNosto404=A,r.useNostoCategory=k,r.useNostoCheckout=I,r.useNostoContext=w,r.useNostoHome=T,r.useNostoOrder=_,r.useNostoOther=q,r.useNostoProduct=H,r.useNostoSearch=M,r.useNostoSession=D,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});
1
+ (function(s,u){typeof exports=="object"&&typeof module<"u"?u(exports,require("react"),require("react/jsx-runtime"),require("react-dom/client")):typeof define=="function"&&define.amd?define(["exports","react","react/jsx-runtime","react-dom/client"],u):(s=typeof globalThis<"u"?globalThis:s||self,u(s["@nosto/nosto-react"]={},s.React,s["react/jsx-runtime"],s.client))})(this,function(s,u,y,V){"use strict";const S=u.createContext({account:"",currentVariation:"",responseMode:"HTML",clientScriptLoaded:!1});function N(){return u.useContext(S)}const g=e=>String(e)==="[object Object]";function j(e){if(!g(e))return!1;const t=e.constructor;if(t===void 0)return!0;const n=t.prototype;return!(!g(n)||!n.hasOwnProperty("isPrototypeOf"))}function P(e,t){if(e===t)return!0;if(e instanceof Date&&t instanceof Date)return e.getTime()===t.getTime();if(e instanceof Array&&t instanceof Array)return e.length!==t.length?!1:e.every((n,o)=>P(n,t[o]));if(j(e)&&j(t)){const n=Object.entries(e);return n.length!==Object.keys(t).length?!1:n.every(([o,r])=>P(r,t[o]))}return!1}function L(e,t){return u.useEffect(e,F(t))}function F(e){const t=u.useRef(e),n=u.useRef(0);return P(e,t.current)||(t.current=e,n.current+=1),u.useMemo(()=>t.current,[n.current])}function v(){window.nostojs=window.nostojs??function(e){(window.nostojs.q=window.nostojs.q??[]).push(e)}}async function p(e){return window.nostojs(e)}typeof window<"u"&&(v(),p(e=>{e.internal.getSettings()}));function z(){return typeof window.nosto<"u"}const U={production:"https://connect.nosto.com/",staging:"https://connect.staging.nosto.com/",local:"https://connect.nosto.com/"};function R(e){return U[e??"production"]}function $({merchantId:e,env:t,options:n,shopifyInternational:o,scriptLoader:r}){var c,a;const i=document.querySelector("script[nosto-language], script[nosto-market-id]"),d=String((o==null?void 0:o.marketId)||""),l=(o==null?void 0:o.language)||"",O=(i==null?void 0:i.getAttribute("nosto-language"))!==l||(i==null?void 0:i.getAttribute("nosto-market-id"))!==d;if(!i||O){const h=document.querySelector("#nosto-sandbox");(c=i==null?void 0:i.parentNode)==null||c.removeChild(i),(a=h==null?void 0:h.parentNode)==null||a.removeChild(h);const w=new URL("/script/shopify/market/nosto.js",R(t));w.searchParams.append("merchant",e),w.searchParams.append("market",d),w.searchParams.append("locale",l.toLowerCase());const ue={...n==null?void 0:n.attributes,"nosto-language":l,"nosto-market-id":d};return(r??E)(w.toString(),{...n,attributes:ue})}return Promise.resolve()}function G(e){if(e.shopifyInternational)return $(e);const{merchantId:t,env:n,options:o,scriptLoader:r}=e,c=r??E,a=new URL(`/include/${t}`,R(n));return c(a.toString(),o)}function E(e,t){return new Promise((n,o)=>{const r=document.createElement("script");r.src=e,r.async=!0,r.type="text/javascript",r.onload=()=>n(),r.onerror=()=>o(),Object.entries((t==null?void 0:t.attributes)??{}).forEach(([c,a])=>r.setAttribute(c,a)),(t==null?void 0:t.position)==="head"?document.head.appendChild(r):document.body.appendChild(r)})}typeof window<"u"&&v();function m(e,t,n){const{clientScriptLoaded:o}=N();(n!=null&&n.deep?L:u.useEffect)(()=>{o&&p(e)},[o,...t??[]])}function J(e){return u.cloneElement(e.recommendationComponent,{nostoRecommendation:e.nostoRecommendation})}function k(e){p(t=>t.placements.injectCampaigns(e))}function W(e){if(!window.nostojs)throw new Error("Nosto has not yet been initialized");k(e.recommendations)}function f(){const{responseMode:e,recommendationComponent:t}=N(),n=u.useRef({});if(e=="HTML")return{renderCampaigns:W};if(!t)throw new Error("recommendationComponent is required for client-side rendering using hook");function o(r){var a,i;k(((a=r.campaigns)==null?void 0:a.content)??{});const c=((i=r.campaigns)==null?void 0:i.recommendations)??{};for(const d in c){const l=c[d],O="#"+d,h=document.querySelector(O);h&&(n.current[d]||(n.current[d]=V.createRoot(h)),n.current[d].render(y.jsx(J,{recommendationComponent:t,nostoRecommendation:l})))}}return{renderCampaigns:o}}function A(e){const{renderCampaigns:t}=f();m(async n=>{const o=await n.defaultSession().viewNotFound().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function Z(e){return A(e),null}function b({category:e,placements:t}){const{renderCampaigns:n}=f();m(async o=>{const r=await o.defaultSession().viewCategory(e).setPlacements(t||o.placements.getPlacements()).load();n(r)},[e])}function B(e){return b(e),null}function I(e){const{renderCampaigns:t}=f();m(async n=>{const o=await n.defaultSession().viewCart().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function K(e){return I(e),null}function T(e){const{renderCampaigns:t}=f();m(async n=>{const o=await n.defaultSession().viewFrontPage().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function Q(e){return T(e),null}function C(e){return!e||typeof e!="object"||X(e)||Y(e)?e:Array.isArray(e)?e.map(C):Object.keys(e).reduce((t,n)=>{const o=n[0].toLowerCase()+n.slice(1).replace(/([A-Z]+)/g,(r,c)=>"_"+c.toLowerCase());return t[o]=C(e[n]),t},{})}function X(e){return Object.prototype.toString.call(e)==="[object Date]"}function Y(e){return Object.prototype.toString.call(e)==="[object RegExp]"}function _({order:e,placements:t}){const{renderCampaigns:n}=f();m(async o=>{const r=await o.defaultSession().addOrder(C(e)).setPlacements(t||o.placements.getPlacements()).load();n(r)},[e],{deep:!0})}function x(e){return _(e),null}function q(e){const{renderCampaigns:t}=f();m(async n=>{const o=await n.defaultSession().viewOther().setPlacements((e==null?void 0:e.placements)||n.placements.getPlacements()).load();t(o)})}function ee(e){return q(e),null}function te({id:e,pageType:t,children:n}){return y.jsx("div",{className:"nosto_element",id:e,children:n},e+(t||""))}function M({product:e,tagging:t,placements:n,reference:o}){const{renderCampaigns:r}=f();if(t&&!t.product_id)throw new Error("The product object must contain a product_id property");const c=(t==null?void 0:t.product_id)??e;m(async a=>{const i=a.defaultSession().viewProduct(t??e).setPlacements(n||a.placements.getPlacements());o&&i.setRef(c,o);const d=await i.load();r(d)},[c,t==null?void 0:t.selected_sku_id])}function ne(e){return M(e),null}function oe(e,t){return new Promise((n,o)=>{const r=document.createElement("script");r.type="text/javascript",r.src=e,r.async=!0,r.onload=()=>n(),r.onerror=()=>o(),Object.entries((t==null?void 0:t.attributes)??{}).forEach(([c,a])=>r.setAttribute(c,a)),(t==null?void 0:t.position)==="head"?document.head.appendChild(r):document.body.appendChild(r)})}const re={"nosto-client-script":""};function se(e){const{scriptLoader:t=oe,account:n,shopifyMarkets:o,loadScript:r=!0}=e,[c,a]=u.useState(!1);return u.useEffect(()=>{function i(){a(!0)}if(v(),p(l=>l.setAutoLoad(!1)),!r){p(i);return}async function d(){await G({merchantId:n,shopifyInternational:o,options:{attributes:re},scriptLoader:t}),i()}(!z()||o)&&d()},[o==null?void 0:o.marketId,o==null?void 0:o.language]),{clientScriptLoaded:c}}function ce(e){const{account:t,multiCurrency:n=!1,children:o,recommendationComponent:r,renderMode:c}=e,a=n?e.currentVariation:"";if(r&&!u.isValidElement(r))throw new Error("The recommendationComponent prop must be a valid React element. Please provide a valid React element.");const i=c||(r?"JSON_ORIGINAL":"HTML"),{clientScriptLoaded:d}=se(e);return d&&p(l=>{l.defaultSession().setVariation(a).setResponseMode(i)}),y.jsx(S.Provider,{value:{account:t,clientScriptLoaded:d,currentVariation:a,responseMode:i,recommendationComponent:r},children:o})}function H({query:e,placements:t}){const{renderCampaigns:n}=f();m(async o=>{const r=await o.defaultSession().viewSearch(e).setPlacements(t||o.placements.getPlacements()).load();n(r)},[e])}function ie(e){return H(e),null}function D({cart:e,customer:t}={}){const{clientScriptLoaded:n}=N();L(()=>{const o=e?C(e):void 0,r=t?C(t):void 0;n&&p(c=>{c.defaultSession().setCart(o).setCustomer(r).viewOther().load({skipPageViews:!0})})},[n,e,t])}function ae(e){return D(e),null}s.Nosto404=Z,s.NostoCategory=B,s.NostoCheckout=K,s.NostoContext=S,s.NostoHome=Q,s.NostoOrder=x,s.NostoOther=ee,s.NostoPlacement=te,s.NostoProduct=ne,s.NostoProvider=ce,s.NostoSearch=ie,s.NostoSession=ae,s.useNosto404=A,s.useNostoCategory=b,s.useNostoCheckout=I,s.useNostoContext=N,s.useNostoHome=T,s.useNostoOrder=_,s.useNostoOther=q,s.useNostoProduct=M,s.useNostoSearch=H,s.useNostoSession=D,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nosto/nosto-react",
3
3
  "description": "Component library to simply implementing Nosto on React.",
4
- "version": "2.6.0",
4
+ "version": "2.7.1",
5
5
  "author": "Mridang Agarwalla, Dominik Gilg",
6
6
  "license": "ISC",
7
7
  "repository": {
@@ -24,7 +24,7 @@
24
24
  "prettier:fix": "prettier '{src,spec}/**' --write",
25
25
  "test": "vitest run --silent",
26
26
  "test-loud": "vitest run",
27
- "typedoc": "typedoc src/index.ts"
27
+ "typedoc": "typedoc --treatWarningsAsErrors src/index.ts"
28
28
  },
29
29
  "files": [
30
30
  "dist",
@@ -44,6 +44,7 @@
44
44
  "@types/user-event": "^4.1.1",
45
45
  "@vitejs/plugin-react": "^4.3.1",
46
46
  "eslint": "^9.13.0",
47
+ "eslint-plugin-barrel-files": "^3.0.1",
47
48
  "eslint-plugin-promise": "^7.1.0",
48
49
  "eslint-plugin-react": "^7.33.2",
49
50
  "prettier": "^3.3.3",
@@ -53,12 +54,12 @@
53
54
  "react-router-dom": "^7.0.1",
54
55
  "rimraf": "^6.0.1",
55
56
  "rollup-plugin-visualizer": "^5.13.1",
56
- "typedoc": "^0.27.2",
57
+ "typedoc": "^0.28.0",
57
58
  "typescript": "^5.6.3",
58
59
  "typescript-eslint": "^8.13.0",
59
60
  "vite": "^6.0.3",
60
61
  "vite-plugin-dts": "^4.2.2",
61
- "vitest": "^2.0.5"
62
+ "vitest": "^3.0.2"
62
63
  },
63
64
  "main": "./dist/index.umd.js",
64
65
  "module": "./dist/index.es.js",
@@ -73,5 +74,7 @@
73
74
  "bugs": {
74
75
  "url": "https://github.com/Nosto/nosto-react/issues"
75
76
  },
76
- "homepage": "https://github.com/Nosto/nosto-react#readme"
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
77
80
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @group Components
3
3
  */
4
- export type NostoPlacementProps = { id: string; pageType?: string }
4
+ export type NostoPlacementProps = { id: string; pageType?: string; children?: React.ReactNode }
5
5
 
6
6
  /**
7
7
  * Nosto React has a special component called NostoPlacement.
@@ -16,6 +16,10 @@ export type NostoPlacementProps = { id: string; pageType?: string }
16
16
  *
17
17
  * @group Components
18
18
  */
19
- export function NostoPlacement({ id, pageType }: NostoPlacementProps) {
20
- return <div className="nosto_element" id={id} key={id + (pageType || "")} />
19
+ export function NostoPlacement({ id, pageType, children }: NostoPlacementProps) {
20
+ return (
21
+ <div className="nosto_element" id={id} key={id + (pageType || "")}>
22
+ {children}
23
+ </div>
24
+ )
21
25
  }
@@ -4,6 +4,7 @@ import type { ReactNode } from "react"
4
4
  import { ScriptLoadOptions } from "../hooks/scriptLoader"
5
5
  import { useLoadClientScript } from "../hooks/useLoadClientScript"
6
6
  import { nostojs } from "@nosto/nosto-js"
7
+ import { RenderMode } from "@nosto/nosto-js/client"
7
8
 
8
9
  /**
9
10
  * @group Components
@@ -33,6 +34,10 @@ export interface NostoProviderProps {
33
34
  * Recommendation component which holds nostoRecommendation object
34
35
  */
35
36
  recommendationComponent?: RecommendationComponent
37
+ /**
38
+ * Recommendation render mode. See {@link https://nosto.github.io/nosto-js/types/client.RenderMode.html}
39
+ */
40
+ renderMode?: RenderMode
36
41
  /**
37
42
  * Enables Shopify markets with language and market id
38
43
  */
@@ -72,7 +77,7 @@ export interface NostoProviderProps {
72
77
  * @group Components
73
78
  */
74
79
  export function NostoProvider(props: NostoProviderProps) {
75
- const { account, multiCurrency = false, children, recommendationComponent } = props
80
+ const { account, multiCurrency = false, children, recommendationComponent, renderMode } = props
76
81
 
77
82
  // Pass currentVariation as empty string if multiCurrency is disabled
78
83
  const currentVariation = multiCurrency ? props.currentVariation : ""
@@ -84,7 +89,7 @@ export function NostoProvider(props: NostoProviderProps) {
84
89
  }
85
90
 
86
91
  // Set responseMode for loading campaigns:
87
- const responseMode = recommendationComponent ? "JSON_ORIGINAL" : "HTML"
92
+ const responseMode = renderMode || (recommendationComponent ? "JSON_ORIGINAL" : "HTML")
88
93
 
89
94
  const { clientScriptLoaded } = useLoadClientScript(props)
90
95
 
package/src/context.ts CHANGED
@@ -2,8 +2,6 @@ import { createContext, ReactElement } from "react"
2
2
  import { Recommendation } from "./types"
3
3
  import { RenderMode } from "@nosto/nosto-js/client"
4
4
 
5
- type AnyFunction = (...args: unknown[]) => unknown
6
-
7
5
  export type RecommendationComponent = ReactElement<{
8
6
  nostoRecommendation: Recommendation
9
7
  }>
@@ -15,7 +13,7 @@ export interface NostoContextType {
15
13
  account: string
16
14
  clientScriptLoaded: boolean
17
15
  currentVariation?: string
18
- renderFunction?: AnyFunction
16
+ renderFunction?: (...args: unknown[]) => unknown
19
17
  responseMode: RenderMode
20
18
  recommendationComponent?: RecommendationComponent
21
19
  }
@@ -22,4 +22,4 @@ export function useNostoHome(props?: NostoHomeProps) {
22
22
  .load()
23
23
  renderCampaigns(data)
24
24
  })
25
- }
25
+ }
@@ -20,7 +20,7 @@ function RecommendationComponentWrapper(props: {
20
20
  }
21
21
 
22
22
  function injectPlacements(data: Record<string, unknown>) {
23
- nostojs(api => api.placements.injectCampaigns(data as Parameters<API['placements']['injectCampaigns']>[0]))
23
+ nostojs(api => api.placements.injectCampaigns(data as Parameters<API["placements"]["injectCampaigns"]>[0]))
24
24
  }
25
25
 
26
26
  function injectCampaigns(data: CampaignData) {
@@ -39,6 +39,10 @@ export function useRenderCampaigns() {
39
39
  return { renderCampaigns: injectCampaigns }
40
40
  }
41
41
 
42
+ if (!recommendationComponent) {
43
+ throw new Error("recommendationComponent is required for client-side rendering using hook")
44
+ }
45
+
42
46
  function renderCampaigns(data: CampaignData) {
43
47
  // inject Onsite content campaigns directly
44
48
  injectPlacements(data.campaigns?.content ?? {})
package/src/index.ts CHANGED
@@ -1,25 +1,27 @@
1
- export type { Product, PushedCustomer as Customer, Cart, WebsiteOrder as Order } from "@nosto/nosto-js/client"
1
+ /* eslint-disable barrel-files/avoid-barrel-files */
2
+ export type { Product, PushedCustomer as Customer, Cart, WebsiteOrder as Order, RenderMode } from "@nosto/nosto-js/client"
2
3
  export type { Recommendation } from "./types"
4
+ export type { SnakeToCamelCase, ToCamelCase } from "./utils/types"
3
5
  export { type ScriptLoadOptions } from "./hooks/scriptLoader"
4
- export { NostoContext, type NostoContextType } from "./context"
6
+ export { NostoContext, type NostoContextType, type RecommendationComponent } from "./context"
5
7
  export { useNostoContext } from "./hooks/useNostoContext"
6
- export * from "./components/Nosto404"
7
- export * from "./components/NostoCategory"
8
- export * from "./components/NostoCheckout"
9
- export * from "./components/NostoHome"
10
- export * from "./components/NostoOrder"
11
- export * from "./components/NostoOther"
12
- export * from "./components/NostoPlacement"
13
- export * from "./components/NostoProduct"
14
- export * from "./components/NostoProvider"
15
- export * from "./components/NostoSearch"
16
- export * from "./components/NostoSession"
17
- export * from "./hooks/useNosto404"
18
- export * from "./hooks/useNostoCategory"
19
- export * from "./hooks/useNostoCheckout"
20
- export * from "./hooks/useNostoHome"
21
- export * from "./hooks/useNostoOrder"
22
- export * from "./hooks/useNostoOther"
23
- export * from "./hooks/useNostoProduct"
24
- export * from "./hooks/useNostoSearch"
25
- export * from "./hooks/useNostoSession"
8
+ export { Nosto404 } from "./components/Nosto404"
9
+ export { NostoCategory } from "./components/NostoCategory"
10
+ export { NostoCheckout } from "./components/NostoCheckout"
11
+ export { NostoHome } from "./components/NostoHome"
12
+ export { NostoOrder } from "./components/NostoOrder"
13
+ export { NostoOther } from "./components/NostoOther"
14
+ export { NostoPlacement, type NostoPlacementProps } from "./components/NostoPlacement"
15
+ export { NostoProduct } from "./components/NostoProduct"
16
+ export { NostoProvider, type NostoProviderProps } from "./components/NostoProvider"
17
+ export { NostoSearch } from "./components/NostoSearch"
18
+ export { NostoSession } from "./components/NostoSession"
19
+ export { useNosto404, type Nosto404Props } from "./hooks/useNosto404"
20
+ export { useNostoCategory, type NostoCategoryProps } from "./hooks/useNostoCategory"
21
+ export { useNostoCheckout, type NostoCheckoutProps } from "./hooks/useNostoCheckout"
22
+ export { useNostoHome, type NostoHomeProps } from "./hooks/useNostoHome"
23
+ export { useNostoOrder, type NostoOrderProps } from "./hooks/useNostoOrder"
24
+ export { useNostoOther, type NostoOtherProps } from "./hooks/useNostoOther"
25
+ export { useNostoProduct, type NostoProductProps } from "./hooks/useNostoProduct"
26
+ export { useNostoSearch, type NostoSearchProps }from "./hooks/useNostoSearch"
27
+ export { useNostoSession, type NostoSessionProps } from "./hooks/useNostoSession"
@@ -1,4 +1,4 @@
1
- type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
1
+ export type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
2
2
  ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
3
3
  : S
4
4
 
@@ -13,7 +13,7 @@ export type ToCamelCase<T> = T extends (infer U)[]
13
13
  }
14
14
  : T
15
15
 
16
- type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
16
+ export type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
17
17
  ? U extends Uncapitalize<U>
18
18
  ? `${Lowercase<T>}${CamelToSnakeCase<U>}`
19
19
  : `${Lowercase<T>}_${CamelToSnakeCase<Uncapitalize<U>>}`