Skip to content

Commit

Permalink
Feature: add sponsored products to list (#392)
Browse files Browse the repository at this point in the history
#### What problem is this solving?

Fetches and displays sponsored products on product shelves.

For that, some changes had to be made:
1. Now, the default position for the `sponsored` label is on container
top. This will ensure a more consistent placement for more stores;
2. `ProductSummary` now receives a `placement` prop, which will tell
Analytics where the product is positioned.
  • Loading branch information
Henrique Caúla authored Aug 1, 2024
1 parent 2c6c445 commit b3ceeb9
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 30 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Calls sponsored products on product list.

## [2.89.0] - 2023-12-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/ProductSummaryShelf.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The Product Summary Shelf is the main block exported by the [Product Summary app
| `priceBehavior` | `enum` | Determines whether the component should fetch the most up-to-date price (`async`) or (`default`). Remember to configure the [Search Result](https://vtex.io/docs/components/content-blocks/vtex.search-result@3.79.1/#configuration)'s `simulationBehavior` prop to `skip` and use the Product Price [`product-price-suspense`](https://github.com/vtex-apps/product-price/blob/master/docs/README.md) block to render a loading spinner while the price information is being fetched. | `default` |
| `trackListName` | `boolean` | Determines whether the component should send the list name to the product page when the product summary is clicked. Disabling it will prevent the `productDetail` GTM event sent on the PDP to identify from which list the user navigated. | `true` |
| `sponsoredBadgeLabel` | `String` | The text of the "Sponsored" tag, if applicable. | `"store/sponsored-badge.label"`|
| `sponsoredBadgePosition` | `enum` | The position of the "Sponsored" tag, if applicable. Possible values are `titleTop`, `containerTopLeft` and `none`. | `"titleTop"`|
| `sponsoredBadgePosition` | `enum` | The position of the "Sponsored" tag, if applicable. Possible values are `titleTop`, `containerTopLeft` and `none`. | `"containerTopLeft"`|

## Customization

Expand Down
6 changes: 6 additions & 0 deletions messages/context.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
"admin/editor.productSummaryList.installmentCriteria.max-without-interest": "Maximum without interest enum name",
"admin/editor.productSummaryList.installmentCriteria.max-with-interest": "Maximum with interest enum name",
"admin/editor.productSummaryList.analyticsListName.title": "List name in Analytics",
"admin/editor.productSummaryList.showSponsoredProducts.title": "Show contextual sponsored products",
"admin/editor.productSummaryList.showSponsoredProducts.description": "Whether or not to show contextual sponsored products in this list.",
"admin/editor.productSummaryList.sponsoredCount.title": "Maximum amount of sponsored products",
"admin/editor.productSummaryList.sponsoredCount.description": "If there are sponsored products for this search, how many of them should be displayed before normal products",
"admin/editor.productSummaryList.repeatSponsoredProducts.title": "Repeat sponsored and regular products",
"admin/editor.productSummaryList.repeatSponsoredProducts.description": "Show the same product twice - once sponsored and another in its regular position. If false, only the sponsored product will be shown.",
"admin/editor.productSummary.trackListName.title": "Should track list name",
"admin/editor.productSummary.sponsoredBadge.title": "admin/editor.productSummary.sponsoredBadge.title",
"admin/editor.productSummary.sponsoredBadge.position": "admin/editor.productSummary.sponsoredBadge.position",
Expand Down
6 changes: 6 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
"admin/editor.productSummaryList.installmentCriteria.max-without-interest": "Maximum without interest",
"admin/editor.productSummaryList.installmentCriteria.max-with-interest": "Maximum",
"admin/editor.productSummaryList.analyticsListName.title": "List name in Analytics",
"admin/editor.productSummaryList.showSponsoredProducts.title": "Show contextual sponsored products",
"admin/editor.productSummaryList.showSponsoredProducts.description": "Whether or not to show contextual sponsored products in this list.",
"admin/editor.productSummaryList.sponsoredCount.title": "Maximum amount of sponsored products",
"admin/editor.productSummaryList.sponsoredCount.description": "If there are sponsored products for this search, how many of them should be displayed before normal products",
"admin/editor.productSummaryList.repeatSponsoredProducts.title": "Repeat sponsored and regular products",
"admin/editor.productSummaryList.repeatSponsoredProducts.description": "Show the same product twice - once sponsored and another in its regular position. If false, only the sponsored product will be shown.",
"admin/editor.productSummary.trackListName.title": "Track list name",
"admin/editor.productSummary.sponsoredBadge.title": "Text of the sponsored product tag, if applicable",
"admin/editor.productSummary.sponsoredBadge.position": "Position of the sponsored product tag, if applicable",
Expand Down
6 changes: 6 additions & 0 deletions messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
"admin/editor.productSummaryList.installmentCriteria.max-without-interest": "Máximo sem juros",
"admin/editor.productSummaryList.installmentCriteria.max-with-interest": "Máximo",
"admin/editor.productSummaryList.analyticsListName.title": "Nome da lista no Analytics",
"admin/editor.productSummaryList.showSponsoredProducts.title": "Mostrar produtos patrocinados contextuais",
"admin/editor.productSummaryList.showSponsoredProducts.description": "Se deve-se mostrar produtos patrocinados contextuais nesta lista.",
"admin/editor.productSummaryList.sponsoredCount.title": "Quantidade máxima de produtos patrocinados",
"admin/editor.productSummaryList.sponsoredCount.description": "Caso haja produtos patrocinados para esta busca, quantos deles devem ser exibidos antes dos produtos normais",
"admin/editor.productSummaryList.repeatSponsoredProducts.title": "Repetir produtos patrocinados e regulares",
"admin/editor.productSummaryList.repeatSponsoredProducts.description": "Mostrar o mesmo produto duas vezes - uma vez patrocinado e outro em sua posição regular. Se falso, apenas o produto patrocinado será exibido.",
"admin/editor.productSummary.trackListName.title": "Rastreia o nome da lista",
"admin/editor.productSummary.sponsoredBadge.title": "Texto da tag de produto patrocinado, se aplicável",
"admin/editor.productSummary.sponsoredBadge.position": "Posição da tag de produto patrocinado",
Expand Down
34 changes: 23 additions & 11 deletions react/ProductSummaryCustom.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React, { useCallback, useMemo, useEffect, useRef } from 'react'
import type { PropsWithChildren } from 'react'
import classNames from 'classnames'
import { Link } from 'vtex.render-runtime'
import type { PropsWithChildren } from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import type { CssHandlesTypes } from 'vtex.css-handles'
import { useCssHandles } from 'vtex.css-handles'
import { useOnView } from 'vtex.on-view'
import type { ProductTypes } from 'vtex.product-context'
import { ProductContextProvider } from 'vtex.product-context'
import { ProductListContext } from 'vtex.product-list-context'
import { ProductSummaryContext } from 'vtex.product-summary-context'
import type { ProductSummaryTypes } from 'vtex.product-summary-context'
import { ProductContextProvider } from 'vtex.product-context'
import type { ProductTypes } from 'vtex.product-context'
import { useCssHandles } from 'vtex.css-handles'
import type { CssHandlesTypes } from 'vtex.css-handles'
import { ProductSummaryContext } from 'vtex.product-summary-context'
import { SponsoredBadgePosition } from 'vtex.product-summary-context/react/ProductSummaryTypes'
import { Link } from 'vtex.render-runtime'

import LocalProductSummaryContext from './ProductSummaryContext'
import { mapCatalogProductToProductSummary } from './utils/normalize'
import ProductPriceSimulationWrapper from './components/ProductPriceSimulationWrapper'
import { SponsoredBadge } from './components/SponsoredBadge'
import getAdsDataProperties from './utils/getAdsDataProperties'
import { mapCatalogProductToProductSummary } from './utils/normalize'
import shouldShowSponsoredBadge from './utils/shouldShowSponsoredBadge'
import { SponsoredBadge } from './components/SponsoredBadge'

const {
ProductSummaryProvider,
Expand All @@ -39,6 +39,7 @@ function ProductSummaryCustom({
children,
href,
priceBehavior = 'default',
placement,
position,
classes,
}: PropsWithChildren<Props>) {
Expand Down Expand Up @@ -167,7 +168,12 @@ function ProductSummaryCustom({
onClickCapture: autocompleteSummary ? undefined : actionOnClick,
}

const adsDataProperties = getAdsDataProperties({ product, position })
const adsDataProperties = getAdsDataProperties({
product,
position,
placement,
})

const showSponsoredBadge = shouldShowSponsoredBadge(
product,
sponsoredBadge?.position as SponsoredBadgePosition,
Expand Down Expand Up @@ -241,6 +247,10 @@ interface Props {
* The label of the sponsored badge, if applicable.
*/
sponsoredBadgeLabel?: string
/**
* Where this ProductSummary is being shown. Used for analytics. E.g. "search" or "shelf".
*/
placement?: string
}

function ProductSummaryWrapper({
Expand All @@ -253,6 +263,7 @@ function ProductSummaryWrapper({
position,
sponsoredBadgePosition,
sponsoredBadgeLabel,
placement,
classes,
children,
}: PropsWithChildren<Props>) {
Expand All @@ -276,6 +287,7 @@ function ProductSummaryWrapper({
actionOnClick={actionOnClick}
priceBehavior={priceBehavior}
position={position}
placement={placement}
classes={classes}
>
{children}
Expand Down
49 changes: 45 additions & 4 deletions react/ProductSummaryList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import React, { useCallback, useEffect, useState } from 'react'
import type { ComponentType, PropsWithChildren } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useQuery } from 'react-apollo'
import { QueryProducts } from 'vtex.store-resources'
import { usePixel } from 'vtex.pixel-manager'
import { QueryProducts } from 'vtex.store-resources'
import { ProductList as ProductListStructuredData } from 'vtex.structured-data'
// eslint-disable-next-line no-restricted-imports
import { equals } from 'ramda'
import { canUseDOM } from 'vtex.render-runtime'

import ProductSummaryListWithoutQuery from './ProductSummaryListWithoutQuery'
import { PreferenceType } from './utils/normalize'
import ProductSummaryListWithoutQuery, {
PRODUCT_LIST_PLACEMENT,
} from './ProductSummaryListWithoutQuery'
import useSession from './hooks/useSession'
import { PreferenceType } from './utils/normalize'

const DEFAULT_SHOW_SPONSORED_PRODUCTS = false
const DEFAULT_SPONSORED_COUNT = 2
const DEFAULT_REPEAT_SPONSORED_PRODUCTS = false

const ORDER_BY_OPTIONS = {
RELEVANCE: {
Expand Down Expand Up @@ -138,6 +144,12 @@ interface Props {
ProductSummary: ComponentType<{ product: any; actionOnClick: any }>
/** Callback on product click. */
actionOnProductClick?: (product: any) => void
/** Whether or not to show sponsored products in this list. */
showSponsoredProducts: boolean
/** Maximum number of sponsored products to put on top of the regular products. */
sponsoredCount: number
/** If true, sponsored and regular products will be repeated on the list. */
repeatSponsoredProducts: boolean
}

function ProductSummaryList(props: PropsWithChildren<Props>) {
Expand All @@ -155,6 +167,9 @@ function ProductSummaryList(props: PropsWithChildren<Props>) {
ProductSummary,
actionOnProductClick,
preferredSKU,
showSponsoredProducts = DEFAULT_SHOW_SPONSORED_PRODUCTS,
sponsoredCount = DEFAULT_SPONSORED_COUNT,
repeatSponsoredProducts = DEFAULT_REPEAT_SPONSORED_PRODUCTS,
} = props

const [shippingOptions, setShippingOptions] = useState([])
Expand Down Expand Up @@ -195,6 +210,12 @@ function ProductSummaryList(props: PropsWithChildren<Props>) {
skusFilter,
installmentCriteria,
variant: getCookie('sp-variant'),
advertisementOptions: {
showSponsored: showSponsoredProducts,
sponsoredCount,
repeatSponsoredProducts,
advertisementPlacement: PRODUCT_LIST_PLACEMENT,
},
},
})

Expand Down Expand Up @@ -322,6 +343,26 @@ ProductSummaryList.schema = {
title: 'admin/editor.productSummaryList.analyticsListName.title',
type: 'string',
},
showSponsoredProducts: {
title: 'admin/editor.productSummaryList.showSponsoredProducts.title',
description:
'admin/editor.productSummaryList.showSponsoredProducts.description',
type: 'boolean',
default: DEFAULT_SHOW_SPONSORED_PRODUCTS,
},
sponsoredCount: {
title: 'admin/editor.productSummaryList.sponsoredCount.title',
description: 'admin/editor.productSummaryList.sponsoredCount.description',
type: 'number',
default: DEFAULT_SPONSORED_COUNT,
},
repeatSponsoredProducts: {
title: 'admin/editor.productSummaryList.repeatSponsoredProducts.title',
description:
'admin/editor.productSummaryList.repeatSponsoredProducts.description',
type: 'boolean',
default: DEFAULT_REPEAT_SPONSORED_PRODUCTS,
},
},
}

Expand Down
15 changes: 10 additions & 5 deletions react/ProductSummaryListWithoutQuery.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useMemo } from 'react'
import type { ComponentType, PropsWithChildren } from 'react'
import { ExtensionPoint, useTreePath } from 'vtex.render-runtime'
import { useListContext, ListContextProvider } from 'vtex.list-context'
import React, { useMemo } from 'react'
import { ListContextProvider, useListContext } from 'vtex.list-context'
import { ProductListContext } from 'vtex.product-list-context'
import { ExtensionPoint, useTreePath } from 'vtex.render-runtime'

import ProductListEventCaller from './components/ProductListEventCaller'
import type { ProductClickParams } from './ProductSummaryList'
import {
mapCatalogProductToProductSummary,
PreferenceType,
} from './utils/normalize'
import ProductListEventCaller from './components/ProductListEventCaller'
import type { ProductClickParams } from './ProductSummaryList'

const { ProductListProvider } = ProductListContext

export const PRODUCT_LIST_PLACEMENT = 'home_shelf'

type Props = PropsWithChildren<{
/** Array of products. */
products?: any[]
Expand All @@ -25,6 +27,7 @@ type Props = PropsWithChildren<{
) => void
listName?: string
position?: number
placement?: string
}>
/** Name of the list property on Google Analytics events. */
listName?: string
Expand Down Expand Up @@ -73,6 +76,7 @@ function List({
listName={listName}
actionOnClick={handleOnClick}
position={position}
placement={PRODUCT_LIST_PLACEMENT}
/>
)
}
Expand All @@ -86,6 +90,7 @@ function List({
listName={listName}
actionOnClick={handleOnClick}
position={position}
placement={PRODUCT_LIST_PLACEMENT}
/>
)
})
Expand Down
3 changes: 3 additions & 0 deletions react/utils/getAdsDataProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Product } from 'vtex.product-summary-context/react/ProductSummaryTypes'
type GetAdsDataPropertiesArgs = {
product: Product
position?: number
placement?: string
}

const getAdsDataProperties = ({
product,
position,
placement,
}: GetAdsDataPropertiesArgs) => {
if (!product.advertisement?.adId) return {}

Expand All @@ -26,6 +28,7 @@ const getAdsDataProperties = ({
'data-van-req-id': adRequestId,
'data-van-res-id': adResponseId,
'data-van-cpc': actionCost,
'data-van-placement': placement,
}
}

Expand Down
10 changes: 2 additions & 8 deletions store/blocks.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
]
},
"product-summary-column#1": {
"children": [
"product-summary-price",
"product-summary-buy-button"
]
"children": ["product-summary-price", "product-summary-buy-button"]
},
"product-summary.unstable--flex": {
"children": [
Expand All @@ -24,9 +21,6 @@
]
},
"unstable--product-summary-column#1": {
"children": [
"product-summary-price",
"product-summary-buy-button"
]
"children": ["product-summary-price", "product-summary-buy-button"]
}
}
2 changes: 1 addition & 1 deletion store/contentSchemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"title": "admin/editor.productSummary.sponsoredBadge.position",
"type": "string",
"enum": ["titleTop", "containerTopLeft", "none"],
"default": "titleTop",
"default": "containerTopLeft",
"enumNames": [
"admin/editor.productSummary.sponsoredBadge.position.titleTop",
"admin/editor.productSummary.sponsoredBadge.position.containerTopLeft",
Expand Down

0 comments on commit b3ceeb9

Please sign in to comment.