@faststore/core 3.14.3 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.husky/_/husky.sh +30 -0
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/build-manifest.json +43 -43
  4. package/.next/cache/.tsbuildinfo +1 -1
  5. package/.next/cache/config.json +3 -3
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/server-production/0.pack +0 -0
  9. package/.next/cache/webpack/server-production/index.pack +0 -0
  10. package/.next/prerender-manifest.js +1 -1
  11. package/.next/prerender-manifest.json +1 -1
  12. package/.next/react-loadable-manifest.json +5 -5
  13. package/.next/routes-manifest.json +1 -1
  14. package/.next/server/middleware-build-manifest.js +1 -1
  15. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  16. package/.next/server/pages/[slug]/p.js +1 -1
  17. package/.next/server/pages/en-US/404.html +2 -2
  18. package/.next/server/pages/en-US/500.html +2 -2
  19. package/.next/server/pages/en-US/account.html +2 -2
  20. package/.next/server/pages/en-US/checkout.html +2 -2
  21. package/.next/server/pages/en-US/login.html +2 -2
  22. package/.next/server/pages/en-US/s.html +2 -2
  23. package/.next/server/pages/en-US.html +2 -2
  24. package/.next/server/pages-manifest.json +1 -1
  25. package/.next/static/I_svGPugEd8B39I034SPS/_buildManifest.js +1 -0
  26. package/.next/static/chunks/4750-e5a9418eaebfb4c8.js +28 -0
  27. package/.next/static/chunks/{5698-27d87aae8c109e79.js → 5698-fa65b2992114887e.js} +1 -1
  28. package/.next/static/chunks/{667-12b06e64d8d692d5.js → 667-53c75d8fc0155d58.js} +1 -1
  29. package/.next/static/chunks/{9466-2f8573654062ca33.js → 9466-e249304f28f683ae.js} +1 -1
  30. package/.next/static/chunks/{BannerNewsletter.cd67899702cefc74.js → BannerNewsletter.e9cddf0d9db5a289.js} +1 -1
  31. package/.next/static/chunks/{Newsletter.16d512eea4770e48.js → Newsletter.b145b5c84a40f69a.js} +1 -1
  32. package/.next/static/chunks/pages/{404-23b334702035ebee.js → 404-c90c20c167cc0e7a.js} +1 -1
  33. package/.next/static/chunks/pages/{500-2fbf273f5ce9ceca.js → 500-0cd22d12b85fffda.js} +1 -1
  34. package/.next/static/chunks/pages/{[...slug]-25f38fa57ace66f5.js → [...slug]-519b90475cfac29a.js} +1 -1
  35. package/.next/static/chunks/pages/[slug]/p-076e901f67a41f5a.js +1 -0
  36. package/.next/static/chunks/pages/{account-432a55e008dba859.js → account-978432926cc98c7f.js} +1 -1
  37. package/.next/static/chunks/pages/{checkout-d811420956257b8d.js → checkout-fc1167ff76bcc8ed.js} +1 -1
  38. package/.next/static/chunks/pages/{index-196c7f6cfa8ed5f3.js → index-5118f05c0bfc0853.js} +1 -1
  39. package/.next/static/chunks/pages/{login-66c720bdd1b3034e.js → login-fcb4f67351defd0e.js} +1 -1
  40. package/.next/static/chunks/pages/{s-91dbf07a75f40f28.js → s-ffb1e51598f8ad52.js} +1 -1
  41. package/.next/static/chunks/{webpack-1cdeb6168a2816ff.js → webpack-95968624e480b497.js} +1 -1
  42. package/.next/trace +103 -102
  43. package/.turbo/turbo-build.log +5 -5
  44. package/.turbo/turbo-test.log +5 -5
  45. package/CHANGELOG.md +6 -0
  46. package/package.json +11 -10
  47. package/src/components/sections/ProductDetails/ProductDetails.tsx +79 -73
  48. package/src/pages/[slug]/p.tsx +42 -7
  49. package/src/sdk/offer/aggregate.ts +48 -0
  50. package/src/sdk/offer/enhance.ts +20 -0
  51. package/src/sdk/offer/fetcher.ts +19 -0
  52. package/src/sdk/offer/index.ts +45 -0
  53. package/src/sdk/offer/sort.ts +28 -0
  54. package/src/sdk/product/useProductLink.ts +1 -3
  55. package/.next/static/chunks/4579-0b95def0ee1ecb0f.js +0 -33
  56. package/.next/static/chunks/pages/[slug]/p-a3a9ea694ac3b322.js +0 -1
  57. package/.next/static/l2e99PO5q3-lFPgwMNoFo/_buildManifest.js +0 -1
  58. /package/.next/static/{l2e99PO5q3-lFPgwMNoFo → I_svGPugEd8B39I034SPS}/_ssgManifest.js +0 -0
@@ -47,11 +47,11 @@ Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Cont
47
47
  Collecting build traces ...
48
48
 
49
49
  Route (pages) Size First Load JS
50
- ┌ ● / 3.36 kB 122 kB
50
+ ┌ ● / 3.36 kB 123 kB
51
51
  ├ └ css/b1806cbafd0c1f81.css 3.06 kB
52
52
  ├ /_app 0 B 92 kB
53
53
  ├ ● /[...slug] 2.09 kB 131 kB
54
- ├ ● /[slug]/p 34.1 kB 153 kB
54
+ ├ ● /[slug]/p 34.9 kB 154 kB
55
55
  ├ ├ css/bf1560439df2c1a1.css 5.66 kB
56
56
  ├ ├ css/e3ff5d95518a5c79.css 6.01 kB
57
57
  ├ └ css/d23b961580181439.css 15.6 kB
@@ -62,18 +62,18 @@ Route (pages) Size First Load JS
62
62
  ├ λ /api/health/live 0 B 92 kB
63
63
  ├ λ /api/health/ready 0 B 92 kB
64
64
  ├ λ /api/preview 0 B 92 kB
65
- ├ ● /checkout 690 B 120 kB
65
+ ├ ● /checkout 693 B 120 kB
66
66
  ├ ● /login 1.59 kB 121 kB
67
67
  └ ● /s 3.16 kB 132 kB
68
68
  + First Load JS shared by all 95.1 kB
69
69
  ├ chunks/framework-12a146e94cfcf7c4.js 45.4 kB
70
70
  ├ chunks/main-bcf4c5804681e40a.js 33.1 kB
71
71
  ├ chunks/pages/_app-fa96080af43b51d4.js 10 kB
72
- ├ chunks/webpack-1cdeb6168a2816ff.js 3.57 kB
72
+ ├ chunks/webpack-95968624e480b497.js 3.57 kB
73
73
  └ css/2eafb8997a3946dc.css 3.07 kB
74
74
 
75
75
  λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
76
76
  ○ (Static) automatically rendered as static HTML (uses no initial props)
77
77
  ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
78
78
 
79
- Done in 53.90s.
79
+ Done in 55.72s.
@@ -1,12 +1,12 @@
1
1
  yarn run v1.22.22
2
2
  $ jest
3
- PASS test/server/cms/index.test.ts (33.238 s)
4
- PASS test/utils/multipleTemplates.test.ts (33.634 s)
5
- PASS test/server/index.test.ts (38.311 s)
3
+ PASS test/server/cms/index.test.ts (36.98 s)
4
+ PASS test/utils/multipleTemplates.test.ts (37.179 s)
5
+ PASS test/server/index.test.ts (40.48 s)
6
6
 
7
7
  Test Suites: 3 passed, 3 total
8
8
  Tests: 19 passed, 19 total
9
9
  Snapshots: 0 total
10
- Time: 39.476 s
10
+ Time: 41.692 s
11
11
  Ran all test suites.
12
- Done in 41.22s.
12
+ Done in 43.67s.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.15.0 (2025-02-04)
7
+
8
+ ### Features
9
+
10
+ - Offer SDK ([#2575](https://github.com/vtex/faststore/issues/2575)) ([f4e11ac](https://github.com/vtex/faststore/commit/f4e11ac68ae4afd79a1a89c122155e18bc452ad5))
11
+
6
12
  ## [3.14.3](https://github.com/vtex/faststore/compare/v3.14.2...v3.14.3) (2025-02-04)
7
13
 
8
14
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.14.3",
3
+ "version": "3.15.0",
4
4
  "license": "MIT",
5
5
  "repository": "vtex/faststore",
6
6
  "browserslist": "supports es6-module and not dead",
@@ -43,12 +43,12 @@
43
43
  "@envelop/graphql-jit": "^8.0.3",
44
44
  "@envelop/parser-cache": "^6.0.2",
45
45
  "@envelop/validation-cache": "^6.0.2",
46
- "@faststore/api": "^3.14.2",
47
- "@faststore/components": "^3.14.2",
48
- "@faststore/graphql-utils": "^3.14.2",
49
- "@faststore/lighthouse": "^3.14.2",
50
- "@faststore/sdk": "^3.14.2",
51
- "@faststore/ui": "^3.14.2",
46
+ "@faststore/api": "^3.15.0",
47
+ "@faststore/components": "^3.15.0",
48
+ "@faststore/graphql-utils": "^3.15.0",
49
+ "@faststore/lighthouse": "^3.15.0",
50
+ "@faststore/sdk": "^3.15.0",
51
+ "@faststore/ui": "^3.15.0",
52
52
  "@graphql-codegen/cli": "5.0.2",
53
53
  "@graphql-codegen/client-preset": "4.2.6",
54
54
  "@graphql-codegen/typescript": "4.0.7",
@@ -80,8 +80,9 @@
80
80
  "sass-loader": "^12.6.0",
81
81
  "sharp": "^0.32.6",
82
82
  "style-loader": "^3.3.1",
83
- "swr": "^1.3.0",
84
- "tsx": "^4.6.2"
83
+ "swr": "^2.2.5",
84
+ "tsx": "^4.6.2",
85
+ "typescript": "4.7.3"
85
86
  },
86
87
  "devDependencies": {
87
88
  "@cypress/code-coverage": "^3.12.1",
@@ -101,5 +102,5 @@
101
102
  "jest": "^29.7.0",
102
103
  "ts-jest": "29.1.1"
103
104
  },
104
- "gitHead": "caf7f74467d49bcb3b9d648983506088afc07711"
105
+ "gitHead": "2cf5890d1b5edd9fbf7c9e7e94fbba57771c2a0a"
105
106
  }
@@ -11,11 +11,22 @@ import Section from '../Section'
11
11
 
12
12
  import styles from './section.module.scss'
13
13
 
14
+ import storeConfig from 'discovery.config'
14
15
  import { getOverridableSection } from '../../../sdk/overrides/getOverriddenSection'
15
16
  import { useOverrideComponents } from '../../../sdk/overrides/OverrideContext'
16
17
  import { usePDP } from '../../../sdk/overrides/PageProvider'
17
18
  import { ProductDetailsDefaultComponents } from './DefaultComponents'
18
19
 
20
+ type StoreConfig = typeof storeConfig & {
21
+ experimental: {
22
+ revalidate?: number
23
+ enableClientOffer?: boolean
24
+ }
25
+ }
26
+
27
+ const isClientOfferEnabled = (storeConfig as StoreConfig).experimental
28
+ .enableClientOffer
29
+
19
30
  export interface ProductDetailsProps {
20
31
  productTitle: {
21
32
  refNumber: boolean
@@ -214,82 +225,77 @@ function ProductDetails({
214
225
  {...ImageGallery.props}
215
226
  images={productImages}
216
227
  />
217
- <section data-fs-product-details-info>
218
- <section
219
- data-fs-product-details-settings
220
- data-fs-product-details-section
221
- >
222
- <ProductDetailsSettings.Component
223
- buyButtonTitle={buyButtonTitle}
224
- buyButtonIcon={buyButtonIcon}
225
- notAvailableButtonTitle={
226
- notAvailableButtonTitle ?? NotAvailableButton.props.title
227
- }
228
- useUnitMultiplier={quantitySelector?.useUnitMultiplier ?? false}
229
- {...ProductDetailsSettings.props}
230
- // Dynamic props shouldn't be overridable
231
- // This decision can be reviewed later if needed
232
- quantity={quantity}
233
- setQuantity={setQuantity}
234
- product={product}
235
- isValidating={isValidating}
236
- taxesConfiguration={taxesConfiguration}
237
- />
238
-
239
- {skuMatrix?.shouldDisplaySKUMatrix &&
240
- Object.keys(slugsMap).length > 1 && (
241
- <>
242
- <div data-fs-product-details-settings-separator>
243
- {skuMatrix.separatorButtonsText}
244
- </div>
245
-
246
- <SKUMatrix.Component>
247
- <SKUMatrixTrigger.Component disabled={isValidating}>
248
- {skuMatrix.triggerButtonLabel}
249
- </SKUMatrixTrigger.Component>
250
228
 
251
- <SKUMatrixSidebar.Component
252
- formatter={useFormattedPrice}
253
- columns={skuMatrix.columns}
254
- overlayProps={{ className: styles.section }}
255
- />
256
- </SKUMatrix.Component>
257
- </>
258
- )}
229
+ {isClientOfferEnabled && isValidating ? (
230
+ <section data-fs-product-details-info>
231
+ <section
232
+ data-fs-product-details-settings
233
+ data-fs-product-details-section
234
+ >
235
+ <p>Loading...</p>
236
+ </section>
259
237
  </section>
260
-
261
- {!outOfStock && (
262
- <ShippingSimulation.Component
238
+ ) : (
239
+ <section data-fs-product-details-info>
240
+ <section
241
+ data-fs-product-details-settings
263
242
  data-fs-product-details-section
264
- data-fs-product-details-shipping
265
- formatter={useFormattedPrice}
266
- {...ShippingSimulation.props}
267
- idkPostalCodeLinkProps={{
268
- ...ShippingSimulation.props.idkPostalCodeLinkProps,
269
- href:
270
- shippingSimulatorLinkUrl ??
271
- ShippingSimulation.props.idkPostalCodeLinkProps?.href,
272
- children:
273
- shippingSimulatorLinkText ??
274
- ShippingSimulation.props.idkPostalCodeLinkProps?.children,
275
- }}
276
- productShippingInfo={{
277
- id,
278
- quantity,
279
- seller: seller.identifier,
280
- }}
281
- title={shippingSimulatorTitle ?? ShippingSimulation.props.title}
282
- inputLabel={
283
- shippingSimulatorInputLabel ??
284
- ShippingSimulation.props.inputLabel
285
- }
286
- optionsLabel={
287
- shippingSimulatorOptionsTableTitle ??
288
- ShippingSimulation.props.optionsLabel
289
- }
290
- />
291
- )}
292
- </section>
243
+ >
244
+ <ProductDetailsSettings.Component
245
+ buyButtonTitle={buyButtonTitle}
246
+ buyButtonIcon={buyButtonIcon}
247
+ notAvailableButtonTitle={
248
+ notAvailableButtonTitle ?? NotAvailableButton.props.title
249
+ }
250
+ useUnitMultiplier={
251
+ quantitySelector?.useUnitMultiplier ?? false
252
+ }
253
+ {...ProductDetailsSettings.props}
254
+ // Dynamic props shouldn't be overridable
255
+ // This decision can be reviewed later if needed
256
+ quantity={quantity}
257
+ setQuantity={setQuantity}
258
+ product={product}
259
+ isValidating={isValidating}
260
+ taxesConfiguration={taxesConfiguration}
261
+ />
262
+ </section>
263
+
264
+ {!outOfStock && (
265
+ <ShippingSimulation.Component
266
+ data-fs-product-details-section
267
+ data-fs-product-details-shipping
268
+ formatter={useFormattedPrice}
269
+ {...ShippingSimulation.props}
270
+ idkPostalCodeLinkProps={{
271
+ ...ShippingSimulation.props.idkPostalCodeLinkProps,
272
+ href:
273
+ shippingSimulatorLinkUrl ??
274
+ ShippingSimulation.props.idkPostalCodeLinkProps?.href,
275
+ children:
276
+ shippingSimulatorLinkText ??
277
+ ShippingSimulation.props.idkPostalCodeLinkProps?.children,
278
+ }}
279
+ productShippingInfo={{
280
+ id,
281
+ quantity,
282
+ seller: seller.identifier,
283
+ }}
284
+ title={
285
+ shippingSimulatorTitle ?? ShippingSimulation.props.title
286
+ }
287
+ inputLabel={
288
+ shippingSimulatorInputLabel ??
289
+ ShippingSimulation.props.inputLabel
290
+ }
291
+ optionsLabel={
292
+ shippingSimulatorOptionsTableTitle ??
293
+ ShippingSimulation.props.optionsLabel
294
+ }
295
+ />
296
+ )}
297
+ </section>
298
+ )}
293
299
 
294
300
  {shouldDisplayProductDescription && (
295
301
  <ProductDescription.Component
@@ -3,6 +3,7 @@ import type { Locator } from '@vtex/client-cms'
3
3
  import deepmerge from 'deepmerge'
4
4
  import type { GetStaticPaths, GetStaticProps } from 'next'
5
5
  import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo'
6
+ import Head from 'next/head'
6
7
  import type { ComponentType } from 'react'
7
8
 
8
9
  import { gql } from '@generated'
@@ -21,19 +22,27 @@ import { OverriddenDefaultNewsletter as Newsletter } from 'src/components/sectio
21
22
  import { OverriddenDefaultProductDetails as ProductDetails } from 'src/components/sections/ProductDetails/OverriddenDefaultProductDetails'
22
23
  import { OverriddenDefaultProductShelf as ProductShelf } from 'src/components/sections/ProductShelf/OverriddenDefaultProductShelf'
23
24
  import ProductTiles from 'src/components/sections/ProductTiles'
24
- import PLUGINS_COMPONENTS from 'src/plugins'
25
25
  import CUSTOM_COMPONENTS from 'src/customizations/src/components'
26
+ import PLUGINS_COMPONENTS from 'src/plugins'
26
27
  import { useSession } from 'src/sdk/session'
27
28
  import { execute } from 'src/server'
28
29
 
29
30
  import storeConfig from 'discovery.config'
30
31
  import {
31
- type GlobalSectionsData,
32
32
  getGlobalSectionsData,
33
+ type GlobalSectionsData,
33
34
  } from 'src/components/cms/GlobalSections'
35
+ import { getOfferUrl, useOffer } from 'src/sdk/offer'
34
36
  import PageProvider, { type PDPContext } from 'src/sdk/overrides/PageProvider'
35
37
  import { useProductQuery } from 'src/sdk/product/useProductQuery'
36
- import { type PDPContentType, getPDP } from 'src/server/cms/pdp'
38
+ import { getPDP, type PDPContentType } from 'src/server/cms/pdp'
39
+
40
+ type StoreConfig = typeof storeConfig & {
41
+ experimental: {
42
+ revalidate?: number
43
+ enableClientOffer?: boolean
44
+ }
45
+ }
37
46
 
38
47
  /**
39
48
  * Sections: Components imported from each store's custom components and '../components/sections' only.
@@ -68,15 +77,31 @@ type Props = PDPContentType & {
68
77
  // https://www.npmjs.com/package/deepmerge
69
78
  const overwriteMerge = (_: any[], sourceArray: any[]) => sourceArray
70
79
 
80
+ const isClientOfferEnabled = (storeConfig as StoreConfig).experimental
81
+ .enableClientOffer
82
+
71
83
  function Page({ data: server, sections, globalSections, offers, meta }: Props) {
72
84
  const { product } = server
73
85
  const { currency } = useSession()
74
86
  const titleTemplate = storeConfig?.seo?.titleTemplate ?? ''
75
87
 
76
- // Stale while revalidate the product for fetching the new price etc
77
- const { data: client, isValidating } = useProductQuery(product.id, {
78
- product: product,
79
- })
88
+ const { client, isValidating } = isClientOfferEnabled
89
+ ? (() => {
90
+ const offer = useOffer({ skuId: product.sku })
91
+ return {
92
+ client: { product: { offers: offer.offers } },
93
+ isValidating: offer.isValidating,
94
+ }
95
+ })()
96
+ : (() => {
97
+ const productQuery = useProductQuery(product.id, {
98
+ product: product,
99
+ })
100
+ return {
101
+ client: productQuery.data,
102
+ isValidating: productQuery.isValidating,
103
+ }
104
+ })()
80
105
 
81
106
  const context = {
82
107
  data: {
@@ -87,6 +112,15 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) {
87
112
 
88
113
  return (
89
114
  <>
115
+ <Head>
116
+ <link
117
+ rel="preload"
118
+ href={getOfferUrl(product.sku)}
119
+ as="fetch"
120
+ crossOrigin="anonymous"
121
+ fetchPriority="high"
122
+ />
123
+ </Head>
90
124
  {/* SEO */}
91
125
  <NextSeo
92
126
  title={meta.title}
@@ -271,6 +305,7 @@ export const getStaticProps: GetStaticProps<
271
305
  globalSections,
272
306
  key: seo.canonical,
273
307
  },
308
+ revalidate: (storeConfig as StoreConfig).experimental.revalidate ?? 0,
274
309
  }
275
310
  }
276
311
 
@@ -0,0 +1,48 @@
1
+ import type { Item, Seller } from '@faststore/api'
2
+ import type { EnhancedCommercialOffer } from './enhance'
3
+ import { inStock, price } from './sort'
4
+
5
+ type Root = EnhancedCommercialOffer<Seller, Item>
6
+
7
+ const withTax = (price: number, tax = 0, unitMultiplier = 1) => {
8
+ const unitTax = tax / unitMultiplier
9
+ return Math.round((price + unitTax) * 100) / 100
10
+ }
11
+
12
+ const getHighPrice = (
13
+ offers: Root[],
14
+ options: { includeTaxes: boolean } = { includeTaxes: false }
15
+ ) => {
16
+ const availableOffers = offers.filter(inStock)
17
+ const highOffer = availableOffers[availableOffers.length - 1]
18
+ const highPrice = highOffer ? price(highOffer) : 0
19
+ if (!options.includeTaxes) {
20
+ return highPrice
21
+ }
22
+
23
+ return withTax(highPrice, highOffer?.Tax, highOffer?.product?.unitMultiplier)
24
+ }
25
+
26
+ const getLowPrice = (
27
+ offers: Root[],
28
+ options: { includeTaxes: boolean } = { includeTaxes: false }
29
+ ) => {
30
+ const [lowOffer] = offers.filter(inStock)
31
+
32
+ const lowPrice = lowOffer ? price(lowOffer) : 0
33
+
34
+ if (!options.includeTaxes) {
35
+ return lowPrice
36
+ }
37
+
38
+ return withTax(lowPrice, lowOffer?.Tax, lowOffer?.product?.unitMultiplier)
39
+ }
40
+
41
+ export function aggregateOffer(offers: Root[]) {
42
+ return {
43
+ highPrice: getHighPrice(offers),
44
+ lowPrice: getLowPrice(offers),
45
+ lowPriceWithTaxes: getLowPrice(offers, { includeTaxes: true }),
46
+ offerCount: offers.length,
47
+ }
48
+ }
@@ -0,0 +1,20 @@
1
+ import type { CommertialOffer } from '@faststore/api'
2
+
3
+ export type EnhancedCommercialOffer<S, P> = CommertialOffer & {
4
+ seller: S
5
+ product: P
6
+ }
7
+
8
+ export const enhanceCommercialOffer = <S, P>({
9
+ offer,
10
+ seller,
11
+ product,
12
+ }: {
13
+ offer: CommertialOffer
14
+ seller: S
15
+ product: P
16
+ }): EnhancedCommercialOffer<S, P> => ({
17
+ ...offer,
18
+ product,
19
+ seller,
20
+ })
@@ -0,0 +1,19 @@
1
+ import type { ProductSearchResult } from '@faststore/api'
2
+ import { api, storeUrl } from '../../../discovery.config'
3
+
4
+ const IS_PROD = process.env.NODE_ENV === 'production'
5
+
6
+ export function getUrl(skuId: string) {
7
+ const base = IS_PROD
8
+ ? storeUrl
9
+ : `https://${api.storeId}.${api.environment}.com.br`
10
+ const url = new URL(`${base}/api/intelligent-search/product_search`)
11
+ url.searchParams.append('query', `sku.id:${skuId}`)
12
+ return url.toString()
13
+ }
14
+
15
+ export async function fetcher(skuId: string) {
16
+ return fetch(getUrl(skuId)).then((res) =>
17
+ res.json()
18
+ ) as Promise<ProductSearchResult>
19
+ }
@@ -0,0 +1,45 @@
1
+ import useSWR from 'swr'
2
+ import { aggregateOffer } from './aggregate'
3
+ import { enhanceCommercialOffer } from './enhance'
4
+ import { fetcher } from './fetcher'
5
+ import { bestOfferFirst } from './sort'
6
+ export { getUrl as getOfferUrl } from './fetcher'
7
+
8
+ const ERROR_DATA = { offers: {}, isValidating: false }
9
+
10
+ export function useOffer(args: { skuId: string }) {
11
+ const { data, error, isValidating } = useSWR(args.skuId, fetcher)
12
+
13
+ if (error || !data || data.products.length === 0) {
14
+ console.warn('Error or no data fetching offer to SKU', args.skuId, error)
15
+ return ERROR_DATA
16
+ }
17
+
18
+ const product = data.products[0]
19
+
20
+ if (!product || product.items.length === 0) {
21
+ console.warn('Product not found or has no items for SKU', args.skuId)
22
+ return ERROR_DATA
23
+ }
24
+
25
+ const item = product.items.find((item) => item.itemId === args.skuId)
26
+
27
+ if (!item) {
28
+ console.warn('Item not found for SKU', args.skuId)
29
+ return ERROR_DATA
30
+ }
31
+
32
+ const sellers = item.sellers
33
+ .map((seller) =>
34
+ enhanceCommercialOffer({
35
+ offer: seller.commertialOffer,
36
+ seller,
37
+ product: item,
38
+ })
39
+ )
40
+ .sort(bestOfferFirst)
41
+
42
+ const offers = aggregateOffer(sellers)
43
+
44
+ return { offers, isValidating }
45
+ }
@@ -0,0 +1,28 @@
1
+ import type { CommertialOffer } from '@faststore/api'
2
+
3
+ export const inStock = (offer: Pick<CommertialOffer, 'AvailableQuantity'>) =>
4
+ offer.AvailableQuantity > 0
5
+
6
+ export const price = (offer: Pick<CommertialOffer, 'spotPrice'>) =>
7
+ offer.spotPrice ?? 0
8
+
9
+ export const availability = (available: boolean) =>
10
+ available ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'
11
+
12
+ export const bestOfferFirst = (
13
+ a: Pick<CommertialOffer, 'AvailableQuantity' | 'spotPrice'>,
14
+ b: Pick<CommertialOffer, 'AvailableQuantity' | 'spotPrice'>
15
+ ) => {
16
+ if (inStock(a) && !inStock(b)) {
17
+ return -1
18
+ }
19
+
20
+ if (!inStock(a) && inStock(b)) {
21
+ return 1
22
+ }
23
+
24
+ return price(a) - price(b)
25
+ }
26
+
27
+ export const inStockOrderFormItem = (itemAvailability: string) =>
28
+ itemAvailability === 'available'
@@ -1,8 +1,6 @@
1
1
  import type { CurrencyCode, SelectItemEvent } from '@faststore/sdk'
2
- import { useCallback } from 'react'
3
-
4
2
  import type { ProductSummary_ProductFragment } from '@generated/graphql'
5
-
3
+ import { useCallback } from 'react'
6
4
  import type { AnalyticsItem, SearchSelectItemEvent } from '../analytics/types'
7
5
  import { useSession } from '../session'
8
6