Skip to content
Merged
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

- Ads SDK integration to fetch sponsored ads.

## [3.141.2] - 2025-12-04

### Changed
Expand Down
3 changes: 3 additions & 0 deletions react/SearchResultFlexible.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const SearchResultFlexible = ({
trackingId,
thresholdForFacetSearch,
lazyItemsRemaining,
sponsoredCount,
}) => {
// This makes infinite scroll unavailable.
// Infinite scroll was deprecated and we have
Expand Down Expand Up @@ -140,6 +141,7 @@ const SearchResultFlexible = ({
showFacetTitle,
trackingId,
thresholdForFacetSearch,
sponsoredCount,
}),
[
hiddenFacets,
Expand All @@ -149,6 +151,7 @@ const SearchResultFlexible = ({
showFacetTitle,
trackingId,
thresholdForFacetSearch,
sponsoredCount,
]
)

Expand Down
29 changes: 26 additions & 3 deletions react/SearchResultLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from 'react'
import { useChildBlock, ExtensionPoint } from 'vtex.render-runtime'
import { useDevice } from 'vtex.device-detector'
import { path, compose, equals, pathOr, isEmpty, isNil } from 'ramda'
import { useAds } from '@vtex/ads-react'

import OldSearchResult from './index'
import useMergeResults from './hooks/useMergeResults'
import { removeTreePath } from './utils/removeTreePath'

const noProducts = compose(
Expand All @@ -27,29 +29,42 @@ const SearchResultLayout = props => {
const hasCustomNotFound = !!useChildBlock({ id: 'search-not-found-layout' })
const { isMobile } = useDevice()

const sponsoredSearchResult = useAds({
placement: 'top_search',
type: 'product',
amount: props?.sponsoredCount ?? 3,
term: searchQuery?.variables?.query ?? '',
selectedFacets: searchQuery?.variables?.selectedFacets ?? [],
})

const newProps = useMergeResults({ props, sponsoredSearchResult })

if (
foundNothing(searchQuery) &&
hasCustomNotFound &&
noRedirect(searchQuery)
) {
return (
<ExtensionPoint id="search-not-found-layout" {...removeTreePath(props)} />
<ExtensionPoint
id="search-not-found-layout"
{...removeTreePath(newProps)}
/>
)
}

if (hasMobileBlock && isMobile) {
return (
<ExtensionPoint
id="search-result-layout.mobile"
{...removeTreePath(props)}
{...removeTreePath(newProps)}
/>
)
}

return (
<ExtensionPoint
id="search-result-layout.desktop"
{...removeTreePath(props)}
{...removeTreePath(newProps)}
/>
)
}
Expand All @@ -62,6 +77,14 @@ SearchResultLayout.getSchema = () => {
return {
...schema,
title: 'admin/editor.search-result-layout.title',
properties: {
...schema?.properties,
sponsoredCount: {
type: 'number',
title: 'Sponsored count',
default: 3,
},
},
}
}

Expand Down
8 changes: 1 addition & 7 deletions react/components/SearchQuery.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { equals } from 'ramda'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { equals } from 'ramda'
import { useQuery } from 'react-apollo'
import { useRuntime } from 'vtex.render-runtime'
import facetsQuery from 'vtex.store-resources/QueryFacetsV2'
Expand Down Expand Up @@ -155,12 +155,6 @@ const useQueries = (variables, facetsArgs, price) => {
variables: {
...variables,
variant: getCookie('sp-variant'),
advertisementOptions: {
showSponsored: true,
sponsoredCount: 3,
advertisementPlacement: 'top_search',
repeatSponsoredProducts: true,
},
},
})

Expand Down
106 changes: 106 additions & 0 deletions react/hooks/useMergeResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useEffect, useMemo, useState } from 'react'
import type { UseAdsReturn } from '@vtex/ads-react'

interface UseMergeResultsProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sponsoredSearchResult: UseAdsReturn<any>
}

const useMergeResults = ({
props,
sponsoredSearchResult,
}: UseMergeResultsProps) => {
const [holdOrganic, setHoldOrganic] = useState(false)

const isFirstPage = Boolean(
(props as unknown as { from?: number })?.from === 0 ||
(props as unknown as { page?: number })?.page === 1
)
Comment thread
MatheusLealv marked this conversation as resolved.

const isAdsLoading = sponsoredSearchResult?.isLoading ?? false

useEffect(() => {
if (!isFirstPage) {
setHoldOrganic(false)

return
}

if (!isAdsLoading) {
setHoldOrganic(false)

return
}

setHoldOrganic(true)

const ADS_WAIT_MS = 2000
const timeoutId = setTimeout(() => {
setHoldOrganic(false)
}, ADS_WAIT_MS)

return () => {
clearTimeout(timeoutId)
}
}, [isAdsLoading, isFirstPage])

const mergedProps = useMemo(() => {
const newProps = { ...props }

// While ads are loading (and within wait window), hide organic products to avoid flicker
if (isFirstPage && holdOrganic && newProps.searchQuery) {
newProps.searchQuery = {
...newProps.searchQuery,
loading: true,
data: {
...newProps.searchQuery.data,
products: [],
productSearch: {
...newProps.searchQuery.data.productSearch,
products: [],
},
},
products: [],
}
} else if (
isFirstPage &&
sponsoredSearchResult?.ads?.length &&
newProps.searchQuery
) {
const sponsoredProducts = sponsoredSearchResult.ads.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ad: any) => ({
...(ad.product ?? ad),
advertisement: ad.advertisement,
cacheId: `ad-${ad.product?.cacheId ?? ''}`,
})
)

const mergedProducts = [
...sponsoredProducts,
...(newProps.searchQuery.products || []),
]

newProps.searchQuery = {
...newProps.searchQuery,
data: {
...newProps.searchQuery.data,
products: mergedProducts,
productSearch: {
...newProps.searchQuery.data.productSearch,
products: mergedProducts,
},
},
products: mergedProducts,
}
}

return newProps
}, [props, sponsoredSearchResult, holdOrganic, isFirstPage])

return mergedProps
}

export default useMergeResults
1 change: 1 addition & 0 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"author": "VTEX",
"license": "ISC",
"dependencies": {
"@vtex/ads-react": "0.3.1",
"@vtex/css-handles": "^1.1.3",
"classnames": "^2.2.6",
"immer": "^9.0.6",
Expand Down
4 changes: 4 additions & 0 deletions react/typings/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
interface Window extends Window {
__hostname__: string | undefined
}

declare module '../utils/removeTreePath' {
export function removeTreePath<T = unknown>(props: T): T
}
46 changes: 46 additions & 0 deletions react/utils/adsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Use types from @vtex/ads-react where available
import type { useAds } from '@vtex/ads-react'

type SponsoredResult = ReturnType<typeof useAds>

export const mergeWithSponsoredProducts = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
productSearchResult: any,
sponsoredSearchResult: SponsoredResult,
from?: number
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): unknown => {
const isFirstPage = from === 0

if (!isFirstPage) return productSearchResult

const originalProducts =
productSearchResult?.data?.productSearch?.products ?? []

const sponsoredProducts = sponsoredSearchResult.ads
.map(p => p.product ?? p)
.filter(Boolean) as any[]

if (!sponsoredProducts?.length) return productSearchResult

const sponsoredIds = new Set(
sponsoredProducts.map(p => p.id).filter(Boolean) as string[]
)

const filteredOriginalProducts = originalProducts.filter(
(p: { id: string }) => !sponsoredIds.has(p.id)
)

const combinedProducts = [...sponsoredProducts, ...filteredOriginalProducts]

return {
...productSearchResult,
data: {
...(productSearchResult?.data ?? {}),
productSearch: {
...(productSearchResult?.data?.productSearch ?? {}),
products: combinedProducts,
},
},
}
}
1 change: 1 addition & 0 deletions react/utils/removeTreePath.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function removeTreePath<T = unknown>(props: T): T
Loading
Loading