@nok-integration/storefront-react 0.19.39 → 0.19.40

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 CHANGED
@@ -2089,6 +2089,10 @@ declare type CategoryPage = z.infer<typeof CategoryPage>;
2089
2089
  *
2090
2090
  * Fetching happens on mount, client-side, through the provider's client — so it inherits
2091
2091
  * the host's session and market without being told about either.
2092
+ *
2093
+ * The page is held under the CODE it belongs to and read back only while that code is
2094
+ * still the one being asked for, so a change of `code` never leaves the previous category
2095
+ * on screen while the next one loads.
2092
2096
  */
2093
2097
  export declare function CategoryView({ code, initialData, onNotFound }: CategoryViewProps): JSX.Element;
2094
2098
 
@@ -2100,6 +2104,10 @@ export declare interface CategoryViewProps {
2100
2104
  * synchronously on the first frame — no skeleton, no waterfall — and revalidated in the
2101
2105
  * background, because a server-rendered page can be arbitrarily old by the time it is
2102
2106
  * interactive and price and availability are the two things worst served by being stale.
2107
+ *
2108
+ * Matched against `code` on every render, not only at mount, so a host that swaps both
2109
+ * together has the new page painted immediately. A page for a different code is ignored
2110
+ * rather than shown.
2103
2111
  */
2104
2112
  initialData?: CategoryPage;
2105
2113
  /**
@@ -4803,6 +4811,19 @@ declare const Product: z.ZodObject<{
4803
4811
 
4804
4812
  declare type Product = z.infer<typeof Product>;
4805
4813
 
4814
+ declare class ProductCache {
4815
+ private readonly now;
4816
+ private readonly entries;
4817
+ /** Injectable so the age limit can be tested without leaning on timers. */
4818
+ constructor(now?: () => number);
4819
+ private static keyFor;
4820
+ get(sku: string, market?: Market): Product | undefined;
4821
+ set(product: Product, market?: Market): void;
4822
+ setMany(products: readonly Product[], market?: Market): void;
4823
+ /** Test seam. Nothing in the SDK calls this. */
4824
+ clear(): void;
4825
+ }
4826
+
4806
4827
  /**
4807
4828
  * 4.3 PDP interactive surface, aligned to the DAZN mobile PDP design. Sections render
4808
4829
  * from server-authoritative contract data: badges, title + price, promo nudge, description
@@ -4950,11 +4971,10 @@ declare type ProductVariant = z.infer<typeof ProductVariant>;
4950
4971
  /**
4951
4972
  * A whole product-detail screen, data fetching included — a **level 1 view**.
4952
4973
  *
4953
- * Fetches the product, then enriches it with inventory and recommendations. Those two are
4954
- * deliberately best-effort: the PDP renders perfectly well without either, so a failure in
4955
- * one must not fault the screen. `Promise.allSettled` rather than `all` is what makes that
4956
- * true with `all`, a recommendations outage would blank a page the fan could otherwise
4957
- * have bought from.
4974
+ * All of the fetching lives in {@link useProduct}, which is exported: a host composing its
4975
+ * own product screen from level-2 pieces gets the same behaviour rather than writing it
4976
+ * again. Switching between two items paints the new one immediately wherever the rail
4977
+ * already carried it, and never leaves the outgoing item up while the next one loads.
4958
4978
  *
4959
4979
  * `sku_not_found` is separated from the generic error path because it is not a fault: the
4960
4980
  * product is gone, and "this is no longer available" is the honest thing to say. An error
@@ -5538,6 +5558,8 @@ export declare function ShippingInfo(): JSX.Element;
5538
5558
  declare interface ShopContextValue {
5539
5559
  api: DaznClient;
5540
5560
  market?: Market;
5561
+ /** Products this provider has already been given — see {@link ProductCache}. */
5562
+ products: ProductCache;
5541
5563
  theme: 'light' | 'dark' | 'auto';
5542
5564
  notifications: 'internal' | 'host';
5543
5565
  locale?: string;
@@ -6395,6 +6417,56 @@ export declare function useMoney(defaultLocale?: string): {
6395
6417
  */
6396
6418
  export declare function useNavigate(): ((to: ShopRoute) => void) | null;
6397
6419
 
6420
+ /**
6421
+ * The data behind a product screen: the product, then inventory and recommendations.
6422
+ *
6423
+ * This is what {@link ProductView} is built from, exported so a host composing its own
6424
+ * product screen from the level-2 pieces does not have to write the fetching again. Two
6425
+ * properties are worth knowing about, because a hand-rolled version has to reproduce both.
6426
+ *
6427
+ * **Everything is held under the SKU it belongs to**, and read back only while that SKU is
6428
+ * still the one being asked for. A change of `sku` invalidates the whole set in the same
6429
+ * render that changes it, so the outgoing product is never left on screen while the next
6430
+ * one loads, and there is no frame in which one item's title sits above another item's
6431
+ * price.
6432
+ *
6433
+ * **A product already in memory is painted at once.** The recommendation rail carries whole
6434
+ * products, so opening one of them is usually drawn on the first frame and corrected a
6435
+ * moment later, rather than spending a round trip behind a skeleton. Inventory and
6436
+ * recommendations always come from the request — they are the two things worst served by
6437
+ * being remembered, and neither is needed to draw the top of the screen.
6438
+ *
6439
+ * Inventory and recommendations are best-effort: the screen renders perfectly well without
6440
+ * either, so a failure in one does not fault it.
6441
+ */ export declare function useProduct(sku: string, options?: UseProductOptions): UseProductResult;
6442
+
6443
+ export declare interface UseProductOptions {
6444
+ /**
6445
+ * An already-fetched product, from a host that server-rendered this screen. Painted on
6446
+ * the first frame and revalidated in the background.
6447
+ *
6448
+ * Read on every render, not only at mount, so a host that swaps `sku` and `initialData`
6449
+ * together has the new product painted immediately. It is matched against `sku` before
6450
+ * use: a product for a different SKU is ignored rather than shown.
6451
+ */
6452
+ initialData?: {
6453
+ product: Product;
6454
+ inventory?: Inventory | null;
6455
+ };
6456
+ /** Fetch the "you may also like" rail. Default `true`. */
6457
+ showRecommendations?: boolean;
6458
+ }
6459
+
6460
+ export declare interface UseProductResult {
6461
+ /** Null until there is something to draw for this SKU. */
6462
+ product: Product | null;
6463
+ inventory: Inventory | null;
6464
+ recommendations: Product[];
6465
+ /** Nothing to draw yet and nothing has gone wrong — it is still on its way. */
6466
+ loading: boolean;
6467
+ error: ReturnType<typeof codeOf> | undefined;
6468
+ }
6469
+
6398
6470
  /** The typed, schema-validating commerce client. Requires a provider. */
6399
6471
  export declare function useShopApi(): DaznClient;
6400
6472
 
package/dist/index.mjs CHANGED
@@ -5112,6 +5112,49 @@ function resolveAssetUrl(src, base) {
5112
5112
  if (!src.startsWith("/") || src.startsWith("//")) return src;
5113
5113
  return `${base.replace(/\/+$/, "")}${src}`;
5114
5114
  }
5115
+ const MAX_AGE_MS = 5 * 6e4;
5116
+ const CAPACITY = 60;
5117
+ class ProductCache {
5118
+ constructor(now = () => Date.now()) {
5119
+ this.now = now;
5120
+ }
5121
+ now;
5122
+ entries = /* @__PURE__ */ new Map();
5123
+ static keyFor(sku, market) {
5124
+ return `${market?.country ?? ""}\0${market?.lang ?? ""}\0${sku}`;
5125
+ }
5126
+ get(sku, market) {
5127
+ const key = ProductCache.keyFor(sku, market);
5128
+ const entry = this.entries.get(key);
5129
+ if (!entry) return void 0;
5130
+ if (this.now() - entry.at > MAX_AGE_MS) {
5131
+ this.entries.delete(key);
5132
+ return void 0;
5133
+ }
5134
+ this.entries.delete(key);
5135
+ this.entries.set(key, entry);
5136
+ return entry.product;
5137
+ }
5138
+ set(product, market) {
5139
+ const key = ProductCache.keyFor(product.sku, market);
5140
+ this.entries.delete(key);
5141
+ this.entries.set(key, {
5142
+ product,
5143
+ at: this.now()
5144
+ });
5145
+ while (this.entries.size > CAPACITY) {
5146
+ const oldest = this.entries.keys().next();
5147
+ if (oldest.done) break;
5148
+ this.entries.delete(oldest.value);
5149
+ }
5150
+ }
5151
+ setMany(products, market) {
5152
+ for (const product of products) this.set(product, market);
5153
+ }
5154
+ clear() {
5155
+ this.entries.clear();
5156
+ }
5157
+ }
5115
5158
  function createShopFetch(opts) {
5116
5159
  const baseFetch = opts.fetchImpl ?? globalThis.fetch;
5117
5160
  if (!baseFetch) throw new Error("No fetch implementation available");
@@ -5226,6 +5269,9 @@ function DaznShopProvider({ children, ...config }) {
5226
5269
  const getTokenRef = useRef(getToken);
5227
5270
  getTokenRef.current = getToken;
5228
5271
  const stableGetToken = useCallback(() => getTokenRef.current(), []);
5272
+ const productsRef = useRef(null);
5273
+ productsRef.current ??= new ProductCache();
5274
+ const products = productsRef.current;
5229
5275
  const emit = useCallback((event) => {
5230
5276
  for (const listener of listeners.current) {
5231
5277
  try {
@@ -5266,6 +5312,7 @@ function DaznShopProvider({ children, ...config }) {
5266
5312
  const value2 = useMemo(() => ({
5267
5313
  api,
5268
5314
  market,
5315
+ products,
5269
5316
  theme,
5270
5317
  notifications,
5271
5318
  locale,
@@ -5273,7 +5320,7 @@ function DaznShopProvider({ children, ...config }) {
5273
5320
  emit,
5274
5321
  subscribe,
5275
5322
  navigate
5276
- }), [api, market, theme, notifications, locale, assetBaseUrl, emit, subscribe, navigate]);
5323
+ }), [api, market, products, theme, notifications, locale, assetBaseUrl, emit, subscribe, navigate]);
5277
5324
  return jsx(ShopContext.Provider, {
5278
5325
  value: value2,
5279
5326
  children: jsx(ScreenTitleProvider, {
@@ -10710,32 +10757,51 @@ function CategoryContent({ page: page2 }) {
10710
10757
  })]
10711
10758
  });
10712
10759
  }
10760
+ function productsIn(page2) {
10761
+ const found = [];
10762
+ for (const block of page2.blocks) {
10763
+ if (block.kind === "rail" || block.kind === "grid") found.push(...block.products);
10764
+ else if (block.kind === "featured" || block.kind === "product_gallery") found.push(block.product);
10765
+ }
10766
+ return found;
10767
+ }
10713
10768
  function CategoryView({ code, initialData, onNotFound }) {
10714
10769
  const t = useT();
10715
- const { api, market, emit } = useShop();
10716
- const [page2, setPage] = useState(initialData ?? null);
10717
- const [error2, setError] = useState();
10718
- const [loading, setLoading] = useState(!initialData);
10770
+ const { api, market, emit, products } = useShop();
10771
+ const [loaded, setLoaded] = useState(null);
10772
+ const [failure, setFailure] = useState(null);
10773
+ const settled = loaded?.code === code;
10774
+ const seeded = initialData && initialData.code === code ? initialData : void 0;
10775
+ const page2 = (settled ? loaded?.page : void 0) ?? seeded ?? null;
10776
+ const error2 = failure?.code === code ? failure.error : void 0;
10777
+ const loading = !page2 && !error2 && !settled;
10719
10778
  useEffect(() => {
10720
10779
  let live = true;
10721
- setError(void 0);
10722
10780
  api.catalog.getCategoryPage(code, market).then((res) => {
10781
+ const fetched = res;
10782
+ products.setMany(productsIn(fetched), market);
10723
10783
  if (!live) return;
10724
- setPage(res);
10784
+ setLoaded({
10785
+ code,
10786
+ page: fetched
10787
+ });
10725
10788
  }).catch((err) => {
10726
10789
  if (!live) return;
10727
10790
  const c = codeOf(err);
10728
- setError(c);
10791
+ setFailure({
10792
+ code,
10793
+ error: c
10794
+ });
10729
10795
  emit({
10730
10796
  type: "error",
10731
10797
  code: c,
10732
10798
  scope: "category"
10733
10799
  });
10734
- }).finally(() => live && setLoading(false));
10800
+ });
10735
10801
  return () => {
10736
10802
  live = false;
10737
10803
  };
10738
- }, [api, code, market, emit]);
10804
+ }, [api, code, market, emit, products]);
10739
10805
  if (error2 === "category_not_found" && onNotFound) return jsx(Fragment, {
10740
10806
  children: onNotFound()
10741
10807
  });
@@ -10756,23 +10822,22 @@ function CategoryView({ code, initialData, onNotFound }) {
10756
10822
  })
10757
10823
  });
10758
10824
  }
10759
- function ProductView({ sku, initialData, showRecommendations = true, onNotFound }) {
10760
- const t = useT();
10761
- const { api, market, emit } = useShop();
10762
- const [loaded, setLoaded] = useState(initialData ? {
10763
- sku,
10764
- product: initialData.product,
10765
- inventory: initialData.inventory ?? null,
10766
- recommendations: []
10767
- } : null);
10825
+ function useProduct(sku, options2 = {}) {
10826
+ const { initialData, showRecommendations = true } = options2;
10827
+ const { api, market, emit, products } = useShop();
10828
+ const [loaded, setLoaded] = useState(null);
10768
10829
  const [failure, setFailure] = useState(null);
10769
10830
  const current2 = loaded?.sku === sku ? loaded : null;
10770
10831
  const error2 = failure?.sku === sku ? failure.code : void 0;
10832
+ const seeded = initialData && initialData.product.sku === sku ? initialData.product : products.get(sku, market);
10833
+ const product = current2?.product ?? seeded ?? null;
10834
+ const inventory2 = current2 ? current2.inventory : initialData && initialData.product.sku === sku ? initialData.inventory ?? null : null;
10771
10835
  useEffect(() => {
10772
10836
  let live = true;
10773
10837
  (async () => {
10774
10838
  try {
10775
10839
  const p = await api.catalog.getProduct(sku, market);
10840
+ products.set(p, market);
10776
10841
  if (!live) return;
10777
10842
  setLoaded({
10778
10843
  sku,
@@ -10784,12 +10849,14 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10784
10849
  const [inv, recs] = await Promise.allSettled([hasVariants ? Promise.resolve(null) : api.inventory.get(sku), showRecommendations ? api.catalog.getRecommendations(sku, market) : Promise.resolve({
10785
10850
  data: []
10786
10851
  })]);
10852
+ const recommendations = recs.status === "fulfilled" ? recs.value.data : [];
10853
+ products.setMany(recommendations, market);
10787
10854
  if (!live) return;
10788
10855
  setLoaded({
10789
10856
  sku,
10790
10857
  product: p,
10791
10858
  inventory: inv.status === "fulfilled" ? inv.value : null,
10792
- recommendations: recs.status === "fulfilled" ? recs.value.data : []
10859
+ recommendations
10793
10860
  });
10794
10861
  } catch (err) {
10795
10862
  if (!live) return;
@@ -10808,7 +10875,23 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10808
10875
  return () => {
10809
10876
  live = false;
10810
10877
  };
10811
- }, [api, sku, market, emit, showRecommendations]);
10878
+ }, [api, sku, market, emit, showRecommendations, products]);
10879
+ return {
10880
+ product,
10881
+ inventory: inventory2,
10882
+ recommendations: current2?.recommendations ?? [],
10883
+ loading: !product && !error2,
10884
+ error: error2
10885
+ };
10886
+ }
10887
+ function ProductView({ sku, initialData, showRecommendations = true, onNotFound }) {
10888
+ const t = useT();
10889
+ const { product, inventory: inventory2, recommendations, loading, error: error2 } = useProduct(sku, {
10890
+ ...initialData ? {
10891
+ initialData
10892
+ } : {},
10893
+ showRecommendations
10894
+ });
10812
10895
  const unavailable = jsx(ShopSurface, {
10813
10896
  tone: "light",
10814
10897
  fill: true,
@@ -10823,14 +10906,14 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10823
10906
  });
10824
10907
  return unavailable;
10825
10908
  }
10826
- if (current2) {
10909
+ if (product) {
10827
10910
  return jsx(ProductDetail, {
10828
- product: current2.product,
10829
- inventory: current2.inventory,
10830
- recommendations: current2.recommendations
10831
- });
10911
+ product,
10912
+ inventory: inventory2,
10913
+ recommendations
10914
+ }, product.sku);
10832
10915
  }
10833
- if (!error2) return jsx(ScreenSkeleton, {
10916
+ if (loading) return jsx(ScreenSkeleton, {
10834
10917
  tone: "light"
10835
10918
  });
10836
10919
  return jsx(ShopSurface, {
@@ -11634,6 +11717,7 @@ export {
11634
11717
  useCartSheet,
11635
11718
  useMoney,
11636
11719
  useNavigate,
11720
+ useProduct,
11637
11721
  useShopApi,
11638
11722
  useShopEvent,
11639
11723
  useShopOptional,
@@ -5112,6 +5112,49 @@ function resolveAssetUrl(src, base) {
5112
5112
  if (!src.startsWith("/") || src.startsWith("//")) return src;
5113
5113
  return `${base.replace(/\/+$/, "")}${src}`;
5114
5114
  }
5115
+ const MAX_AGE_MS = 5 * 6e4;
5116
+ const CAPACITY = 60;
5117
+ class ProductCache {
5118
+ constructor(now = () => Date.now()) {
5119
+ this.now = now;
5120
+ }
5121
+ now;
5122
+ entries = /* @__PURE__ */ new Map();
5123
+ static keyFor(sku, market) {
5124
+ return `${market?.country ?? ""}\0${market?.lang ?? ""}\0${sku}`;
5125
+ }
5126
+ get(sku, market) {
5127
+ const key = ProductCache.keyFor(sku, market);
5128
+ const entry = this.entries.get(key);
5129
+ if (!entry) return void 0;
5130
+ if (this.now() - entry.at > MAX_AGE_MS) {
5131
+ this.entries.delete(key);
5132
+ return void 0;
5133
+ }
5134
+ this.entries.delete(key);
5135
+ this.entries.set(key, entry);
5136
+ return entry.product;
5137
+ }
5138
+ set(product, market) {
5139
+ const key = ProductCache.keyFor(product.sku, market);
5140
+ this.entries.delete(key);
5141
+ this.entries.set(key, {
5142
+ product,
5143
+ at: this.now()
5144
+ });
5145
+ while (this.entries.size > CAPACITY) {
5146
+ const oldest = this.entries.keys().next();
5147
+ if (oldest.done) break;
5148
+ this.entries.delete(oldest.value);
5149
+ }
5150
+ }
5151
+ setMany(products, market) {
5152
+ for (const product of products) this.set(product, market);
5153
+ }
5154
+ clear() {
5155
+ this.entries.clear();
5156
+ }
5157
+ }
5115
5158
  function createShopFetch(opts) {
5116
5159
  const baseFetch = opts.fetchImpl ?? globalThis.fetch;
5117
5160
  if (!baseFetch) throw new Error("No fetch implementation available");
@@ -5226,6 +5269,9 @@ function DaznShopProvider({ children, ...config }) {
5226
5269
  const getTokenRef = useRef(getToken);
5227
5270
  getTokenRef.current = getToken;
5228
5271
  const stableGetToken = useCallback(() => getTokenRef.current(), []);
5272
+ const productsRef = useRef(null);
5273
+ productsRef.current ??= new ProductCache();
5274
+ const products = productsRef.current;
5229
5275
  const emit = useCallback((event) => {
5230
5276
  for (const listener of listeners.current) {
5231
5277
  try {
@@ -5266,6 +5312,7 @@ function DaznShopProvider({ children, ...config }) {
5266
5312
  const value2 = useMemo(() => ({
5267
5313
  api,
5268
5314
  market,
5315
+ products,
5269
5316
  theme,
5270
5317
  notifications,
5271
5318
  locale,
@@ -5273,7 +5320,7 @@ function DaznShopProvider({ children, ...config }) {
5273
5320
  emit,
5274
5321
  subscribe,
5275
5322
  navigate
5276
- }), [api, market, theme, notifications, locale, assetBaseUrl, emit, subscribe, navigate]);
5323
+ }), [api, market, products, theme, notifications, locale, assetBaseUrl, emit, subscribe, navigate]);
5277
5324
  return jsx(ShopContext.Provider, {
5278
5325
  value: value2,
5279
5326
  children: jsx(ScreenTitleProvider, {
@@ -10710,32 +10757,51 @@ function CategoryContent({ page: page2 }) {
10710
10757
  })]
10711
10758
  });
10712
10759
  }
10760
+ function productsIn(page2) {
10761
+ const found = [];
10762
+ for (const block of page2.blocks) {
10763
+ if (block.kind === "rail" || block.kind === "grid") found.push(...block.products);
10764
+ else if (block.kind === "featured" || block.kind === "product_gallery") found.push(block.product);
10765
+ }
10766
+ return found;
10767
+ }
10713
10768
  function CategoryView({ code, initialData, onNotFound }) {
10714
10769
  const t = useT();
10715
- const { api, market, emit } = useShop();
10716
- const [page2, setPage] = useState(initialData ?? null);
10717
- const [error2, setError] = useState();
10718
- const [loading, setLoading] = useState(!initialData);
10770
+ const { api, market, emit, products } = useShop();
10771
+ const [loaded, setLoaded] = useState(null);
10772
+ const [failure, setFailure] = useState(null);
10773
+ const settled = loaded?.code === code;
10774
+ const seeded = initialData && initialData.code === code ? initialData : void 0;
10775
+ const page2 = (settled ? loaded?.page : void 0) ?? seeded ?? null;
10776
+ const error2 = failure?.code === code ? failure.error : void 0;
10777
+ const loading = !page2 && !error2 && !settled;
10719
10778
  useEffect(() => {
10720
10779
  let live = true;
10721
- setError(void 0);
10722
10780
  api.catalog.getCategoryPage(code, market).then((res) => {
10781
+ const fetched = res;
10782
+ products.setMany(productsIn(fetched), market);
10723
10783
  if (!live) return;
10724
- setPage(res);
10784
+ setLoaded({
10785
+ code,
10786
+ page: fetched
10787
+ });
10725
10788
  }).catch((err) => {
10726
10789
  if (!live) return;
10727
10790
  const c = codeOf(err);
10728
- setError(c);
10791
+ setFailure({
10792
+ code,
10793
+ error: c
10794
+ });
10729
10795
  emit({
10730
10796
  type: "error",
10731
10797
  code: c,
10732
10798
  scope: "category"
10733
10799
  });
10734
- }).finally(() => live && setLoading(false));
10800
+ });
10735
10801
  return () => {
10736
10802
  live = false;
10737
10803
  };
10738
- }, [api, code, market, emit]);
10804
+ }, [api, code, market, emit, products]);
10739
10805
  if (error2 === "category_not_found" && onNotFound) return jsx(Fragment, {
10740
10806
  children: onNotFound()
10741
10807
  });
@@ -10756,23 +10822,22 @@ function CategoryView({ code, initialData, onNotFound }) {
10756
10822
  })
10757
10823
  });
10758
10824
  }
10759
- function ProductView({ sku, initialData, showRecommendations = true, onNotFound }) {
10760
- const t = useT();
10761
- const { api, market, emit } = useShop();
10762
- const [loaded, setLoaded] = useState(initialData ? {
10763
- sku,
10764
- product: initialData.product,
10765
- inventory: initialData.inventory ?? null,
10766
- recommendations: []
10767
- } : null);
10825
+ function useProduct(sku, options2 = {}) {
10826
+ const { initialData, showRecommendations = true } = options2;
10827
+ const { api, market, emit, products } = useShop();
10828
+ const [loaded, setLoaded] = useState(null);
10768
10829
  const [failure, setFailure] = useState(null);
10769
10830
  const current2 = loaded?.sku === sku ? loaded : null;
10770
10831
  const error2 = failure?.sku === sku ? failure.code : void 0;
10832
+ const seeded = initialData && initialData.product.sku === sku ? initialData.product : products.get(sku, market);
10833
+ const product = current2?.product ?? seeded ?? null;
10834
+ const inventory2 = current2 ? current2.inventory : initialData && initialData.product.sku === sku ? initialData.inventory ?? null : null;
10771
10835
  useEffect(() => {
10772
10836
  let live = true;
10773
10837
  (async () => {
10774
10838
  try {
10775
10839
  const p = await api.catalog.getProduct(sku, market);
10840
+ products.set(p, market);
10776
10841
  if (!live) return;
10777
10842
  setLoaded({
10778
10843
  sku,
@@ -10784,12 +10849,14 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10784
10849
  const [inv, recs] = await Promise.allSettled([hasVariants ? Promise.resolve(null) : api.inventory.get(sku), showRecommendations ? api.catalog.getRecommendations(sku, market) : Promise.resolve({
10785
10850
  data: []
10786
10851
  })]);
10852
+ const recommendations = recs.status === "fulfilled" ? recs.value.data : [];
10853
+ products.setMany(recommendations, market);
10787
10854
  if (!live) return;
10788
10855
  setLoaded({
10789
10856
  sku,
10790
10857
  product: p,
10791
10858
  inventory: inv.status === "fulfilled" ? inv.value : null,
10792
- recommendations: recs.status === "fulfilled" ? recs.value.data : []
10859
+ recommendations
10793
10860
  });
10794
10861
  } catch (err) {
10795
10862
  if (!live) return;
@@ -10808,7 +10875,23 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10808
10875
  return () => {
10809
10876
  live = false;
10810
10877
  };
10811
- }, [api, sku, market, emit, showRecommendations]);
10878
+ }, [api, sku, market, emit, showRecommendations, products]);
10879
+ return {
10880
+ product,
10881
+ inventory: inventory2,
10882
+ recommendations: current2?.recommendations ?? [],
10883
+ loading: !product && !error2,
10884
+ error: error2
10885
+ };
10886
+ }
10887
+ function ProductView({ sku, initialData, showRecommendations = true, onNotFound }) {
10888
+ const t = useT();
10889
+ const { product, inventory: inventory2, recommendations, loading, error: error2 } = useProduct(sku, {
10890
+ ...initialData ? {
10891
+ initialData
10892
+ } : {},
10893
+ showRecommendations
10894
+ });
10812
10895
  const unavailable = jsx(ShopSurface, {
10813
10896
  tone: "light",
10814
10897
  fill: true,
@@ -10823,14 +10906,14 @@ function ProductView({ sku, initialData, showRecommendations = true, onNotFound
10823
10906
  });
10824
10907
  return unavailable;
10825
10908
  }
10826
- if (current2) {
10909
+ if (product) {
10827
10910
  return jsx(ProductDetail, {
10828
- product: current2.product,
10829
- inventory: current2.inventory,
10830
- recommendations: current2.recommendations
10831
- });
10911
+ product,
10912
+ inventory: inventory2,
10913
+ recommendations
10914
+ }, product.sku);
10832
10915
  }
10833
- if (!error2) return jsx(ScreenSkeleton, {
10916
+ if (loading) return jsx(ScreenSkeleton, {
10834
10917
  tone: "light"
10835
10918
  });
10836
10919
  return jsx(ShopSurface, {
@@ -11634,6 +11717,7 @@ export {
11634
11717
  useCartSheet,
11635
11718
  useMoney,
11636
11719
  useNavigate,
11720
+ useProduct,
11637
11721
  useShopApi,
11638
11722
  useShopEvent,
11639
11723
  useShopOptional,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nok-integration/storefront-react",
3
- "version": "0.19.39",
3
+ "version": "0.19.40",
4
4
  "description": "Mountable React storefront components: catalogue, product detail and cart, mounted directly into a host DOM tree. No iframe.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {