From e39e9394f05a6c63d9f0dea74f3528d59fc49029 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:24:50 +0300 Subject: [PATCH 1/3] feat: add "Advertise here" CTA on ad surfaces and make sidebar ad fully clickable Add a feature-flagged "Advertise here" button across all ad placements (feed cards, sidebar widget, comment ads) to encourage engineers to refer their marketing teams. Rename "Remove" to "Go ad-free" for clarity. Make PostSidebarAdWidget fully clickable like feed ad cards. Made-with: Cursor --- .../src/components/cards/ad/AdCard.spec.tsx | 85 ++++++++++++++---- .../shared/src/components/cards/ad/AdGrid.tsx | 83 +++++++++-------- .../shared/src/components/cards/ad/AdList.tsx | 88 +++++++++++-------- .../src/components/cards/ad/SignalAdList.tsx | 36 ++++++-- .../cards/ad/common/AdvertiseLink.tsx | 86 ++++++++++++++++++ .../components/cards/ad/common/RemoveAd.tsx | 2 +- .../src/components/comments/AdAsComment.tsx | 18 +++- .../components/post/PostSidebarAdWidget.tsx | 23 ++++- packages/shared/src/lib/featureManagement.ts | 1 + packages/shared/src/lib/log.ts | 4 + 10 files changed, 319 insertions(+), 107 deletions(-) create mode 100644 packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx diff --git a/packages/shared/src/components/cards/ad/AdCard.spec.tsx b/packages/shared/src/components/cards/ad/AdCard.spec.tsx index 5e1df160a2a..1290116756b 100644 --- a/packages/shared/src/components/cards/ad/AdCard.spec.tsx +++ b/packages/shared/src/components/cards/ad/AdCard.spec.tsx @@ -2,6 +2,7 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; import { render, screen, waitFor, within } from '@testing-library/react'; import { QueryClient } from '@tanstack/react-query'; +import { GrowthBook } from '@growthbook/growthbook-react'; import ad from '../../../../__tests__/fixture/ad'; import { AdGrid } from './AdGrid'; import { AdList } from './AdList'; @@ -9,9 +10,12 @@ import { SignalAdList } from './SignalAdList'; import type { AdCardProps } from './common/common'; import { TestBootProvider } from '../../../../__tests__/helpers/boot'; import { ActiveFeedContext } from '../../../contexts'; +import { businessWebsiteUrl } from '../../../lib/constants'; const defaultProps: AdCardProps = { ad, + index: 0, + feedIndex: 0, onLinkClick: jest.fn(), }; @@ -19,24 +23,14 @@ beforeEach(() => { jest.clearAllMocks(); }); -const renderComponent = (props: Partial = {}): RenderResult => { - const client = new QueryClient(); - return render( - - - - - , - ); -}; - const renderListComponent = ( props: Partial = {}, + gb = new GrowthBook(), ): RenderResult => { const client = new QueryClient(); return render( - - + + , @@ -49,18 +43,45 @@ const renderSignalListComponent = ( const client = new QueryClient(); return render( - + , ); }; +const getGrowthBook = (isAdReferralCtaEnabled = false): GrowthBook => { + const gb = new GrowthBook(); + + gb.setFeatures({ + ad_referral_cta: { + defaultValue: isAdReferralCtaEnabled, + }, + }); + + return gb; +}; + const getNormalizedText = (element?: Element | null): string => element?.textContent?.replace(/\u200B/g, '').trim() ?? ''; +const renderGridComponent = ( + props: Partial = {}, + gb = new GrowthBook(), +): RenderResult => { + const client = new QueryClient(); + + return render( + + + + + , + ); +}; + it('should call on click on component left click', async () => { - renderComponent(); + renderGridComponent(); const el = await screen.findByTestId('adItem'); const links = await within(el).findAllByRole('link'); links[0].click(); @@ -68,7 +89,7 @@ it('should call on click on component left click', async () => { }); it('should call on click on component middle mouse up', async () => { - renderComponent(); + renderGridComponent(); const el = await screen.findByTestId('adItem'); const links = await within(el).findAllByRole('link'); links[0].dispatchEvent( @@ -78,7 +99,7 @@ it('should call on click on component middle mouse up', async () => { }); it('should show a single image by default', async () => { - renderComponent(); + renderGridComponent(); const img = await screen.findByAltText('Ad image'); const background = screen.queryByAltText('Ad image background'); expect(img).toBeInTheDocument(); @@ -86,7 +107,7 @@ it('should show a single image by default', async () => { }); it('should show blurred image for carbon', async () => { - renderComponent({ ad: { ...ad, source: 'Carbon' } }); + renderGridComponent({ ad: { ...ad, source: 'Carbon' } }); const img = await screen.findByAltText('Ad image'); const background = screen.queryByAltText('Ad image background'); expect(img).toHaveClass('absolute'); @@ -94,13 +115,30 @@ it('should show blurred image for carbon', async () => { }); it('should show pixel images', async () => { - renderComponent({ + renderGridComponent({ ad: { ...ad, pixel: ['https://daily.dev/pixel'] }, }); const el = await screen.findByTestId('pixel'); expect(el).toHaveAttribute('src', 'https://daily.dev/pixel'); }); +it('should not render advertise link by default', () => { + renderGridComponent(); + + expect( + screen.queryByRole('link', { name: 'Advertise here' }), + ).not.toBeInTheDocument(); +}); + +it('should render advertise link on grid ad when feature is enabled', () => { + renderGridComponent({}, getGrowthBook(true)); + + expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute( + 'href', + businessWebsiteUrl, + ); +}); + it('should render promoted attribution outside of list title clamp', async () => { const promotedMatcher = (_: string, element?: Element | null): boolean => { const text = getNormalizedText(element); @@ -114,6 +152,15 @@ it('should render promoted attribution outside of list title clamp', async () => expect(await screen.findByText(promotedMatcher)).toBeInTheDocument(); }); +it('should render advertise link on list ad when feature is enabled', () => { + renderListComponent({}, getGrowthBook(true)); + + expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute( + 'href', + businessWebsiteUrl, + ); +}); + it('should render company logo and company name in signal ad header', async () => { const companyLogo = 'https://daily.dev/company-logo.png'; const companyName = 'daily.dev'; diff --git a/packages/shared/src/components/cards/ad/AdGrid.tsx b/packages/shared/src/components/cards/ad/AdGrid.tsx index 9f6680b99c1..023562a37ca 100644 --- a/packages/shared/src/components/cards/ad/AdGrid.tsx +++ b/packages/shared/src/components/cards/ad/AdGrid.tsx @@ -1,4 +1,4 @@ -import type { ReactElement } from 'react'; +import type { ForwardedRef, ReactElement } from 'react'; import React, { forwardRef, useCallback } from 'react'; import { @@ -18,31 +18,38 @@ import { RemoveAd } from './common/RemoveAd'; import { usePlusSubscription } from '../../../hooks/usePlusSubscription'; import type { InViewRef } from '../../../hooks/feed/useAutoRotatingAds'; import { useAutoRotatingAds } from '../../../hooks/feed/useAutoRotatingAds'; -import { AdRefresh } from './common/AdRefresh'; import { Button } from '../../buttons/Button'; import { ButtonSize, ButtonVariant } from '../../buttons/common'; import { AdFavicon } from './common/AdFavicon'; import PostTags from '../common/PostTags'; import { useFeature } from '../../GrowthBookProvider'; import { adImprovementsV3Feature } from '../../../lib/featureManagement'; +import { TargetId } from '../../../lib/log'; +import { AdvertiseLink } from './common/AdvertiseLink'; -export const AdGrid = forwardRef(function AdGrid( - { ad, onLinkClick, onRefresh, domProps, index, feedIndex }: AdCardProps, - inViewRef: InViewRef, +export const AdGrid = forwardRef(function AdGrid( + { ad, onLinkClick, domProps, index, feedIndex }: AdCardProps, + forwardedRef: ForwardedRef, ): ReactElement { const { isPlus } = usePlusSubscription(); const adImprovementsV3 = useFeature(adImprovementsV3Feature); - const { ref, refetch, isRefetching } = useAutoRotatingAds( - ad, - index, - feedIndex, - inViewRef, - ); + const matchingTags = ad.matchingTags ?? []; + const inViewRef = useCallback( + (node) => { + const nextNode = node as HTMLElement | null; + + if (typeof forwardedRef === 'function') { + forwardedRef(nextNode); + return; + } - const onRefreshClick = useCallback(async () => { - onRefresh?.(ad); - await refetch(); - }, [ad, onRefresh, refetch]); + if (forwardedRef) { + forwardedRef.current = nextNode; + } + }, + [forwardedRef], + ); + const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef); return ( @@ -51,9 +58,9 @@ export const AdGrid = forwardRef(function AdGrid( {ad.description} - {adImprovementsV3 && ad?.matchingTags?.length > 0 ? ( + {adImprovementsV3 && matchingTags.length > 0 ? ( ) : null} @@ -62,33 +69,33 @@ export const AdGrid = forwardRef(function AdGrid(
- {!!ad.callToAction && ( - - )} -
- {!!onRefresh && ( - + {!!ad.callToAction && ( + )} + +
+
{!isPlus && ( )}
diff --git a/packages/shared/src/components/cards/ad/AdList.tsx b/packages/shared/src/components/cards/ad/AdList.tsx index 88cb46fd672..f8a6dc40fcb 100644 --- a/packages/shared/src/components/cards/ad/AdList.tsx +++ b/packages/shared/src/components/cards/ad/AdList.tsx @@ -1,4 +1,4 @@ -import type { AnchorHTMLAttributes, ReactElement } from 'react'; +import type { AnchorHTMLAttributes, ForwardedRef, ReactElement } from 'react'; import React, { forwardRef, useCallback } from 'react'; import { CardContent, @@ -17,7 +17,6 @@ import { RemoveAd } from './common/RemoveAd'; import { usePlusSubscription } from '../../../hooks/usePlusSubscription'; import type { InViewRef } from '../../../hooks/feed/useAutoRotatingAds'; import { useAutoRotatingAds } from '../../../hooks/feed/useAutoRotatingAds'; -import { AdRefresh } from './common/AdRefresh'; import { Button } from '../../buttons/Button'; import { ButtonSize, ButtonVariant } from '../../buttons/common'; import AdAttribution from './common/AdAttribution'; @@ -25,13 +24,15 @@ import { AdFavicon } from './common/AdFavicon'; import PostTags from '../common/PostTags'; import { useFeature } from '../../GrowthBookProvider'; import { adImprovementsV3Feature } from '../../../lib/featureManagement'; +import { TargetId } from '../../../lib/log'; +import { AdvertiseLink } from './common/AdvertiseLink'; const getLinkProps = ({ ad, onLinkClick, }: { ad: Ad; - onLinkClick: (ad: Ad) => unknown; + onLinkClick?: (ad: Ad) => unknown; }): AnchorHTMLAttributes => { return { href: ad.link, @@ -42,27 +43,33 @@ const getLinkProps = ({ }; }; -export const AdList = forwardRef(function AdCard( - { ad, onLinkClick, onRefresh, domProps, index, feedIndex }: AdCardProps, - inViewRef: InViewRef, +export const AdList = forwardRef(function AdCard( + { ad, onLinkClick, domProps, index, feedIndex }: AdCardProps, + forwardedRef: ForwardedRef, ): ReactElement { const { isPlus } = usePlusSubscription(); const adImprovementsV3 = useFeature(adImprovementsV3Feature); - const { ref, refetch, isRefetching } = useAutoRotatingAds( - ad, - index, - feedIndex, - inViewRef, - ); + const matchingTags = ad.matchingTags ?? []; + const inViewRef = useCallback( + (node) => { + const nextNode = node as HTMLElement | null; + + if (typeof forwardedRef === 'function') { + forwardedRef(nextNode); + return; + } - const onRefreshClick = useCallback(async () => { - onRefresh?.(ad); - await refetch(); - }, [ad, onRefresh, refetch]); + if (forwardedRef) { + forwardedRef.current = nextNode; + } + }, + [forwardedRef], + ); + const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef); return ( {ad.description} - {adImprovementsV3 && ad?.matchingTags?.length > 0 ? ( - + {adImprovementsV3 && matchingTags.length > 0 ? ( + ) : null}
- {!!ad.callToAction && ( - + )} + onLinkClick?.(ad))} - > - {ad.callToAction} - - )} -
- {!!onRefresh && ( - + /> +
+
+ {!isPlus && ( + )} - {!isPlus && }
diff --git a/packages/shared/src/components/cards/ad/SignalAdList.tsx b/packages/shared/src/components/cards/ad/SignalAdList.tsx index 5aa81d4f517..c20ef079ad9 100644 --- a/packages/shared/src/components/cards/ad/SignalAdList.tsx +++ b/packages/shared/src/components/cards/ad/SignalAdList.tsx @@ -1,5 +1,5 @@ -import type { AnchorHTMLAttributes, ReactElement } from 'react'; -import React, { forwardRef } from 'react'; +import type { AnchorHTMLAttributes, ForwardedRef, ReactElement } from 'react'; +import React, { forwardRef, useCallback } from 'react'; import classNames from 'classnames'; import type { Ad } from '../../../graphql/posts'; import { combinedClicks } from '../../../lib/click'; @@ -26,7 +26,7 @@ const getLinkProps = ({ onLinkClick, }: { ad: Ad; - onLinkClick: (ad: Ad) => unknown; + onLinkClick?: (ad: Ad) => unknown; }): AnchorHTMLAttributes => { return { href: ad.link, @@ -37,12 +37,29 @@ const getLinkProps = ({ }; }; -export const SignalAdList = forwardRef(function SignalAdList( +export const SignalAdList = forwardRef( + function SignalAdList( { ad, onLinkClick, domProps, index, feedIndex }: AdCardProps, - inViewRef: InViewRef, + forwardedRef: ForwardedRef, ): ReactElement { const { isPlus } = usePlusSubscription(); - const adImprovementsV3 = useFeature(adImprovementsV3Feature); + const adImprovementsV3 = Boolean(useFeature(adImprovementsV3Feature)); + const matchingTags = ad.matchingTags ?? []; + const inViewRef = useCallback( + (node) => { + const nextNode = node as HTMLElement | null; + + if (typeof forwardedRef === 'function') { + forwardedRef(nextNode); + return; + } + + if (forwardedRef) { + forwardedRef.current = nextNode; + } + }, + [forwardedRef], + ); const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef); const sourceName = ad.company?.trim() || ad.source?.trim() || 'Promoted'; @@ -97,8 +114,8 @@ export const SignalAdList = forwardRef(function SignalAdList(

{ad.description}

- {adImprovementsV3 && ad?.matchingTags?.length > 0 ? ( - + {adImprovementsV3 && matchingTags.length > 0 ? ( + ) : null} {!!ad.callToAction && (
@@ -119,4 +136,5 @@ export const SignalAdList = forwardRef(function SignalAdList( ); -}); +}, +); diff --git a/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx b/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx new file mode 100644 index 00000000000..4c36d39f0db --- /dev/null +++ b/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx @@ -0,0 +1,86 @@ +import type { ReactElement } from 'react'; +import React, { useEffect } from 'react'; +import classNames from 'classnames'; +import { Button, ButtonSize, ButtonVariant } from '../../../buttons/Button'; +import { useFeature } from '../../../GrowthBookProvider'; +import { useLogContext } from '../../../../contexts/LogContext'; +import { combinedClicks } from '../../../../lib/click'; +import { featureAdReferralCta } from '../../../../lib/featureManagement'; +import { businessWebsiteUrl } from '../../../../lib/constants'; +import { LogEvent, TargetId, TargetType } from '../../../../lib/log'; +import { anchorDefaultRel } from '../../../../lib/strings'; + +type AdvertiseLinkProps = { + targetId: TargetId; + className?: string; + buttonStyle?: boolean; + size?: ButtonSize; +}; + +export const AdvertiseLink = ({ + targetId, + className, + buttonStyle = false, + size = ButtonSize.Medium, +}: AdvertiseLinkProps): ReactElement | null => { + const isEnabled = useFeature(featureAdReferralCta); + const { logEvent } = useLogContext(); + + useEffect(() => { + if (!isEnabled) { + return; + } + + logEvent({ + event_name: LogEvent.Impression, + target_type: TargetType.AdvertiseHereCta, + target_id: targetId, + }); + }, [isEnabled, logEvent, targetId]); + + const onClick = () => + logEvent({ + event_name: LogEvent.Click, + target_type: TargetType.AdvertiseHereCta, + target_id: targetId, + }); + + if (!isEnabled) { + return null; + } + + if (buttonStyle) { + return ( + + ); + } + + return ( + + Advertise here + + ); +}; diff --git a/packages/shared/src/components/cards/ad/common/RemoveAd.tsx b/packages/shared/src/components/cards/ad/common/RemoveAd.tsx index a6b8aad01a4..ff7eb68af9e 100644 --- a/packages/shared/src/components/cards/ad/common/RemoveAd.tsx +++ b/packages/shared/src/components/cards/ad/common/RemoveAd.tsx @@ -38,7 +38,7 @@ export const RemoveAd = ({ }} {...props} > - {!iconOnly ? 'Remove' : undefined} + {!iconOnly ? 'Go ad-free' : undefined} ); diff --git a/packages/shared/src/components/comments/AdAsComment.tsx b/packages/shared/src/components/comments/AdAsComment.tsx index 4ecae5b0c91..3afefc231b5 100644 --- a/packages/shared/src/components/comments/AdAsComment.tsx +++ b/packages/shared/src/components/comments/AdAsComment.tsx @@ -19,11 +19,15 @@ import { MiniCloseIcon } from '../icons'; import { useAdQuery } from '../../features/monetization/useAdQuery'; import { ImpressionStatus } from '../../hooks/feed/useLogImpression'; import { useScrambler } from '../../hooks/useScrambler'; +import { TargetId } from '../../lib/log'; +import { AdvertiseLink } from '../cards/ad/common/AdvertiseLink'; interface AdAsCommentProps { postId: string; } -export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => { +export const AdAsComment = ({ + postId, +}: AdAsCommentProps): ReactElement | null => { const { logEvent } = useLogContext(); const { user } = useAuthContext(); const { isPlus } = usePlusSubscription(); @@ -41,6 +45,10 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => { const onAdAction = useCallback( (action: AdActions) => { + if (!ad) { + return; + } + logEvent( adLogEvent(action, ad, { extra: { @@ -52,7 +60,9 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => { [logEvent, ad], ); - const promotedText = useScrambler(!ad ? null : `Promoted by ${ad.source}`); + const promotedText = useScrambler( + ad ? `Promoted by ${ad.source}` : undefined, + ); const onRefreshClick = useCallback(async () => { onAdAction(AdActions.Refresh); @@ -117,6 +127,10 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => {
{description}

+
); diff --git a/packages/shared/src/components/post/PostSidebarAdWidget.tsx b/packages/shared/src/components/post/PostSidebarAdWidget.tsx index 5585c0a6e51..5e442100b06 100644 --- a/packages/shared/src/components/post/PostSidebarAdWidget.tsx +++ b/packages/shared/src/components/post/PostSidebarAdWidget.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react'; import React, { useCallback, useEffect } from 'react'; +import classNames from 'classnames'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import EntityCard from '../cards/entity/EntityCard'; import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton'; @@ -16,12 +17,15 @@ import { AdActions, AdPlacement } from '../../lib/ads'; import { adLogEvent } from '../../lib/feed'; import { adImprovementsV3Feature } from '../../lib/featureManagement'; import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query'; +import { TargetId } from '../../lib/log'; +import { combinedClicks } from '../../lib/click'; import { Typography, TypographyColor, TypographyTag, TypographyType, } from '../typography/Typography'; +import { AdvertiseLink } from '../cards/ad/common/AdvertiseLink'; interface PostSidebarAdWidgetProps { postId: string; @@ -37,7 +41,7 @@ export function PostSidebarAdWidget({ const { user } = useAuthContext(); const { isPlus } = usePlusSubscription(); const { logEvent } = useLogContext(); - const adImprovementsV3 = useFeature(adImprovementsV3Feature); + const adImprovementsV3 = Boolean(useFeature(adImprovementsV3Feature)); const { data: ad, isPending } = useAdQuery({ placement: AdPlacement.PostSidebar, @@ -96,7 +100,7 @@ export function PostSidebarAdWidget({ permalink={ad.link} entityName={ad.source} className={{ - container: className?.container, + container: classNames('relative cursor-pointer', className?.container), image: 'size-10 rounded-full', }} actionButtons={ @@ -107,12 +111,21 @@ export function PostSidebarAdWidget({ rel="noopener" variant={ButtonVariant.Primary} size={ButtonSize.Small} + className="relative z-1" onClick={() => onAdAction(AdActions.Click)} > Visit } > + onAdAction(AdActions.Click))} + />
{company && ( )} - + {hasDescription && ( )} +
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index e3a53c8e85f..92a4dc83f5a 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -51,6 +51,7 @@ export const featurePlusCtaCopy = new Feature('plus_cta_copy', { }); export const featureAutorotateAds = new Feature('autorotate_ads', 0); +export const featureAdReferralCta = new Feature('ad_referral_cta', false); export const featureFeedAdTemplate = new Feature('feed_ad_template', { default: { diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index d5be9bc4700..c6c285cd98b 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -466,6 +466,7 @@ export enum TargetType { AchievementCard = 'achievement card', BriefCard = 'brief card', HighlightsCard = 'highlights card', + AdvertiseHereCta = 'advertise here cta', } export enum TargetId { @@ -491,6 +492,9 @@ export enum TargetId { Header = 'header', ProfileDropdown = 'profile dropdown', Ads = 'ads', + AdCard = 'ad card', + AdSidebar = 'ad sidebar', + AdComment = 'ad comment', MyProfile = 'my profile', PlusBadge = 'plus badge', PlusPage = 'plus page', From 7eea87a700e999db45dcfdcea39a8a736c058de7 Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:34:27 +0300 Subject: [PATCH 2/3] fix: resolve shared lint issues for ad CTA changes Address the forwarded ref lint violations and formatting drift introduced in ad card components so the shared package passes CI again. Made-with: Cursor --- .../shared/src/components/cards/ad/AdGrid.tsx | 5 +- .../shared/src/components/cards/ad/AdList.tsx | 3 +- .../src/components/cards/ad/SignalAdList.tsx | 183 +++++++++--------- .../cards/ad/common/AdvertiseLink.tsx | 3 +- 4 files changed, 99 insertions(+), 95 deletions(-) diff --git a/packages/shared/src/components/cards/ad/AdGrid.tsx b/packages/shared/src/components/cards/ad/AdGrid.tsx index 023562a37ca..20ecab93450 100644 --- a/packages/shared/src/components/cards/ad/AdGrid.tsx +++ b/packages/shared/src/components/cards/ad/AdGrid.tsx @@ -44,7 +44,8 @@ export const AdGrid = forwardRef(function AdGrid( } if (forwardedRef) { - forwardedRef.current = nextNode; + const forwardedRefObject = forwardedRef; + forwardedRefObject.current = nextNode; } }, [forwardedRef], @@ -78,7 +79,7 @@ export const AdGrid = forwardRef(function AdGrid( rel="noopener" variant={ButtonVariant.Primary} size={ButtonSize.Small} - className="z-1 typo-footnote" + className="z-1 typo-footnote" {...combinedClicks(() => onLinkClick?.(ad))} > {ad.callToAction} diff --git a/packages/shared/src/components/cards/ad/AdList.tsx b/packages/shared/src/components/cards/ad/AdList.tsx index f8a6dc40fcb..7e0bc05add7 100644 --- a/packages/shared/src/components/cards/ad/AdList.tsx +++ b/packages/shared/src/components/cards/ad/AdList.tsx @@ -60,7 +60,8 @@ export const AdList = forwardRef(function AdCard( } if (forwardedRef) { - forwardedRef.current = nextNode; + const forwardedRefObject = forwardedRef; + forwardedRefObject.current = nextNode; } }, [forwardedRef], diff --git a/packages/shared/src/components/cards/ad/SignalAdList.tsx b/packages/shared/src/components/cards/ad/SignalAdList.tsx index c20ef079ad9..6dd51de1c84 100644 --- a/packages/shared/src/components/cards/ad/SignalAdList.tsx +++ b/packages/shared/src/components/cards/ad/SignalAdList.tsx @@ -39,102 +39,103 @@ const getLinkProps = ({ export const SignalAdList = forwardRef( function SignalAdList( - { ad, onLinkClick, domProps, index, feedIndex }: AdCardProps, - forwardedRef: ForwardedRef, -): ReactElement { - const { isPlus } = usePlusSubscription(); - const adImprovementsV3 = Boolean(useFeature(adImprovementsV3Feature)); - const matchingTags = ad.matchingTags ?? []; - const inViewRef = useCallback( - (node) => { - const nextNode = node as HTMLElement | null; + { ad, onLinkClick, domProps, index, feedIndex }: AdCardProps, + forwardedRef: ForwardedRef, + ): ReactElement { + const { isPlus } = usePlusSubscription(); + const adImprovementsV3 = Boolean(useFeature(adImprovementsV3Feature)); + const matchingTags = ad.matchingTags ?? []; + const inViewRef = useCallback( + (node) => { + const nextNode = node as HTMLElement | null; - if (typeof forwardedRef === 'function') { - forwardedRef(nextNode); - return; - } + if (typeof forwardedRef === 'function') { + forwardedRef(nextNode); + return; + } - if (forwardedRef) { - forwardedRef.current = nextNode; - } - }, - [forwardedRef], - ); - const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef); + if (forwardedRef) { + const forwardedRefObject = forwardedRef; + forwardedRefObject.current = nextNode; + } + }, + [forwardedRef], + ); + const { ref } = useAutoRotatingAds(ad, index, feedIndex, inViewRef); - const sourceName = ad.company?.trim() || ad.source?.trim() || 'Promoted'; - const sourceImage = getAdFaviconImageLink({ - ad, - adImprovementsV3, - size: 20, - }); - const sourceHandle = ad.company?.trim() || ad.source?.trim() || 'promoted'; + const sourceName = ad.company?.trim() || ad.source?.trim() || 'Promoted'; + const sourceImage = getAdFaviconImageLink({ + ad, + adImprovementsV3, + size: 20, + }); + const sourceHandle = ad.company?.trim() || ad.source?.trim() || 'promoted'; - return ( - -
-
- - - {sourceName} - - · - - {!isPlus && ( - } - className="relative z-1" + return ( + +
+
+ + + {sourceName} + + · + + {!isPlus && ( + } + className="relative z-1" + /> + )} +
+

+ {ad.description} +

+ {adImprovementsV3 && matchingTags.length > 0 ? ( + + ) : null} + {!!ad.callToAction && ( +
+ +
)}
-

- {ad.description} -

- {adImprovementsV3 && matchingTags.length > 0 ? ( - - ) : null} - {!!ad.callToAction && ( -
- -
- )} -
- - - ); -}, + + + ); + }, ); diff --git a/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx b/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx index 4c36d39f0db..2fc9862aac0 100644 --- a/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx +++ b/packages/shared/src/components/cards/ad/common/AdvertiseLink.tsx @@ -7,7 +7,8 @@ import { useLogContext } from '../../../../contexts/LogContext'; import { combinedClicks } from '../../../../lib/click'; import { featureAdReferralCta } from '../../../../lib/featureManagement'; import { businessWebsiteUrl } from '../../../../lib/constants'; -import { LogEvent, TargetId, TargetType } from '../../../../lib/log'; +import type { TargetId } from '../../../../lib/log'; +import { LogEvent, TargetType } from '../../../../lib/log'; import { anchorDefaultRel } from '../../../../lib/strings'; type AdvertiseLinkProps = { From 99f29effa3c2252463caad3a912177f92966219d Mon Sep 17 00:00:00 2001 From: Nimrod Kramer <41470823+nimrodkra@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:40:50 +0300 Subject: [PATCH 3/3] fix: address ad CTA review feedback Remove the dead feed ad refresh wiring left behind after the requested UI removal, and add the advertise CTA to SignalAdList so all feed ad variants covered by this change behave consistently. Made-with: Cursor --- .../src/components/FeedItemComponent.tsx | 6 --- .../src/components/cards/ad/AdCard.spec.tsx | 12 +++++- .../src/components/cards/ad/SignalAdList.tsx | 39 ++++++++++++------- .../src/components/cards/ad/common/common.tsx | 1 - 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index 243de76c3df..3744e8733c1 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -268,7 +268,6 @@ function FeedItemComponent({ onCommentClick, onReadArticleClick, virtualizedNumCards, - disableAdRefresh, }: FeedItemComponentProps): ReactElement | null { const { logEvent } = useLogContext(); const queryClient = useQueryClient(); @@ -516,11 +515,6 @@ function FeedItemComponent({ index={item.index} feedIndex={index} onLinkClick={(ad: Ad) => onAdAction(AdActions.Click, ad)} - onRefresh={ - disableAdRefresh - ? undefined - : (ad: Ad) => onAdAction(AdActions.Refresh, ad) - } /> ); } diff --git a/packages/shared/src/components/cards/ad/AdCard.spec.tsx b/packages/shared/src/components/cards/ad/AdCard.spec.tsx index 1290116756b..89dc6368ff4 100644 --- a/packages/shared/src/components/cards/ad/AdCard.spec.tsx +++ b/packages/shared/src/components/cards/ad/AdCard.spec.tsx @@ -39,10 +39,11 @@ const renderListComponent = ( const renderSignalListComponent = ( props: Partial = {}, + gb = new GrowthBook(), ): RenderResult => { const client = new QueryClient(); return render( - + @@ -173,3 +174,12 @@ it('should render company logo and company name in signal ad header', async () = expect(logo).toHaveAttribute('src', companyLogo); expect(screen.getByText(companyName)).toBeInTheDocument(); }); + +it('should render advertise link on signal list ad when feature is enabled', () => { + renderSignalListComponent({}, getGrowthBook(true)); + + expect(screen.getByRole('link', { name: 'Advertise here' })).toHaveAttribute( + 'href', + businessWebsiteUrl, + ); +}); diff --git a/packages/shared/src/components/cards/ad/SignalAdList.tsx b/packages/shared/src/components/cards/ad/SignalAdList.tsx index 6dd51de1c84..a4d927fce67 100644 --- a/packages/shared/src/components/cards/ad/SignalAdList.tsx +++ b/packages/shared/src/components/cards/ad/SignalAdList.tsx @@ -11,7 +11,10 @@ import FeedItemContainer from '../common/list/FeedItemContainer'; import { ProfileImageSize } from '../../ProfilePicture'; import AdAttribution from './common/AdAttribution'; import { useFeature } from '../../GrowthBookProvider'; -import { adImprovementsV3Feature } from '../../../lib/featureManagement'; +import { + adImprovementsV3Feature, + featureAdReferralCta, +} from '../../../lib/featureManagement'; import PostTags from '../common/PostTags'; import { Button } from '../../buttons/Button'; import { ButtonSize, ButtonVariant } from '../../buttons/common'; @@ -20,6 +23,8 @@ import { AdPixel } from './common/AdPixel'; import { SourceAvatar } from '../../profile/source/SourceAvatar'; import { MiniCloseIcon } from '../../icons'; import { getAdFaviconImageLink } from './common/getAdFaviconImageLink'; +import { AdvertiseLink } from './common/AdvertiseLink'; +import { TargetId } from '../../../lib/log'; const getLinkProps = ({ ad, @@ -44,6 +49,7 @@ export const SignalAdList = forwardRef( ): ReactElement { const { isPlus } = usePlusSubscription(); const adImprovementsV3 = Boolean(useFeature(adImprovementsV3Feature)); + const isAdReferralCtaEnabled = useFeature(featureAdReferralCta); const matchingTags = ad.matchingTags ?? []; const inViewRef = useCallback( (node) => { @@ -118,19 +124,26 @@ export const SignalAdList = forwardRef( {adImprovementsV3 && matchingTags.length > 0 ? ( ) : null} - {!!ad.callToAction && ( -
- + )} + onLinkClick?.(ad))} - > - {ad.callToAction} - + />
)}
diff --git a/packages/shared/src/components/cards/ad/common/common.tsx b/packages/shared/src/components/cards/ad/common/common.tsx index 59b350e986d..2b56f91c8e6 100644 --- a/packages/shared/src/components/cards/ad/common/common.tsx +++ b/packages/shared/src/components/cards/ad/common/common.tsx @@ -7,6 +7,5 @@ export interface AdCardProps { index: number; feedIndex: number; onLinkClick?: Callback; - onRefresh?: Callback; domProps?: HTMLAttributes; }