Skip to content

Commit f4e11ac

Browse files
emersonlaurentinoeduardoformigaPedro Soares
authored
feat: Offer SDK (#2575)
Co-authored-by: eduardoformiga <[email protected]> Co-authored-by: Pedro Soares <[email protected]>
1 parent caf7f74 commit f4e11ac

File tree

13 files changed

+340
-89
lines changed

13 files changed

+340
-89
lines changed

packages/api/src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,10 @@ export const getSchema = async (options: Options) => {
4848

4949
export * from './platforms/vtex/resolvers/root'
5050
export type { Resolver } from './platforms/vtex'
51+
52+
export type {
53+
CommertialOffer,
54+
Item,
55+
ProductSearchResult,
56+
Seller,
57+
} from './platforms/vtex/clients/search/types/ProductSearchResult'

packages/core/.husky/_/husky.sh

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/sh
2+
if [ -z "$husky_skip_init" ]; then
3+
debug () {
4+
[ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
5+
}
6+
7+
readonly hook_name="$(basename "$0")"
8+
debug "starting $hook_name..."
9+
10+
if [ "$HUSKY" = "0" ]; then
11+
debug "HUSKY env variable is set to 0, skipping hook"
12+
exit 0
13+
fi
14+
15+
if [ -f ~/.huskyrc ]; then
16+
debug "sourcing ~/.huskyrc"
17+
. ~/.huskyrc
18+
fi
19+
20+
export readonly husky_skip_init=1
21+
sh -e "$0" "$@"
22+
exitCode="$?"
23+
24+
if [ $exitCode != 0 ]; then
25+
echo "husky - $hook_name hook exited with code $exitCode (error)"
26+
exit $exitCode
27+
fi
28+
29+
exit 0
30+
fi

packages/core/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@
8080
"sass-loader": "^12.6.0",
8181
"sharp": "^0.32.6",
8282
"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"
8586
},
8687
"devDependencies": {
8788
"@cypress/code-coverage": "^3.12.1",

packages/core/src/components/sections/ProductDetails/ProductDetails.tsx

+79-73
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,22 @@ import Section from '../Section'
1111

1212
import styles from './section.module.scss'
1313

14+
import storeConfig from 'discovery.config'
1415
import { getOverridableSection } from '../../../sdk/overrides/getOverriddenSection'
1516
import { useOverrideComponents } from '../../../sdk/overrides/OverrideContext'
1617
import { usePDP } from '../../../sdk/overrides/PageProvider'
1718
import { ProductDetailsDefaultComponents } from './DefaultComponents'
1819

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+
1930
export interface ProductDetailsProps {
2031
productTitle: {
2132
refNumber: boolean
@@ -214,82 +225,77 @@ function ProductDetails({
214225
{...ImageGallery.props}
215226
images={productImages}
216227
/>
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>
250228

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>
259237
</section>
260-
261-
{!outOfStock && (
262-
<ShippingSimulation.Component
238+
) : (
239+
<section data-fs-product-details-info>
240+
<section
241+
data-fs-product-details-settings
263242
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+
)}
293299

294300
{shouldDisplayProductDescription && (
295301
<ProductDescription.Component

packages/core/src/pages/[slug]/p.tsx

+42-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Locator } from '@vtex/client-cms'
33
import deepmerge from 'deepmerge'
44
import type { GetStaticPaths, GetStaticProps } from 'next'
55
import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo'
6+
import Head from 'next/head'
67
import type { ComponentType } from 'react'
78

89
import { gql } from '@generated'
@@ -21,19 +22,27 @@ import { OverriddenDefaultNewsletter as Newsletter } from 'src/components/sectio
2122
import { OverriddenDefaultProductDetails as ProductDetails } from 'src/components/sections/ProductDetails/OverriddenDefaultProductDetails'
2223
import { OverriddenDefaultProductShelf as ProductShelf } from 'src/components/sections/ProductShelf/OverriddenDefaultProductShelf'
2324
import ProductTiles from 'src/components/sections/ProductTiles'
24-
import PLUGINS_COMPONENTS from 'src/plugins'
2525
import CUSTOM_COMPONENTS from 'src/customizations/src/components'
26+
import PLUGINS_COMPONENTS from 'src/plugins'
2627
import { useSession } from 'src/sdk/session'
2728
import { execute } from 'src/server'
2829

2930
import storeConfig from 'discovery.config'
3031
import {
31-
type GlobalSectionsData,
3232
getGlobalSectionsData,
33+
type GlobalSectionsData,
3334
} from 'src/components/cms/GlobalSections'
35+
import { getOfferUrl, useOffer } from 'src/sdk/offer'
3436
import PageProvider, { type PDPContext } from 'src/sdk/overrides/PageProvider'
3537
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+
}
3746

3847
/**
3948
* Sections: Components imported from each store's custom components and '../components/sections' only.
@@ -68,15 +77,31 @@ type Props = PDPContentType & {
6877
// https://www.npmjs.com/package/deepmerge
6978
const overwriteMerge = (_: any[], sourceArray: any[]) => sourceArray
7079

80+
const isClientOfferEnabled = (storeConfig as StoreConfig).experimental
81+
.enableClientOffer
82+
7183
function Page({ data: server, sections, globalSections, offers, meta }: Props) {
7284
const { product } = server
7385
const { currency } = useSession()
7486
const titleTemplate = storeConfig?.seo?.titleTemplate ?? ''
7587

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+
})()
80105

81106
const context = {
82107
data: {
@@ -87,6 +112,15 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) {
87112

88113
return (
89114
<>
115+
<Head>
116+
<link
117+
rel="preload"
118+
href={getOfferUrl(product.sku)}
119+
as="fetch"
120+
crossOrigin="anonymous"
121+
fetchPriority="high"
122+
/>
123+
</Head>
90124
{/* SEO */}
91125
<NextSeo
92126
title={meta.title}
@@ -271,6 +305,7 @@ export const getStaticProps: GetStaticProps<
271305
globalSections,
272306
key: seo.canonical,
273307
},
308+
revalidate: (storeConfig as StoreConfig).experimental.revalidate ?? 0,
274309
}
275310
}
276311

+48
Original file line numberDiff line numberDiff line change
@@ -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+
}
+20
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)