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
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ReactElement, ReactNode } from 'react';
import React, { useMemo } from 'react';
import { useInView } from 'react-intersection-observer';
import { useQuery } from '@tanstack/react-query';
import type { Squad } from '../../../graphql/sources';
import type { SourcesQueryProps } from '../../../hooks/source/useSources';
import {
Expand All @@ -21,11 +20,12 @@ import type { HorizontalScrollTitleProps } from '../../HorizontalScroll/Horizont
import { HorizontalScrollTitle } from '../../HorizontalScroll/HorizontalScrollHeader';
import { generateQueryKey, RequestKey } from '../../../lib/query';
import { useAuthContext } from '../../../contexts/AuthContext';
import { fetchDirectoryAd } from '../../../lib/ads';
import { AdPlacement } from '../../../lib/ads';
import { LogExtraContextProvider } from '../../../contexts/LogExtraContext';
import type { Ad } from '../../../graphql/posts';
import { AdPixel } from '../ad/common/AdPixel';
import { TargetType } from '../../../lib/log';
import { useAdQuery } from '../../../features/monetization/useAdQuery';

interface SquadHorizontalListProps {
title: HorizontalScrollTitleProps;
Expand Down Expand Up @@ -97,14 +97,14 @@ export function SquadsDirectoryFeed({
const { isFetched } = result;
const isMobile = useViewSize(ViewSize.MobileL);
const isLoading = !isFetched || (!inView && !result.data);
const { data: ad, isLoading: isLoadingAd } = useQuery({
const { data: ad, isLoading: isLoadingAd } = useAdQuery({
placement: AdPlacement.SquadDirectory,
queryKey: generateQueryKey(
RequestKey.Ads,
user,
'squads_directory',
title.copy,
),
queryFn: fetchDirectoryAd,
enabled: firstItemShouldBeAd && isAuthReady && !user?.isPlus,
});
const { squad: squadAd } = useSquad({ handle: ad?.data?.source?.handle });
Expand Down
13 changes: 4 additions & 9 deletions packages/shared/src/components/comments/AdAsComment.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ReactElement } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { TruncateText } from '../utilities';
import AdLink from '../cards/ad/common/AdLink';
import { adLogEvent } from '../../lib/feed';
Expand All @@ -11,13 +10,13 @@ import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query';
import { useAuthContext } from '../../contexts/AuthContext';
import { cloudinaryPostImageCoverPlaceholder } from '../../lib/image';
import { AdPixel } from '../cards/ad/common/AdPixel';
import { AdActions, fetchCommentAd } from '../../lib/ads';
import { AdActions, AdPlacement } from '../../lib/ads';
import { usePlusSubscription } from '../../hooks/usePlusSubscription';
import { RemoveAd } from '../cards/ad/common/RemoveAd';
import { AdRefresh } from '../cards/ad/common/AdRefresh';
import { ButtonVariant } from '../buttons/common';
import { MiniCloseIcon } from '../icons';
import type { Ad } from '../../graphql/posts';
import { useAdQuery } from '../../features/monetization/useAdQuery';
import { ImpressionStatus } from '../../hooks/feed/useLogImpression';
import { useScrambler } from '../../hooks/useScrambler';

Expand All @@ -34,13 +33,9 @@ export const AdAsComment = ({ postId }: AdAsCommentProps): ReactElement => {
refetch,
isFetching,
isRefetching,
} = useQuery<Ad>({
} = useAdQuery({
placement: AdPlacement.PostComment,
queryKey: generateQueryKey(RequestKey.Ads, user, postId),
queryFn: fetchCommentAd,
enabled: true,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: StaleTime.OneHour,
});

Expand Down
14 changes: 4 additions & 10 deletions packages/shared/src/components/post/PostSidebarAdWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ReactElement } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import EntityCard from '../cards/entity/EntityCard';
import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton';
Expand All @@ -10,11 +9,10 @@ import { getAdFaviconImageLink } from '../cards/ad/common/getAdFaviconImageLink'
import { useFeature } from '../GrowthBookProvider';
import { useAuthContext } from '../../contexts/AuthContext';
import { useLogContext } from '../../contexts/LogContext';
import type { Ad } from '../../graphql/posts';
import { useFetchAd } from '../../features/monetization/useFetchAd';
import { useAdQuery } from '../../features/monetization/useAdQuery';
import { usePlusSubscription } from '../../hooks/usePlusSubscription';
import { ImpressionStatus } from '../../hooks/feed/useLogImpression';
import { AdActions } from '../../lib/ads';
import { AdActions, AdPlacement } from '../../lib/ads';
import { adLogEvent } from '../../lib/feed';
import { adImprovementsV3Feature } from '../../lib/featureManagement';
import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query';
Expand All @@ -39,16 +37,12 @@ export function PostSidebarAdWidget({
const { user } = useAuthContext();
const { isPlus } = usePlusSubscription();
const { logEvent } = useLogContext();
const { fetchAd } = useFetchAd();
const adImprovementsV3 = useFeature(adImprovementsV3Feature);

const { data: ad, isPending } = useQuery<Ad | null>({
const { data: ad, isPending } = useAdQuery({
placement: AdPlacement.PostSidebar,
queryKey: generateQueryKey(RequestKey.Ads, user, postId, 'post-sidebar'),
queryFn: () => fetchAd({ active: false }),
enabled: !isPlus,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: StaleTime.OneHour,
});

Expand Down
45 changes: 45 additions & 0 deletions packages/shared/src/features/monetization/useAdQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { QueryKey } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useFeature } from '../../components/GrowthBookProvider';
import type { Ad } from '../../graphql/posts';
import { fetchAdByPlacement, resolveAdFetchOptions } from '../../lib/ads';
import type { FetchAdByPlacementOptions } from '../../lib/ads';
import { featurePostBoostAds } from '../../lib/featureManagement';

interface UseAdQueryOptions {
queryKey: QueryKey;
enabled?: boolean;
staleTime?: number;
placement: FetchAdByPlacementOptions['placement'];
active?: boolean;
}

export const useAdQuery = ({
queryKey,
enabled = true,
staleTime,
placement,
active,
}: UseAdQueryOptions) => {
const boostsEnabled = useFeature(featurePostBoostAds);
const fetchOptions = useMemo(
() =>
resolveAdFetchOptions({
placement,
active,
boostsEnabled,
}),
[placement, active, boostsEnabled],
);

return useQuery<Ad | null>({
queryKey,
queryFn: () => fetchAdByPlacement(fetchOptions),
enabled,
staleTime,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
};
34 changes: 19 additions & 15 deletions packages/shared/src/features/monetization/useFetchAd.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { useCallback } from 'react';
import { useFeature } from '../../components/GrowthBookProvider';
import type { Ad } from '../../graphql/posts';
import { fetchAd } from '../../lib/ads';
import {
AdPlacement,
fetchAdByPlacement,
resolveAdFetchOptions,
} from '../../lib/ads';
import { featurePostBoostAds } from '../../lib/featureManagement';

interface UseFetchAds {
fetchAd: (params: { active?: boolean }) => Promise<Ad | null>;
fetchAd: (params: {
active?: boolean;
placement?: AdPlacement;
}) => Promise<Ad | null>;
}

export const useFetchAd = (): UseFetchAds => {
const isEnabled = useFeature(featurePostBoostAds);
const boostsEnabled = useFeature(featurePostBoostAds);

const fetchAdQuery: UseFetchAds['fetchAd'] = useCallback(
({ active }) => {
const params = new URLSearchParams({
active: active ? 'true' : 'false',
});

if (isEnabled) {
params.append('allow_post_boost', 'true');
params.append('allow_squad_boost', 'true');
}

return fetchAd(params);
({ active, placement = AdPlacement.Feed }) => {
return fetchAdByPlacement(
resolveAdFetchOptions({
placement,
active,
boostsEnabled,
}),
);
},
[isEnabled],
[boostsEnabled],
);

return {
Expand Down
6 changes: 5 additions & 1 deletion packages/shared/src/hooks/feed/useAutoRotatingAds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { featureAutorotateAds } from '../../lib/featureManagement';
import type { Ad } from '../../graphql/posts';
import { generateAdLogEventKey } from './useLogImpression';
import { disabledRefetch } from '../../lib/func';
import { AdPlacement } from '../../lib/ads';
import { RequestKey } from '../../lib/query';
import { useFetchAd } from '../../features/monetization/useFetchAd';

Expand Down Expand Up @@ -54,7 +55,10 @@ export const useAutoRotatingAds = (
);

const fetchNewAd = useCallback(async (): Promise<Ad> => {
const newAd = await fetchAd({ active: true });
const newAd = await fetchAd({
placement: AdPlacement.Feed,
active: true,
});
if (!newAd) {
return null;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/shared/src/hooks/useFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { FeedAdTemplate } from '../lib/feed';
import { featureFeedAdTemplate } from '../lib/featureManagement';
import { cloudinaryPostImageCoverPlaceholder } from '../lib/image';
import { AD_PLACEHOLDER_SOURCE_ID } from '../lib/constants';
import { AdPlacement } from '../lib/ads';
import { SharedFeedPage } from '../components/utilities';
import { useTranslation } from './translation/useTranslation';
import { useFetchAd } from '../features/monetization/useFetchAd';
Expand Down Expand Up @@ -210,7 +211,10 @@ export default function useFeed<T>(
const adsQuery = useInfiniteQuery<Ad>({
queryKey: [RequestKey.Ads, ...feedQueryKey],
queryFn: async ({ pageParam }) => {
const ad = await fetchAd({ active: !!pageParam });
const ad = await fetchAd({
placement: AdPlacement.Feed,
active: !!pageParam,
});

if (!ad) {
return {
Expand Down
101 changes: 101 additions & 0 deletions packages/shared/src/lib/ads.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import ad from '../../__tests__/fixture/ad';
import { AdPlacement, fetchAdByPlacement, resolveAdFetchOptions } from './ads';

describe('ads', () => {
const originalFetch = global.fetch;

beforeEach(() => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue([ad]),
headers: {
get: (name: string) =>
name === 'x-generation-id' ? 'gen-123' : undefined,
},
} as unknown as Response) as typeof fetch;
});

afterEach(() => {
global.fetch = originalFetch;
jest.clearAllMocks();
});

describe('resolveAdFetchOptions', () => {
it('should enable boost flags for feed placements', () => {
expect(
resolveAdFetchOptions({
placement: AdPlacement.Feed,
active: true,
boostsEnabled: true,
}),
).toEqual({
placement: AdPlacement.Feed,
active: true,
allowPostBoost: true,
allowSquadBoost: true,
});
});

it('should keep comment placement minimal', () => {
expect(
resolveAdFetchOptions({
placement: AdPlacement.PostComment,
boostsEnabled: true,
}),
).toEqual({
placement: AdPlacement.PostComment,
});
});

it('should always enable squad boost for directory placement', () => {
expect(
resolveAdFetchOptions({
placement: AdPlacement.SquadDirectory,
boostsEnabled: false,
}),
).toEqual({
placement: AdPlacement.SquadDirectory,
allowSquadBoost: true,
});
});
});

describe('fetchAdByPlacement', () => {
it('should fetch feed ads from the general ads endpoint', async () => {
const result = await fetchAdByPlacement({
placement: AdPlacement.Feed,
active: true,
allowPostBoost: true,
allowSquadBoost: true,
});

expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3000/v1/a?active=true&allow_post_boost=true&allow_squad_boost=true',
{ credentials: 'include' },
);
expect(result).toEqual({ ...ad, generationId: 'gen-123' });
});

it('should fetch comment ads from the post ads endpoint', async () => {
await fetchAdByPlacement({
placement: AdPlacement.PostComment,
});

expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3000/v1/a/post',
{ credentials: 'include' },
);
});

it('should fetch directory ads from the squads directory endpoint', async () => {
await fetchAdByPlacement({
placement: AdPlacement.SquadDirectory,
allowSquadBoost: true,
});

expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3000/v1/a/squads_directory?allow_squad_boost=true',
{ credentials: 'include' },
);
});
});
});
Loading
Loading