Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offer and cookie #2613

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4913f6b
refactor: simplify product data handling in ProductDetails component
emersonlaurentino Nov 21, 2024
145d4f6
feat: enhance loading state handling in ProductDetails component
emersonlaurentino Nov 21, 2024
e7694a2
feat: implement offer fetching and aggregation functionality
emersonlaurentino Nov 26, 2024
9dbb5cf
feat: add revalidation for static props in page component
emersonlaurentino Nov 26, 2024
3221584
feat: update offer fetcher to use secure subdomain for API requests
emersonlaurentino Nov 26, 2024
6dcead8
feat: update offer fetcher to use dynamic base URL for API requests
emersonlaurentino Nov 26, 2024
a43fbf3
feat: refactor ProductDetails component to improve data structure and…
emersonlaurentino Nov 26, 2024
c2811d9
feat: update fetcher to include credentials in API requests
emersonlaurentino Nov 26, 2024
de787bf
feat: update offer fetcher to use dynamic store URL and conditionally…
emersonlaurentino Nov 26, 2024
1def59c
feat: update revalidation settings to use dynamic configuration
emersonlaurentino Nov 26, 2024
6978522
feat: enhance error handling in useOffer hook to return initial state…
emersonlaurentino Nov 26, 2024
8c44707
feat: update useOffer hook to return error state on fetch failures
emersonlaurentino Nov 26, 2024
2daa84c
feat: refactor offer fetching logic to separate URL generation and fe…
emersonlaurentino Nov 28, 2024
3a5ab83
feat: set fetch priority to high for offer URLs in page component
emersonlaurentino Nov 28, 2024
cd543fc
feat: remove fetcherOffer export and related preload calls in useProd…
emersonlaurentino Nov 28, 2024
1ed7859
feat: update product search URL to remove unnecessary versioning
emersonlaurentino Dec 6, 2024
18b0940
chore: update yarn.lock to remove deprecated dependencies and clean u…
emersonlaurentino Jan 10, 2025
182d580
test: Order form ID from cookie
lucasfp13 Dec 12, 2024
a9287ef
chore: Remove console logs
lucasfp13 Jan 2, 2025
d652941
feat: remove workspace parameter from product search URL in production
emersonlaurentino Jan 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ const platforms = {
},
}

const directives: Directive[] = [
cacheControlDirective
]
const directives: Directive[] = [cacheControlDirective]

export const getTypeDefs = () => [typeDefs, ...directives.map(d => d.typeDefs)]
export const getTypeDefs = () => [
typeDefs,
...directives.map((d) => d.typeDefs),
]

export const getResolvers = (options: Options) =>
platforms[options.platform].getResolvers(options)
Expand All @@ -47,3 +48,10 @@ export const getSchema = async (options: Options) => {

export * from './platforms/vtex/resolvers/root'
export type { Resolver } from './platforms/vtex'

export type {
CommertialOffer,
Item,
ProductSearchResult,
Seller,
} from './platforms/vtex/clients/search/types/ProductSearchResult'
5 changes: 2 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/validateCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,8 @@ export const validateCart = async (
{ cart: { order }, session }: MutationValidateCartArgs,
ctx: Context
) => {
const orderNumber = order?.orderNumber
? order.orderNumber
: getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com')
const orderFormIdFromCookie = getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com')
const orderNumber = orderFormIdFromCookie !== '' ? orderFormIdFromCookie : order?.orderNumber

const { acceptedOffer, shouldSplitItem } = order
const {
Expand Down
1 change: 1 addition & 0 deletions packages/core/discovery.config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,6 @@ module.exports = {
noRobots: false,
preact: false,
enableRedirects: false,
revalidate: 300, // Revalidate every 5 minutes
},
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"sass-loader": "^12.6.0",
"sharp": "^0.32.6",
"style-loader": "^3.3.1",
"swr": "^1.3.0",
"swr": "^2.2.5",
"tsx": "^4.6.2",
"typescript": "4.7.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,61 +192,77 @@ function ProductDetails({
{...ImageGallery.props}
images={productImages}
/>
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<ProductDetailsSettings.Component
buyButtonTitle={buyButtonTitle}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
}
useUnitMultiplier={quantitySelector?.useUnitMultiplier ?? false}
{...ProductDetailsSettings.props}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
quantity={quantity}
setQuantity={setQuantity}
product={product}
isValidating={isValidating}
taxesConfiguration={taxesConfiguration}
/>
</section>

{!outOfStock && (
<ShippingSimulation.Component
{isValidating ? (
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<p>Loading...</p>
</section>
</section>
) : (
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={shippingSimulatorTitle ?? ShippingSimulation.props.title}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
/>
)}
</section>
>
<ProductDetailsSettings.Component
buyButtonTitle={buyButtonTitle}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
}
useUnitMultiplier={
quantitySelector?.useUnitMultiplier ?? false
}
{...ProductDetailsSettings.props}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
quantity={quantity}
setQuantity={setQuantity}
product={product}
isValidating={isValidating}
taxesConfiguration={taxesConfiguration}
/>
</section>

{!outOfStock && (
<ShippingSimulation.Component
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={
shippingSimulatorTitle ?? ShippingSimulation.props.title
}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
/>
)}
</section>
)}

{shouldDisplayProductDescription && (
<ProductDescription.Component
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/pages/[slug]/p.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Locator } from '@vtex/client-cms'
import deepmerge from 'deepmerge'
import type { GetStaticPaths, GetStaticProps } from 'next'
import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo'
import Head from 'next/head'
import type { ComponentType } from 'react'

import { gql } from '@generated'
Expand Down Expand Up @@ -30,8 +31,8 @@ import {
GlobalSectionsData,
getGlobalSectionsData,
} from 'src/components/cms/GlobalSections'
import { getOfferUrl, useOffer } from 'src/sdk/offer'
import PageProvider, { PDPContext } from 'src/sdk/overrides/PageProvider'
import { useProductQuery } from 'src/sdk/product/useProductQuery'
import { PDPContentType, getPDP } from 'src/server/cms/pdp'

/**
Expand Down Expand Up @@ -71,20 +72,27 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) {
const { currency } = useSession()
const titleTemplate = storeConfig?.seo?.titleTemplate ?? ''

// Stale while revalidate the product for fetching the new price etc
const { data: client, isValidating } = useProductQuery(product.id, {
product: product,
})
const offer = useOffer({ skuId: product.sku })
const client = { product: { offers: offer.offers } }

const context = {
data: {
...deepmerge(server, client, { arrayMerge: overwriteMerge }),
isValidating,
isValidating: offer.isValidating,
},
} as PDPContext

return (
<>
<Head>
<link
rel="preload"
href={getOfferUrl(product.sku)}
as="fetch"
crossOrigin="anonymous"
fetchPriority="high"
/>
</Head>
{/* SEO */}
<NextSeo
title={meta.title}
Expand Down Expand Up @@ -269,6 +277,7 @@ export const getStaticProps: GetStaticProps<
globalSections,
key: seo.canonical,
},
revalidate: storeConfig.experimental.revalidate,
}
}

Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/sdk/offer/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Item, Seller } from '@faststore/api'
import { EnhancedCommercialOffer } from './enhance'
import { inStock, price } from './sort'

type Root = EnhancedCommercialOffer<Seller, Item>

const withTax = (
price: number,
tax: number = 0,
unitMultiplier: number = 1
) => {
const unitTax = tax / unitMultiplier
return Math.round((price + unitTax) * 100) / 100
}

const getHighPrice = (
offers: Root[],
options: { includeTaxes: boolean } = { includeTaxes: false }
) => {
const availableOffers = offers.filter(inStock)
const highOffer = availableOffers[availableOffers.length - 1]
const highPrice = highOffer ? price(highOffer) : 0
if (!options.includeTaxes) {
return highPrice
}

return withTax(highPrice, highOffer?.Tax, highOffer?.product?.unitMultiplier)
}

const getLowPrice = (
offers: Root[],
options: { includeTaxes: boolean } = { includeTaxes: false }
) => {
const [lowOffer] = offers.filter(inStock)

const lowPrice = lowOffer ? price(lowOffer) : 0

if (!options.includeTaxes) {
return lowPrice
}

return withTax(lowPrice, lowOffer?.Tax, lowOffer?.product?.unitMultiplier)
}

export function aggregateOffer(offers: Root[]) {
return {
highPrice: getHighPrice(offers),
lowPrice: getLowPrice(offers),
lowPriceWithTaxes: getLowPrice(offers, { includeTaxes: true }),
offerCount: offers.length,
}
}
20 changes: 20 additions & 0 deletions packages/core/src/sdk/offer/enhance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommertialOffer } from '@faststore/api'

export type EnhancedCommercialOffer<S, P> = CommertialOffer & {
seller: S
product: P
}

export const enhanceCommercialOffer = <S, P>({
offer,
seller,
product,
}: {
offer: CommertialOffer
seller: S
product: P
}): EnhancedCommercialOffer<S, P> => ({
...offer,
product,
seller,
})
19 changes: 19 additions & 0 deletions packages/core/src/sdk/offer/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ProductSearchResult } from '@faststore/api'
import { api, storeUrl } from '../../../discovery.config'

const IS_PROD = process.env.NODE_ENV === 'production'

export function getUrl(skuId: string) {
const base = IS_PROD
? storeUrl
: `https://${api.storeId}.${api.environment}.com.br`
const url = new URL(`${base}/api/intelligent-search/product_search`)
url.searchParams.append('query', `sku.id:${skuId}`)
return url.toString()
}

export async function fetcher(skuId: string) {
return fetch(getUrl(skuId)).then((res) =>
res.json()
) as Promise<ProductSearchResult>
}
45 changes: 45 additions & 0 deletions packages/core/src/sdk/offer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import useSWR from 'swr'
import { aggregateOffer } from './aggregate'
import { enhanceCommercialOffer } from './enhance'
import { fetcher } from './fetcher'
import { bestOfferFirst } from './sort'
export { getUrl as getOfferUrl } from './fetcher'

const ERROR_DATA = { offers: {}, isValidating: false }

export function useOffer(args: { skuId: string }) {
const { data, error, isValidating } = useSWR(args.skuId, fetcher)

if (error || !data || data.products.length === 0) {
console.warn('Error or no data fetching offer to SKU', args.skuId, error)
return ERROR_DATA
}

const product = data.products[0]

if (!product || product.items.length === 0) {
console.warn('Product not found or has no items for SKU', args.skuId)
return ERROR_DATA
}

const item = product.items.find((item) => item.itemId === args.skuId)

if (!item) {
console.warn('Item not found for SKU', args.skuId)
return ERROR_DATA
}

const sellers = item.sellers
.map((seller) =>
enhanceCommercialOffer({
offer: seller.commertialOffer,
seller,
product: item,
})
)
.sort(bestOfferFirst)

const offers = aggregateOffer(sellers)

return { offers, isValidating }
}
Loading
Loading