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

Feature: implement sponsored product badge and wrapper #381

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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

- Add sponsored products data properties in wrapper and implement the Sponsored Badge.

## [2.84.0] - 2023-08-03

### Added
Expand Down
39 changes: 39 additions & 0 deletions docs/ProductSummarySponsoredBadge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
📢 Use this project, [contribute](https://github.com/vtex-apps/product-summary/blob/master/docs/ProductSummarySponsoredBadge.md) to it or open issues to help evolve it using [Store Discussion](https://github.com/vtex-apps/store-discussion).

# Product Summary Sponsored Badge

`ProductSummarySponsoredBadge` is a block exported by the [Product Summary app](https://developers.vtex.com/vtex-developer-docs/docs/vtex-product-summary) responsible for rendering the "Sponsored" tag in sponsored products.

### Configuration

1. Import the `vtex.product-summary` app to your theme's dependencies in the `manifest.json`:

```json
dependencies: {
"vtex.product-summary": "2.x"
}
```

2. Add the `product-summary-sponsored-badge` block to your store theme as a child of the `product-summary.shelf` block. For example:

```diff
"product-summary.shelf": {
"children": [
"product-summary-image",
"product-summary-name",
+ "product-summary-sponsored-badge",
"product-summary-attachment-list",
"product-summary-space",
"product-summary-column#1"
]
},
```

## Customization

To apply CSS customizations in this and other blocks, follow the [Using CSS Handles for store customization](https://developers.vtex.com/vtex-developer-docs/docs/vtex-io-documentation-using-css-handles-for-store-customization) guide.

| CSS Handles |
| ------------------------- |
| `sponsoredBadgeContainer` |
| `sponsoredBadge` |
1 change: 1 addition & 0 deletions messages/ar-SA.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "الوحدة",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "بدون {name}",
"store/productSummarySponsoredBadge.title": "برعاية",
"admin/editor.productSummaryPrice.title": "سعر ملخص المنتج",
"admin/editor.productSummaryPrice.description": "المكون الذي يظهر سعر المنتج داخل ملخص المنتج",
"admin/editor.productSummaryPrice.showListPrice.title": "إظهار سعر القائمة",
Expand Down
1 change: 1 addition & 0 deletions messages/bg-BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Единица",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Няма {name}",
"store/productSummarySponsoredBadge.title": "Спонсориран",
"admin/editor.productSummaryPrice.title": "Цена в резюме на продукта",
"admin/editor.productSummaryPrice.description": "Компонент, който показва цената на продукта в резюмето на продукта",
"admin/editor.productSummaryPrice.showListPrice.title": "Показване на цената в каталога",
Expand Down
1 change: 1 addition & 0 deletions messages/context.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "store/productSummary.unit",
"store/productSummary.attachmentName": "store/productSummary.attachmentName",
"store/productSummary.missingOptionName": "String displayed when the assembly option was removed by user",
"store/productSummarySponsoredBadge.title": "String displayed in the sponsored product badge",
"admin/editor.productSummaryPrice.title": "Title of ProductSummaryPrice component",
"admin/editor.productSummaryPrice.description": "Description of ProductSummaryPrice component",
"admin/editor.productSummaryPrice.showListPrice.title": "Title of showListPrice prop",
Expand Down
1 change: 1 addition & 0 deletions messages/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Einheit",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Kein {name}",
"store/productSummarySponsoredBadge.title": "Gesponsert",
"admin/editor.productSummaryPrice.title": "Produktzusammenfassung Preis",
"admin/editor.productSummaryPrice.description": "Komponente, die den Produktpreis in der Produktübersicht anzeigt",
"admin/editor.productSummaryPrice.showListPrice.title": "Listenpreis anzeigen",
Expand Down
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unit",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "No {name}",
"store/productSummarySponsoredBadge.title": "Sponsored",
"admin/editor.productSummaryPrice.title": "Product Summary Price",
"admin/editor.productSummaryPrice.description": "Component that shows product price inside the product summary",
"admin/editor.productSummaryPrice.showListPrice.title": "Show list price",
Expand Down
1 change: 1 addition & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unidad",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Sin {name}",
"store/productSummarySponsoredBadge.title": "Patrocinado",
"admin/editor.productSummaryPrice.title": "Precio del resumen del producto",
"admin/editor.productSummaryPrice.description": "Componente que muestra el precio del producto dentro del resumen del producto",
"admin/editor.productSummaryPrice.showListPrice.title": "Mostrar el precio de lista",
Expand Down
1 change: 1 addition & 0 deletions messages/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unité",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "No {name}",
"store/productSummarySponsoredBadge.title": "Sponsorisé",
"admin/editor.productSummaryPrice.title": "Prix de la synthèse du produit",
"admin/editor.productSummaryPrice.description": "Composant qui indique le prix du produit dans la synthèse du produit",
"admin/editor.productSummaryPrice.showListPrice.title": "Afficher la liste de prix",
Expand Down
1 change: 1 addition & 0 deletions messages/id-ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unit",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Tidak ada {name}",
"store/productSummarySponsoredBadge.title": "Disponsori",
"admin/editor.productSummaryPrice.title": "Harga Ringkasan Produk",
"admin/editor.productSummaryPrice.description": "Komponen yang menampilkan harga produk di dalam ringkasan produk",
"admin/editor.productSummaryPrice.showListPrice.title": "Tampilkan harga eceran",
Expand Down
1 change: 1 addition & 0 deletions messages/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Pezzo",
"store/productSummary.attachmentName": "{sign}{quantity}x {name}",
"store/productSummary.missingOptionName": "Senza {name}",
"store/productSummarySponsoredBadge.title": "Sponsorizzato",
"admin/editor.productSummaryPrice.title": "Prezzo nel riepilogo del prodotto",
"admin/editor.productSummaryPrice.description": "Componente che mostra il prezzo all'interno del riepilogo del prodotto",
"admin/editor.productSummaryPrice.showListPrice.title": "Mostra prezzo di listino",
Expand Down
1 change: 1 addition & 0 deletions messages/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "ユニット",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "{name} なし",
"store/productSummarySponsoredBadge.title": "スポンサー付き",
"admin/editor.productSummaryPrice.title": "製品サマリ価格",
"admin/editor.productSummaryPrice.description": "製品サマリ内で製品価格を表示するコンポーネント",
"admin/editor.productSummaryPrice.showListPrice.title": "希望小売価格を表示する",
Expand Down
1 change: 1 addition & 0 deletions messages/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "단위",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "{name} 없음",
"store/productSummarySponsoredBadge.title": "후원",
"admin/editor.productSummaryPrice.title": "제품 요약 가격",
"admin/editor.productSummaryPrice.description": "제품 요약 내에 제품 가격을 표시하는 구성 요소",
"admin/editor.productSummaryPrice.showListPrice.title": "정가 표시",
Expand Down
1 change: 1 addition & 0 deletions messages/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Eenheid",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Geen {name}",
"store/productSummarySponsoredBadge.title": "Gesponsord",
"admin/editor.productSummaryPrice.title": "Productoverzicht prijs",
"admin/editor.productSummaryPrice.description": "Component die de prijs van het product toont in het productoverzicht",
"admin/editor.productSummaryPrice.showListPrice.title": "Toon lijstprijs",
Expand Down
1 change: 1 addition & 0 deletions messages/nn-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Enhet",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Nei {name}",
"store/productSummarySponsoredBadge.title": "Sponset",
"admin/editor.productSummaryPrice.title": "Pris for produktsammendrag",
"admin/editor.productSummaryPrice.description": "Komponent som viser produktpris inne i produktsammendraget",
"admin/editor.productSummaryPrice.showListPrice.title": "Vis listepris",
Expand Down
1 change: 1 addition & 0 deletions messages/no-NO.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Enhet",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Nei {name}",
"store/productSummarySponsoredBadge.title": "Sponset",
"admin/editor.productSummaryPrice.title": "Pris for produktsammendrag",
"admin/editor.productSummaryPrice.description": "Komponent som viser produktpris inne i produktsammendraget",
"admin/editor.productSummaryPrice.showListPrice.title": "Vis listepris",
Expand Down
1 change: 1 addition & 0 deletions messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unidade",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Sem {name}",
"store/productSummarySponsoredBadge.title": "Patrocinado",
"admin/editor.productSummaryPrice.title": "Preço do resumo do produto",
"admin/editor.productSummaryPrice.description": "Componente que mostra o preço do produto",
"admin/editor.productSummaryPrice.showListPrice.title": "Mostrar preço de lista",
Expand Down
1 change: 1 addition & 0 deletions messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unidade",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Sem {name}",
"store/productSummarySponsoredBadge.title": "Patrocinado",
"admin/editor.productSummaryPrice.title": "Preço do resumo do produto",
"admin/editor.productSummaryPrice.description": "Componente que mostra o preço do produto",
"admin/editor.productSummaryPrice.showListPrice.title": "Mostrar preço de lista",
Expand Down
1 change: 1 addition & 0 deletions messages/ro-RO.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "Unitate",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "Fără {name}",
"store/productSummarySponsoredBadge.title": "Спонсор",
"admin/editor.productSummaryPrice.title": "Preț rezumat produs",
"admin/editor.productSummaryPrice.description": "Componenta care arată prețul produsului în rezumatul produsului",
"admin/editor.productSummaryPrice.showListPrice.title": "Arată prețul de listă",
Expand Down
1 change: 1 addition & 0 deletions messages/th-TH.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"store/productSummary.unit": "หน่วย",
"store/productSummary.attachmentName": "{sign} {quantity}x {name}",
"store/productSummary.missingOptionName": "ไม่มี {name}",
"store/productSummarySponsoredBadge.title": "ได้รับการสนับสนุน",
"admin/editor.productSummaryPrice.title": "ราคาตามภาพรวมผลิตภัณฑ์",
"admin/editor.productSummaryPrice.description": "คอมโพเนนต์ที่แสดงราคาผลิตภัณฑ์ภายในภาพรวมผลิตภัณฑ์",
"admin/editor.productSummaryPrice.showListPrice.title": "แสดงราคาตามบัญชีราคา",
Expand Down
43 changes: 23 additions & 20 deletions react/ProductSummaryCustom.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
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 { Link } from 'vtex.render-runtime'

import LocalProductSummaryContext from './ProductSummaryContext'
import { mapCatalogProductToProductSummary } from './utils/normalize'
import ProductPriceSimulationWrapper from './components/ProductPriceSimulationWrapper'
import SponsoredProductWrapper from './components/SponsoredProductWrapper'
import { mapCatalogProductToProductSummary } from './utils/normalize'

const {
ProductSummaryProvider,
Expand Down Expand Up @@ -162,17 +163,19 @@ function ProductSummaryCustom({
inView={inView}
priceBehavior={priceBehavior}
>
<section
className={containerClasses}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ maxWidth: PRODUCT_SUMMARY_MAX_WIDTH }}
ref={inViewRef}
>
<Link className={linkClasses} {...linkProps}>
<article className={summaryClasses}>{children}</article>
</Link>
</section>
<SponsoredProductWrapper product={product} position={position}>
<section
className={containerClasses}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ maxWidth: PRODUCT_SUMMARY_MAX_WIDTH }}
ref={inViewRef}
>
<Link className={linkClasses} {...linkProps}>
<article className={summaryClasses}>{children}</article>
</Link>
</section>
</SponsoredProductWrapper>
</ProductPriceSimulationWrapper>
</ProductContextProvider>
</LocalProductSummaryContext.Provider>
Expand Down
29 changes: 29 additions & 0 deletions react/ProductSummarySponsoredBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'
import { IOMessage } from 'vtex.native-types'
import { ProductSummaryContext } from 'vtex.product-summary-context'

const { useProductSummary } = ProductSummaryContext

const CSS_HANDLES = ['sponsoredBadgeContainer', 'sponsoredBadge'] as const

function ProductSummarySponsoredBadge() {
const { product } = useProductSummary()
const { handles } = useCssHandles(CSS_HANDLES)

const isSponsored = !!product.advertisement?.adId

return isSponsored ? (
<div className={handles.sponsoredBadgeContainer}>
<div className={handles.sponsoredBadge}>
<IOMessage id="store/productSummarySponsoredBadge.title" />
</div>
</div>
) : null
}

ProductSummarySponsoredBadge.schema = {
title: 'store/productSummarySponsoredBadge.title',
}

export default ProductSummarySponsoredBadge
66 changes: 66 additions & 0 deletions react/components/SponsoredProductWrapper.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { render, screen } from '@vtex/test-tools/react'
import React from 'react'
import { ProductSummaryTypes } from 'vtex.product-summary-context'

import SponsoredProductWrapper from './SponsoredProductWrapper'

const position = 8
const mockProduct = {
productName: 'productName',
productId: 'productId',
} as ProductSummaryTypes.Product

const advertisement = {
adId: 'adId',
campaignId: 'campaignId',
adRequestId: 'adRequestId',
adResponseId: 'adResponseId',
actionCost: 0.32,
}

const mockSponsoredProduct = {
...mockProduct,
advertisement,
} as ProductSummaryTypes.Product

const Children = <>Mock Children</>

const setup = (product: ProductSummaryTypes.Product) =>
render(
<SponsoredProductWrapper product={product} position={position}>
{Children}
</SponsoredProductWrapper>
)

describe('<SponsoredProductWrapper />', () => {
it('should render the children', () => {
setup(mockSponsoredProduct)

expect(screen.getByText('Mock Children')).toBeInTheDocument()
})

describe('when a product is sponsored', () => {
it('should add the sponsored data properties', () => {
setup(mockSponsoredProduct)
const wrapper = screen.getByTestId('sponsored-product-wrapper')

expect(wrapper.getAttribute('data-van-aid')).toBe('adId')
expect(wrapper.getAttribute('data-van-cid')).toBe('campaignId')
expect(wrapper.getAttribute('data-van-req-id')).toBe('adRequestId')
expect(wrapper.getAttribute('data-van-res-id')).toBe('adResponseId')
expect(wrapper.getAttribute('data-van-cpc')).toBe('0.32')
expect(wrapper.getAttribute('data-van-position')).toBe('8')
expect(wrapper.getAttribute('data-van-prod-id')).toBe('productId')
expect(wrapper.getAttribute('data-van-prod-name')).toBe('productName')
})
})

describe('when a product is not sponsored', () => {
it('should not render the wrapper', () => {
setup(mockProduct)
const wrapper = screen.queryByTestId('sponsored-product-wrapper')

expect(wrapper).not.toBeInTheDocument()
})
})
})
42 changes: 42 additions & 0 deletions react/components/SponsoredProductWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { PropsWithChildren } from 'react'
import { ProductSummaryTypes } from 'vtex.product-summary-context'

interface Props {
product: ProductSummaryTypes.Product
position?: number
}

/**
* Wrapper responsible for adding the neccesary data-properties in sponsored products.
* These data-properties are used by the Activity Flow script to track the product.
* If the product is not sponsored, it will return the children as is.
/** */
function SponsoredProductWrapper({
product,
position,
children,
}: PropsWithChildren<Props>) {
const { advertisement, productName, productId } = product
const isSponsored = !!advertisement?.adId

if (!isSponsored) return <>{children}</>

const dataProperties = {
'data-van-prod-id': productId,
'data-van-prod-name': productName,
'data-van-position': position,
'data-van-aid': advertisement?.adId,
'data-van-cid': advertisement?.campaignId,
'data-van-req-id': advertisement?.adRequestId,
'data-van-res-id': advertisement?.adResponseId,
'data-van-cpc': advertisement?.actionCost,
}

return (
<div {...dataProperties} data-testid="sponsored-product-wrapper">
{children}
</div>
)
}

export default SponsoredProductWrapper
Loading