@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
@@ -0,0 +1,220 @@
1
+ import React, { useEffect, isValidElement, useState, useRef } from "react"
2
+ import { NostoContext } from "./context"
3
+ import { createRoot, Root } from "react-dom/client"
4
+ import { NostoClient, Recommendation } from "../types"
5
+
6
+ /**
7
+ * @group Components
8
+ */
9
+ export interface NostoProviderProps {
10
+ /**
11
+ * Indicates merchant id
12
+ */
13
+ account: string
14
+ /**
15
+ * Indicates currency
16
+ */
17
+ currentVariation?: string
18
+ /**
19
+ * Indicates an url of a server
20
+ */
21
+ host?: string
22
+ children: React.ReactElement
23
+ /**
24
+ * Indicates if merchant uses multiple currencies
25
+ */
26
+ multiCurrency?: boolean
27
+ /**
28
+ * Recommendation component which holds nostoRecommendation object
29
+ */
30
+ recommendationComponent?: React.ReactElement<{
31
+ nostoRecommendation: Recommendation
32
+ }>
33
+ /**
34
+ * Enables Shopify markets with language and market id
35
+ */
36
+ shopifyMarkets?: {
37
+ language?: string
38
+ marketId?: string | number
39
+ }
40
+ }
41
+
42
+ /**
43
+ * This widget is what we call the Nosto root widget, which is responsible for adding the actual Nosto script and the JS API stub.
44
+ * This widget wraps all other React Nosto widgets.
45
+ *
46
+ * ```
47
+ * <NostoProvider account="your-nosto-account-id" recommendationComponent={<NostoSlot />}>
48
+ * <App />
49
+ * </NostoProvider>
50
+ * ```
51
+ *
52
+ * **Note:** the component also accepts a prop to configure the host `host="connect.nosto.com"`.
53
+ * In advanced use-cases, the need to configure the host may surface.
54
+ *
55
+ * In order to implement client-side rendering, the requires a designated component to render the recommendations provided by Nosto.
56
+ * This component should be capable of processing the JSON response received from our backend.
57
+ * Notice the `recommendationComponent` prop passed to `<NostoProvider>` above.
58
+ *
59
+ * 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.
60
+ *
61
+ * @group Components
62
+ */
63
+ export default function NostoProvider(props: NostoProviderProps) {
64
+ const {
65
+ account,
66
+ multiCurrency = false,
67
+ host,
68
+ children,
69
+ recommendationComponent,
70
+ shopifyMarkets,
71
+ } = props
72
+ const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
73
+ const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
74
+
75
+ // Pass currentVariation as empty string if multiCurrency is disabled
76
+ const currentVariation = multiCurrency ? props.currentVariation : ""
77
+
78
+ // Set responseMode for loading campaigns:
79
+ const responseMode = isValidElement(recommendationComponent) ? "JSON_ORIGINAL" : "HTML"
80
+
81
+ // RecommendationComponent for client-side rendering:
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 }
134
+ }
135
+
136
+ useEffect(() => {
137
+ if (!window.nostojs) {
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))
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
+ }, [clientScriptLoadedState, shopifyMarkets])
204
+
205
+ return (
206
+ <NostoContext.Provider
207
+ value={{
208
+ account,
209
+ clientScriptLoaded,
210
+ currentVariation,
211
+ responseMode,
212
+ recommendationComponent,
213
+ useRenderCampaigns,
214
+ pageType,
215
+ }}
216
+ >
217
+ {children}
218
+ </NostoContext.Provider>
219
+ )
220
+ }
@@ -1,5 +1,5 @@
1
- import { useEffect } from "react";
2
- import { useNostoContext } from "../Provider/context.client";
1
+ import { useNostoContext } from "./context"
2
+ import { useNostoApi } from "../utils/hooks"
3
3
 
4
4
  /**
5
5
  * You can personalise your search pages by using the NostoSearch component.
@@ -22,42 +22,24 @@ import { useNostoContext } from "../Provider/context.client";
22
22
  * A query for `black shoes` must be provided as-is and not as `black+shoes`.
23
23
  * Doing so will lead to invalid results.
24
24
  *
25
- * @group Personalisation Components
25
+ * @group Components
26
26
  */
27
- export default function NostoSearch(props: { query: string }): JSX.Element {
28
- const { query } = props;
29
- const {
30
- clientScriptLoaded,
31
- currentVariation,
32
- responseMode,
33
- recommendationComponent,
34
- useRenderCampaigns,
35
- } = useNostoContext();
27
+ export default function NostoSearch(props: { query: string; placements?: string[] }) {
28
+ const { query, placements } = props
29
+ const { recommendationComponent, useRenderCampaigns } = useNostoContext()
36
30
 
37
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search");
31
+ const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search")
38
32
 
39
- useEffect(() => {
40
- if (clientScriptLoaded && pageTypeUpdated) {
41
- window.nostojs((api) => {
42
- api
43
- .defaultSession()
44
- .setVariation(currentVariation)
45
- .setResponseMode(responseMode)
46
- .viewSearch(query)
47
- .setPlacements(api.placements.getPlacements())
48
- .load()
49
- .then((data) => {
50
- renderCampaigns(data, api);
51
- });
52
- });
53
- }
54
- }, [
55
- clientScriptLoaded,
56
- currentVariation,
57
- query,
58
- recommendationComponent,
59
- pageTypeUpdated,
60
- ]);
33
+ useNostoApi(
34
+ async (api) => {
35
+ const data = await api.defaultSession()
36
+ .viewSearch(query)
37
+ .setPlacements(placements || api.placements.getPlacements())
38
+ .load()
39
+ renderCampaigns(data, api)
40
+ },
41
+ [query, recommendationComponent, pageTypeUpdated]
42
+ )
61
43
 
62
44
  return (
63
45
  <>
@@ -68,5 +50,5 @@ export default function NostoSearch(props: { query: string }): JSX.Element {
68
50
  {query}
69
51
  </div>
70
52
  </>
71
- );
53
+ )
72
54
  }
@@ -1,7 +1,7 @@
1
- import { useNostoContext } from "../Provider/context.client";
2
- import { Cart, Customer } from "../../types";
3
- import { snakeize } from "../../utils/snakeize";
4
- import { useDeepCompareEffect } from "../../utils/hooks";
1
+ import { useNostoContext } from "./context"
2
+ import { Cart, Customer } from "../types"
3
+ import { snakeize } from "../utils/snakeize"
4
+ import { useDeepCompareEffect } from "../utils/hooks"
5
5
 
6
6
  /**
7
7
  * 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.
@@ -13,29 +13,26 @@ import { useDeepCompareEffect } from "../../utils/hooks";
13
13
  *
14
14
  * @group Essential Functions
15
15
  */
16
- export default function NostoSession(props: {
17
- cart: Cart;
18
- customer: Customer;
19
- }): JSX.Element {
20
- const { cart, customer } = props;
21
- const { clientScriptLoaded } = useNostoContext();
16
+ export default function NostoSession(props: { cart: Cart; customer: Customer }) {
17
+ const { cart, customer } = props
18
+ const { clientScriptLoaded } = useNostoContext()
22
19
 
23
20
  useDeepCompareEffect(() => {
24
- const currentCart = cart ? snakeize(cart) : undefined;
25
- const currentCustomer = customer ? snakeize(customer) : undefined;
21
+ const currentCart = cart ? snakeize(cart) : undefined
22
+ const currentCustomer = customer ? snakeize(customer) : undefined
26
23
 
27
24
  if (clientScriptLoaded) {
28
- window.nostojs((api) => {
25
+ window.nostojs(api => {
29
26
  api
30
27
  .defaultSession()
31
28
  .setResponseMode("HTML")
32
29
  .setCart(currentCart)
33
30
  .setCustomer(currentCustomer)
34
31
  .viewOther()
35
- .load();
36
- });
32
+ .load()
33
+ })
37
34
  }
38
- }, [clientScriptLoaded, cart, customer]);
35
+ }, [clientScriptLoaded, cart, customer])
39
36
 
40
- return <></>;
37
+ return <></>
41
38
  }
@@ -0,0 +1,55 @@
1
+ import { createContext, useContext } from "react"
2
+ import { NostoClient, Recommendation } from "../types"
3
+
4
+ type AnyFunction = (...args: unknown[]) => unknown
5
+
6
+ /**
7
+ * @group Types
8
+ */
9
+ export interface NostoContextType {
10
+ account: string
11
+ clientScriptLoaded: boolean
12
+ currentVariation?: string
13
+ renderFunction?: AnyFunction
14
+ responseMode: string
15
+ recommendationComponent?: React.ReactElement<{
16
+ nostoRecommendation: Recommendation
17
+ }>
18
+ useRenderCampaigns(type: string): {
19
+ renderCampaigns(data: unknown, api: NostoClient): void
20
+ pageTypeUpdated: boolean
21
+ }
22
+ pageType: string
23
+ }
24
+
25
+ /**
26
+ * @group Essential Functions
27
+ */
28
+ export const NostoContext = createContext<NostoContextType>({
29
+ account: "",
30
+ currentVariation: "",
31
+ pageType: "",
32
+ responseMode: "HTML",
33
+ clientScriptLoaded: false,
34
+ useRenderCampaigns: () => {
35
+ return {
36
+ renderCampaigns: () => {},
37
+ pageTypeUpdated: false,
38
+ }
39
+ },
40
+ })
41
+
42
+ /**
43
+ * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
44
+ *
45
+ * @group Essential Functions
46
+ */
47
+ export function useNostoContext(): NostoContextType {
48
+ const context = useContext(NostoContext)
49
+
50
+ if (!context) {
51
+ throw new Error("No nosto context found")
52
+ }
53
+
54
+ return context
55
+ }
@@ -0,0 +1,14 @@
1
+ export { default as Nosto404 } from "./Nosto404"
2
+ export { default as NostoOther } from "./NostoOther"
3
+ export { default as NostoCheckout } from "./NostoCheckout"
4
+ export { default as NostoProduct } from "./NostoProduct"
5
+ export { default as NostoCategory } from "./NostoCategory"
6
+ export { default as NostoSearch } from "./NostoSearch"
7
+ export { default as NostoOrder } from "./NostoOrder"
8
+ export { default as NostoHome } from "./NostoHome"
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"
14
+ export { default as NostoSession } from "./NostoSession"
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { Buyer, Cart, Customer, Item, Product, Purchase, Recommendation, SKU } from "./types"
2
+
3
+ export * from "./components"
package/src/types.ts CHANGED
@@ -1,158 +1,173 @@
1
1
  declare global {
2
2
  interface Window {
3
3
  nosto?: {
4
- reload(settings: unknown): void;
5
- };
4
+ reload(settings: unknown): void
5
+ }
6
6
  nostojs: {
7
- (callback: (api: NostoClient) => void): void;
8
- q?: unknown[];
9
- };
7
+ (callback: (api: NostoClient) => void): void
8
+ q?: unknown[]
9
+ }
10
10
  }
11
11
  }
12
12
 
13
13
  /**
14
14
  * @group Types
15
15
  */
16
- export interface NostoClient {
17
- addOrder(order: { purchase: Purchase }): NostoClient;
18
- defaultSession(): NostoClient;
19
- setAutoLoad(autoload: boolean): NostoClient;
20
- setCart(cart?: Cart): NostoClient;
21
- setCustomer(customer?: Customer): NostoClient;
22
- setPlacements(placements: string[]): NostoClient;
23
- setResponseMode(mode: string): NostoClient;
24
- setVariation(variation: string): NostoClient;
25
- viewCategory(category: string): NostoClient;
26
- viewProduct(product: string): NostoClient;
27
- viewFrontPage(): NostoClient;
28
- viewNotFound(): NostoClient;
29
- viewOther(): NostoClient;
30
- viewSearch(query: string): NostoClient;
31
- viewCart(): NostoClient;
16
+ export interface Affinity {
17
+ name: string
18
+ score: number
19
+ }
20
+
21
+ /**
22
+ * @group Types
23
+ */
24
+ export interface SessionAction {
25
+ setPlacements(placements: string[]): SessionAction
32
26
  load(): Promise<{
33
- affinities: Record<
34
- string,
35
- {
36
- name: string;
37
- score: number;
38
- }[]
39
- >;
40
- geo_location?: string[];
41
- page_views: number;
42
- recommendations: Recommendation[];
43
- }>;
27
+ affinities: Record<string, Affinity[]>
28
+ geo_location?: string[]
29
+ page_views: number
30
+ recommendations: Recommendation[]
31
+ }>
32
+ }
33
+
34
+ /**
35
+ * @group Types
36
+ */
37
+ export interface NostoSession {
38
+ setCart(cart?: Cart): NostoSession
39
+ setCustomer(customer?: Customer): NostoSession
40
+ setResponseMode(mode: string): NostoSession
41
+ setVariation(variation?: string): NostoSession
42
+ addOrder(order: { purchase: Purchase }): SessionAction
43
+ viewCategory(category: string): SessionAction
44
+ viewProduct(product: string): SessionAction
45
+ viewFrontPage(): SessionAction
46
+ viewNotFound(): SessionAction
47
+ viewOther(): SessionAction
48
+ viewSearch(query: string): SessionAction
49
+ viewCart(): SessionAction
50
+ }
51
+
52
+ /**
53
+ * @group Types
54
+ */
55
+ export interface NostoClient {
56
+ setAutoLoad(autoload: boolean): NostoClient
57
+ defaultSession(): NostoSession
44
58
  placements: {
45
- getPlacements(): string[];
46
- };
59
+ getPlacements(): string[]
60
+ injectCampaigns(recommendations: Record<string, Recommendation>): void
61
+ }
47
62
  }
48
63
 
49
64
  /**
50
65
  * @group Types
51
66
  */
52
67
  export interface Recommendation {
53
- result_id: string;
54
- products: Product[];
55
- result_type: string;
56
- title: string;
57
- div_id: string;
58
- source_product_ids: string[];
59
- params: unknown;
68
+ result_id: string
69
+ products: Product[]
70
+ result_type: string
71
+ title: string
72
+ div_id: string
73
+ source_product_ids: string[]
74
+ params: unknown
60
75
  }
61
76
 
62
77
  /**
63
78
  * @group Types
64
79
  */
65
80
  export interface Item {
66
- name: string;
67
- price_currency_code: string;
68
- product_id: string;
69
- quantity: number;
70
- sku_id: string;
71
- unit_price: number;
81
+ name: string
82
+ price_currency_code: string
83
+ product_id: string
84
+ quantity: number
85
+ sku_id: string
86
+ unit_price: number
72
87
  }
73
88
 
74
89
  /**
75
90
  * @group Types
76
91
  */
77
92
  export interface Cart {
78
- items: Item[];
93
+ items: Item[]
79
94
  }
80
95
 
81
96
  /**
82
97
  * @group Types
83
98
  */
84
99
  export interface Customer {
85
- customer_reference: string;
86
- email: string;
87
- first_name: string;
88
- last_name: string;
89
- newsletter: boolean;
100
+ customer_reference: string
101
+ email: string
102
+ first_name: string
103
+ last_name: string
104
+ newsletter: boolean
90
105
  }
91
106
 
92
107
  /**
93
108
  * @group Types
94
109
  */
95
110
  export interface Buyer {
96
- first_name: string;
97
- last_name: string;
98
- email: string;
99
- type: string;
100
- newsletter: boolean;
111
+ first_name: string
112
+ last_name: string
113
+ email: string
114
+ type: string
115
+ newsletter: boolean
101
116
  }
102
117
 
103
118
  /**
104
119
  * @group Types
105
120
  */
106
121
  export interface Purchase {
107
- number: string;
108
- info: Buyer;
109
- items: Item[];
122
+ number: string
123
+ info: Buyer
124
+ items: Item[]
110
125
  }
111
126
 
112
127
  /**
113
128
  * @group Types
114
129
  */
115
130
  export interface SKU {
116
- id: string;
117
- name: string;
118
- price: number;
119
- listPrice?: number;
120
- url: URL;
121
- imageUrl: URL;
122
- gtin?: string;
123
- availability: "InStock" | "OutOfStock";
124
- customFields?: { [key: string]: string };
131
+ id: string
132
+ name: string
133
+ price: number
134
+ listPrice?: number
135
+ url: URL
136
+ imageUrl: URL
137
+ gtin?: string
138
+ availability: "InStock" | "OutOfStock"
139
+ customFields?: { [key: string]: string }
125
140
  }
126
141
 
127
142
  /**
128
143
  * @group Types
129
144
  */
130
145
  export interface Product {
131
- alternateImageUrls?: URL[];
132
- availability: "InStock" | "OutOfStock";
133
- brand?: string;
134
- category: string[];
135
- categoryIds?: string[];
136
- description: string;
137
- googleCategory?: string;
138
- condition?: string;
139
- gender?: string;
140
- ageGroup?: string;
141
- gtin?: string;
142
- imageUrl: URL;
143
- listPrice?: number;
144
- name: string;
145
- price: number;
146
- ratingValue?: number;
147
- reviewCount?: number;
148
- priceCurrencyCode: string;
149
- productId: string;
150
- tags1?: string[];
151
- tags2?: string[];
152
- tags3?: string[];
153
- thumbUrl?: URL;
154
- url: URL;
155
- customFields?: { [key: string]: string };
156
- variationId?: string;
157
- skus?: SKU[];
146
+ alternateImageUrls?: URL[]
147
+ availability: "InStock" | "OutOfStock"
148
+ brand?: string
149
+ category: string[]
150
+ categoryIds?: string[]
151
+ description: string
152
+ googleCategory?: string
153
+ condition?: string
154
+ gender?: string
155
+ ageGroup?: string
156
+ gtin?: string
157
+ imageUrl: URL
158
+ listPrice?: number
159
+ name: string
160
+ price: number
161
+ ratingValue?: number
162
+ reviewCount?: number
163
+ priceCurrencyCode: string
164
+ productId: string
165
+ tags1?: string[]
166
+ tags2?: string[]
167
+ tags3?: string[]
168
+ thumbUrl?: URL
169
+ url: URL
170
+ customFields?: { [key: string]: string }
171
+ variationId?: string
172
+ skus?: SKU[]
158
173
  }