diff --git a/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx b/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx index 8717308efd..d9f5c96f08 100644 --- a/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx +++ b/packages/shared/src/components/cards/highlight/HighlightPostSidebarWidget.tsx @@ -33,16 +33,18 @@ const prefersReducedMotion = (): boolean => { }; export const HighlightPostSidebarWidget = (): ReactElement | null => { - const { user } = useAuthContext(); + const { isAuthReady } = useAuthContext(); const { logEvent } = useLogContext(); + // Shown to logged-in AND anonymous readers: the live dev pulse is the + // strongest "this place is alive" hook for a first-time visitor. const { value: isEnabled } = useConditionalFeature({ feature: featurePostPageHighlights, - shouldEvaluate: !!user, + shouldEvaluate: isAuthReady, }); const { data } = useQuery({ ...majorHeadlinesQueryOptions({ first: HIGHLIGHTS_LIMIT }), - enabled: isEnabled && !!user, + enabled: isEnabled, refetchInterval: ONE_MINUTE, }); diff --git a/packages/shared/src/components/post/BasePostContent.tsx b/packages/shared/src/components/post/BasePostContent.tsx index 0e1d81a904..3b971c022d 100644 --- a/packages/shared/src/components/post/BasePostContent.tsx +++ b/packages/shared/src/components/post/BasePostContent.tsx @@ -6,6 +6,7 @@ import PostEngagements from './PostEngagements'; import type { BasePostContentProps } from './common'; import { PostHeaderActions } from './PostHeaderActions'; import { ButtonSize } from '../buttons/common'; +import { ReadNext } from '../../features/postPageOnboarding/ReadNext'; const Custom404 = dynamic( () => import(/* webpackChunkName: "custom404" */ '../Custom404'), @@ -70,6 +71,7 @@ export function BasePostContent({ shouldOnboardAuthor={shouldOnboardAuthor} /> )} + {isPostPage && } ); } diff --git a/packages/shared/src/components/post/PostContent.tsx b/packages/shared/src/components/post/PostContent.tsx index 17e4a1f73a..0161bffc05 100644 --- a/packages/shared/src/components/post/PostContent.tsx +++ b/packages/shared/src/components/post/PostContent.tsx @@ -28,6 +28,8 @@ import { useSmartTitle } from '../../hooks/post/useSmartTitle'; import { PostTagList } from './tags/PostTagList'; import PostSourceInfo from './PostSourceInfo'; import { useReaderInstallPromptGate } from '../../hooks/useReaderInstallPromptGate'; +import { useAnonPostOnboarding } from '../../features/postPageOnboarding/useAnonPostOnboarding'; +import { AnonSourceStrip } from '../../features/postPageOnboarding/AnonSourceStrip'; type PostContentRawProps = Omit & { post: Post }; @@ -98,6 +100,10 @@ export function PostContentRaw({ : undefined, }, ); + // Anonymous "build your feed" experience — only on the full post page. + const { isEnabled: isAnonExperience } = useAnonPostOnboarding(); + const anonExperienceActive = isAnonExperience && !!isPostPage; + const handleImageClick = (event: React.MouseEvent) => { if (onReaderInstallGateClick(event)) { return; @@ -170,15 +176,24 @@ export function PostContentRaw({ post={post} >
-
- void} onReadArticle={onReadArticle} - hideSubscribeAction={hideSubscribeAction} /> -
+ ) : ( +
+ +
+ )}

- {post.clickbaitTitleDetected && } + {post.clickbaitTitleDetected && !anonExperienceActive && ( + + )}
{isVideoType && ( - {postMainColumn} - {postWidgetsColumn} - + <> + + {postMainColumn} + {postWidgetsColumn} + + ); } diff --git a/packages/shared/src/components/post/PostWidgets.tsx b/packages/shared/src/components/post/PostWidgets.tsx index 27bf7aa79c..bb3059f5f0 100644 --- a/packages/shared/src/components/post/PostWidgets.tsx +++ b/packages/shared/src/components/post/PostWidgets.tsx @@ -16,8 +16,10 @@ import EntityCardSkeleton from '../cards/entity/EntityCardSkeleton'; import { PostSidebarAdWidget } from './PostSidebarAdWidget'; import { FeaturedArchives } from '../widgets/FeaturedArchives'; import { MentionedToolsWidget } from '../brand/MentionedToolsWidget'; -import { PostSignupWidget } from './PostSignupWidget'; import { HighlightPostSidebarWidget } from '../cards/highlight/HighlightPostSidebarWidget'; +import { PostSignupWidget } from './PostSignupWidget'; +import { useAnonPostOnboarding } from '../../features/postPageOnboarding/useAnonPostOnboarding'; +import { BuildFeedConversionCard } from '../../features/postPageOnboarding/BuildFeedConversionCard'; const UserEntityCard = dynamic( /* webpackChunkName: "userEntityCard" */ () => @@ -53,10 +55,27 @@ export function PostWidgets({ origin, }: PostWidgetsProps): ReactElement { const { tokenRefreshed } = useContext(AuthContext); + const { isEnabled: isAnonExperience } = useAnonPostOnboarding(); const { source } = post; const cardClasses = 'w-full bg-transparent'; + // Anonymous "build your feed" experience: the whole right column becomes a + // single cohesive conversion card, with the promo demoted to the last slot. + if (isAnonExperience) { + // Right rail for anonymous readers: a single calm, sticky invite. No ads + // or promos competing for a first-time reader's trust; "Happening Now" is + // reserved for signed-in users. + return ( + +
+ +
+ +
+ ); + } + const creator = post.author || post.scout; let sourceCard = null; diff --git a/packages/shared/src/features/postPageOnboarding/AnonSourceStrip.tsx b/packages/shared/src/features/postPageOnboarding/AnonSourceStrip.tsx new file mode 100644 index 0000000000..68e21c3c1a --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/AnonSourceStrip.tsx @@ -0,0 +1,45 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import type { Post } from '../../graphql/posts'; +import type { SourceTooltip } from '../../graphql/sources'; +import { SourceStrip } from '../../components/post/reader/SourceStrip'; +import { PostHeaderActions } from '../../components/post/PostHeaderActions'; +import { ButtonSize } from '../../components/buttons/Button'; + +interface AnonSourceStripProps { + post: Post; + onReadArticle?: () => void; + onClose?: () => void; + className?: string; +} + +/** + * The post-page source area for anonymous readers, styled after the "Read + * inside daily.dev" reader: a single horizontal strip with the source + * (avatar + name), the "Read post" button, and the three-dots menu. Cleaner + * and more action-oriented than the legacy inline source line. + */ +export const AnonSourceStrip = ({ + post, + onReadArticle, + onClose, + className, +}: AnonSourceStripProps): ReactElement => ( +
+ + +
+); diff --git a/packages/shared/src/features/postPageOnboarding/BuildFeedAuthOptions.tsx b/packages/shared/src/features/postPageOnboarding/BuildFeedAuthOptions.tsx new file mode 100644 index 0000000000..73e836d26a --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/BuildFeedAuthOptions.tsx @@ -0,0 +1,50 @@ +import type { MutableRefObject, ReactElement } from 'react'; +import React from 'react'; +import AuthOptions from '../../components/auth/AuthOptions'; +import { AuthDisplay } from '../../components/auth/common'; +import { AuthTriggers } from '../../lib/auth'; +import { ButtonSize, ButtonVariant } from '../../components/buttons/Button'; +import { useBuildFeedSignup } from './useBuildFeedSignup'; +import type { BuildFeedSignupOrigin } from './useBuildFeedSignup'; + +interface BuildFeedAuthOptionsProps { + tags: string[]; + origin: BuildFeedSignupOrigin; + className?: string; + hideLoginLink?: boolean; +} + +/** + * Inline signup (Google / GitHub / email) reused across the anonymous + * build-feed surfaces. One-tap social signs up in place; the email path + * escalates to the modal. Either way the followed topics are applied to the + * new feed. + */ +export const BuildFeedAuthOptions = ({ + tags, + origin, + className, + hideLoginLink = false, +}: BuildFeedAuthOptionsProps): ReactElement => { + const { getAuthStateHandler, applyFollowedTags } = useBuildFeedSignup(); + + return ( + } + trigger={AuthTriggers.PostPage} + simplified + defaultDisplay={AuthDisplay.OnboardingSignup} + forceDefaultDisplay + className={{ onboardingSignup: className ?? '!gap-3' }} + onAuthStateUpdate={getAuthStateHandler(tags, origin)} + onSuccessfulRegistration={() => applyFollowedTags(tags)} + onboardingSignupButton={{ + variant: ButtonVariant.Primary, + size: ButtonSize.Medium, + }} + hideLoginLink={hideLoginLink} + compact + /> + ); +}; diff --git a/packages/shared/src/features/postPageOnboarding/BuildFeedConversionCard.tsx b/packages/shared/src/features/postPageOnboarding/BuildFeedConversionCard.tsx new file mode 100644 index 0000000000..52e5e34a94 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/BuildFeedConversionCard.tsx @@ -0,0 +1,104 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { Post } from '../../graphql/posts'; +import { capitalize } from '../../lib/strings'; +import { + Button, + ButtonColor, + ButtonSize, + ButtonVariant, +} from '../../components/buttons/Button'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../components/typography/Typography'; +import { useAnonFeedTags } from './useAnonFeedTags'; +import { BuildFeedAuthOptions } from './BuildFeedAuthOptions'; + +interface BuildFeedConversionCardProps { + post: Post; +} + +const MAX_CHIPS = 6; + +/** + * The anonymous right rail — deliberately calm and substance-first. No FOMO, + * no fake proof, no animation: an honest one-line description of what + * daily.dev is, the article's real topics presented as a quiet way to shape a + * feed, and a single low-key signup. The value does the work, not the pitch. + */ +export const BuildFeedConversionCard = ({ + post, +}: BuildFeedConversionCardProps): ReactElement => { + const { chips, selectedTags, toggleTag } = useAnonFeedTags({ + postTags: post?.tags ?? [], + enabled: true, + }); + + return ( + + ); +}; diff --git a/packages/shared/src/features/postPageOnboarding/ReadNext.tsx b/packages/shared/src/features/postPageOnboarding/ReadNext.tsx new file mode 100644 index 0000000000..2ba0098661 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/ReadNext.tsx @@ -0,0 +1,181 @@ +import type { ReactElement } from 'react'; +import React, { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import type { Post } from '../../graphql/posts'; +import { gqlClient } from '../../graphql/common'; +import { + FURTHER_READING_QUERY, + type FurtherReadingData, +} from '../../graphql/furtherReading'; +import { capitalize } from '../../lib/strings'; +import { CardLink } from '../../components/cards/common/Card'; +import { ElementPlaceholder } from '../../components/ElementPlaceholder'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../../components/typography/Typography'; +import { useAnonPostOnboarding } from './useAnonPostOnboarding'; +import { useAnonFeedTags } from './useAnonFeedTags'; +import { BuildFeedAuthOptions } from './BuildFeedAuthOptions'; + +interface ReadNextProps { + post: Post; +} + +const MAX_ITEMS = 7; + +const dedupeById = (posts: Post[], excludeId?: string): Post[] => { + const seen = new Set(); + return posts.filter((item) => { + if (!item?.id || item.id === excludeId || seen.has(item.id)) { + return false; + } + seen.add(item.id); + return true; + }); +}; + +const ReadNextItem = ({ + post, + index, +}: { + post: Post; + index: number; +}): ReactElement => { + const meta = [ + post.source?.name, + post.readTime ? `${post.readTime} min read` : null, + ] + .filter(Boolean) + .join(' · '); + + return ( +
+ + + {(index + 1).toString().padStart(2, '0')} + +
+

+ {post.title} +

+ {meta && ( + + {meta} + + )} +
+
+ ); +}; + +/** + * "Read next" — real related posts (similar / trending / discussed) for the + * current article, presented as a calm editorial column. No images-as-bait, no + * locked rows, no fabricated content: if there's nothing real to show, it + * renders nothing. The invitation at the end is honest and low-key. + */ +export const ReadNext = ({ post }: ReadNextProps): ReactElement | null => { + const { isEnabled } = useAnonPostOnboarding(); + const { selectedTags } = useAnonFeedTags({ + postTags: post?.tags ?? [], + enabled: isEnabled, + }); + const tags = post?.tags ?? []; + + const { data, isLoading } = useQuery({ + queryKey: ['readNext', post?.id], + queryFn: () => + gqlClient.request(FURTHER_READING_QUERY, { + loggedIn: false, + post: post.id, + trendingFirst: 3, + similarFirst: MAX_ITEMS, + discussedFirst: 4, + withDiscussedPosts: true, + tags, + }), + enabled: isEnabled && !!post?.id && tags.length > 0, + staleTime: 5 * 60 * 1000, + }); + + const posts = useMemo(() => { + const merged = [ + ...(data?.similarPosts ?? []), + ...(data?.trendingPosts ?? []), + ...(data?.discussedPosts ?? []), + ]; + return dedupeById(merged, post?.id).slice(0, MAX_ITEMS); + }, [data, post?.id]); + + if (!isEnabled || tags.length === 0) { + return null; + } + + if (!isLoading && posts.length === 0) { + return null; + } + + const topic = tags[0] ? capitalize(tags[0]) : null; + + return ( +
+ + Read next + + + {topic + ? `More on ${topic}, from what the developer community is reading.` + : 'More from what the developer community is reading.'} + + +
+ {isLoading ? ( + Array.from({ length: 4 }).map((_, index) => ( +
+ + +
+ )) + ) : ( + <> + {posts.map((item, index) => ( + + ))} + + )} +
+ +
+ + This is a glimpse of what a daily.dev feed gives you every day — the + best of {topic ?? 'your stack'}, curated and tuned to you. Make one + that's yours. + +
+ +
+
+
+ ); +}; diff --git a/packages/shared/src/features/postPageOnboarding/common.ts b/packages/shared/src/features/postPageOnboarding/common.ts new file mode 100644 index 0000000000..4158eae533 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/common.ts @@ -0,0 +1,5 @@ +// Local persistence keys for the anonymous post-page "build your feed" +// experience. Kept here (not in the shared PersistentContextKeys enum) so the +// feature is fully self-contained. +export const ANON_FEED_TAGS_KEY = 'anon_feed_tags'; +export const ANON_CONVERT_PROMPT_SEEN_KEY = 'anon_convert_prompt_seen'; diff --git a/packages/shared/src/features/postPageOnboarding/useAnonFeedTags.ts b/packages/shared/src/features/postPageOnboarding/useAnonFeedTags.ts new file mode 100644 index 0000000000..e1af4b8e57 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/useAnonFeedTags.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import usePersistentContext from '../../hooks/usePersistentContext'; +import { ANON_FEED_TAGS_KEY } from './common'; + +const MAX_SEED_TAGS = 5; + +interface UseAnonFeedTagsProps { + /** Tags of the post currently being read — the personalization seed. */ + postTags: string[]; + /** Only seed/persist when the experience is active. */ + enabled: boolean; +} + +interface UseAnonFeedTags { + /** Tag chips to render: this post's tags plus anything followed earlier. */ + chips: string[]; + /** The tags the visitor is currently following (accumulates across posts). */ + selectedTags: string[]; + /** Tags to drive the live feed taste — falls back to the post's tags. */ + previewTags: string[]; + isReady: boolean; + toggleTag: (tag: string) => void; +} + +const dedupe = (tags: string[]): string[] => Array.from(new Set(tags)); + +/** + * Progressive, no-password personalization. The article's own tags are the + * single strongest signal we have about an anonymous reader, so we pre-select + * them and let the visitor refine — all stored locally (IndexedDB) with no + * account. The selection accumulates across posts so the feed they're shown + * keeps getting more "theirs" the more they read. + */ +export const useAnonFeedTags = ({ + postTags, + enabled, +}: UseAnonFeedTagsProps): UseAnonFeedTags => { + const cleanPostTags = useMemo(() => dedupe(postTags ?? []), [postTags]); + const [stored, setStored, isFetched] = + usePersistentContext(ANON_FEED_TAGS_KEY); + const hasSeeded = useRef(false); + + // Seed once from the current article's tags for a brand-new visitor. + useEffect(() => { + if (!enabled || !isFetched || hasSeeded.current) { + return; + } + hasSeeded.current = true; + if (!stored && cleanPostTags.length > 0) { + setStored(cleanPostTags.slice(0, MAX_SEED_TAGS)); + } + }, [enabled, isFetched, stored, cleanPostTags, setStored]); + + const selectedTags = useMemo(() => stored ?? [], [stored]); + + const toggleTag = useCallback( + (tag: string) => { + const next = selectedTags.includes(tag) + ? selectedTags.filter((item) => item !== tag) + : [...selectedTags, tag]; + setStored(next); + }, + [selectedTags, setStored], + ); + + const chips = useMemo( + () => dedupe([...cleanPostTags, ...selectedTags]), + [cleanPostTags, selectedTags], + ); + + const previewTags = selectedTags.length > 0 ? selectedTags : cleanPostTags; + + return { + chips, + selectedTags, + previewTags, + isReady: isFetched, + toggleTag, + }; +}; diff --git a/packages/shared/src/features/postPageOnboarding/useAnonPostOnboarding.ts b/packages/shared/src/features/postPageOnboarding/useAnonPostOnboarding.ts new file mode 100644 index 0000000000..b869fcb893 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/useAnonPostOnboarding.ts @@ -0,0 +1,30 @@ +import { useAuthContext } from '../../contexts/AuthContext'; +import { useConditionalFeature } from '../../hooks/useConditionalFeature'; +import { featurePostBuildFeed } from '../../lib/featureManagement'; + +interface UseAnonPostOnboarding { + /** True only for logged-out visitors once auth is resolved. */ + isAnonymous: boolean; + /** True when the anonymous build-feed experience should render. */ + isEnabled: boolean; +} + +/** + * Central gate for the anonymous post-page "build your feed" experience. + * Every piece of the experience (sidebar widget, feed taste, conversion + * prompt, banner consolidation) reads from this single hook so the behavior + * is consistent and evaluated once per surface. + */ +export const useAnonPostOnboarding = (): UseAnonPostOnboarding => { + const { user, isAuthReady } = useAuthContext(); + const isAnonymous = isAuthReady && !user; + const { value: isFlagEnabled } = useConditionalFeature({ + feature: featurePostBuildFeed, + shouldEvaluate: isAnonymous, + }); + + return { + isAnonymous, + isEnabled: isAnonymous && isFlagEnabled, + }; +}; diff --git a/packages/shared/src/features/postPageOnboarding/useBuildFeedSignup.ts b/packages/shared/src/features/postPageOnboarding/useBuildFeedSignup.ts new file mode 100644 index 0000000000..5f4cd98c19 --- /dev/null +++ b/packages/shared/src/features/postPageOnboarding/useBuildFeedSignup.ts @@ -0,0 +1,98 @@ +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useLogContext } from '../../contexts/LogContext'; +import { gqlClient } from '../../graphql/common'; +import { ADD_FILTERS_TO_FEED_MUTATION } from '../../graphql/feedSettings'; +import { AuthTriggers } from '../../lib/auth'; +import { LogEvent } from '../../lib/log'; +import { RequestKey } from '../../lib/query'; +import type { AuthProps } from '../../components/auth/common'; + +export type BuildFeedSignupOrigin = 'sidebar' | 'feed'; + +interface UseBuildFeedSignup { + /** Apply the followed topics to the new account's feed (best effort). */ + applyFollowedTags: (tags: string[]) => void; + /** Open the auth modal from a button, carrying the followed topics. */ + triggerSignup: (tags: string[], origin: BuildFeedSignupOrigin) => void; + /** Handler for inline AuthOptions' email-continue path (escalates to modal). */ + getAuthStateHandler: ( + tags: string[], + origin: BuildFeedSignupOrigin, + ) => (props: Partial) => void; +} + +/** + * Signup wiring for the anonymous build-feed surfaces. The tags the visitor + * followed (no-password, stored locally) are applied to their feed the moment + * the account is created — whether they sign up via an inline social button + * (onSuccessfulRegistration) or via the email→modal path (onRegistrationSuccess). + */ +export const useBuildFeedSignup = (): UseBuildFeedSignup => { + const { showLogin } = useAuthContext(); + const { logEvent } = useLogContext(); + const queryClient = useQueryClient(); + + const applyFollowedTags = useCallback( + (tags: string[]) => { + if (!tags?.length) { + return; + } + gqlClient + .request(ADD_FILTERS_TO_FEED_MUTATION, { + filters: { includeTags: tags }, + }) + .then(() => + queryClient.invalidateQueries({ + predicate: (query) => + query.queryKey.includes(RequestKey.FeedSettings), + }), + ) + .catch(() => { + // Onboarding lets them pick topics anyway; never block signup. + }); + }, + [queryClient], + ); + + const logStart = useCallback( + (tags: string[], origin: BuildFeedSignupOrigin) => { + logEvent({ + event_name: LogEvent.Click, + extra: JSON.stringify({ origin, tags }), + }); + }, + [logEvent], + ); + + const triggerSignup = useCallback( + (tags: string[], origin: BuildFeedSignupOrigin) => { + logStart(tags, origin); + showLogin({ + trigger: AuthTriggers.PostPage, + options: { onRegistrationSuccess: () => applyFollowedTags(tags) }, + }); + }, + [showLogin, logStart, applyFollowedTags], + ); + + const getAuthStateHandler = useCallback( + (tags: string[], origin: BuildFeedSignupOrigin) => + (props: Partial) => { + logStart(tags, origin); + showLogin({ + trigger: AuthTriggers.PostPage, + options: { + isLogin: !!props.isLoginFlow, + defaultDisplay: props.defaultDisplay, + formValues: props.email ? { email: props.email } : undefined, + onRegistrationSuccess: () => applyFollowedTags(tags), + }, + }); + }, + [showLogin, logStart, applyFollowedTags], + ); + + return { applyFollowedTags, triggerSignup, getAuthStateHandler }; +}; diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index 132e1e7173..e18408347a 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -34,7 +34,8 @@ export const latestFeedVersion = new Feature('latest_feed_version', 2); export const customFeedVersion = new Feature('custom_feed_version', 2); export const featurePostPageHighlights = new Feature( 'post_page_highlights', - false, + // TODO: revert to false — temporarily on to preview the anonymous experience. + true, ); // @ts-expect-error stale feature without default @@ -177,6 +178,13 @@ export const featureOnboardingPersonas = new Feature( export const featurePostSignupWidget = new Feature('post_signup_widget', false); +// Anonymous post-page "build your feed" conversion experience. When enabled, +// it replaces the simple signup widget with a personalized feed-building +// surface and consolidates the redundant auth banners into a single timed +// prompt. +// TODO: revert to false — temporarily on to preview the anonymous experience. +export const featurePostBuildFeed = new Feature('post_build_feed', true); + export const featureReaderModal = new Feature('reader_modal_v2', false); export const featureGenericReferralPopupV2 = new Feature( diff --git a/packages/webapp/pages/posts/[id]/index.tsx b/packages/webapp/pages/posts/[id]/index.tsx index fce81b9431..e02d33e942 100644 --- a/packages/webapp/pages/posts/[id]/index.tsx +++ b/packages/webapp/pages/posts/[id]/index.tsx @@ -48,6 +48,7 @@ import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import useDebounceFn from '@dailydotdev/shared/src/hooks/useDebounceFn'; import { useEngagementAdsContext } from '@dailydotdev/shared/src/contexts/EngagementAdsContext'; import { CompanionDemoWidget } from '@dailydotdev/shared/src/components/post/CompanionDemoWidget'; +import { useAnonPostOnboarding } from '@dailydotdev/shared/src/features/postPageOnboarding/useAnonPostOnboarding'; import { getPageSeoTitles } from '../../../components/layouts/utils'; import { getLayout } from '../../../components/layouts/MainLayout'; import FooterNavBarLayout from '../../../components/layouts/FooterNavBarLayout'; @@ -186,6 +187,11 @@ export const PostPage = ({ const router = useRouter(); const isFallback = false; const { shouldShowAuthBanner } = useOnboardingActions(); + // When the "build your feed" experience is on, it owns the single conversion + // surface (sidebar widget + one timed prompt), so suppress the redundant + // post-page auth banners. + const { isEnabled: isBuildFeedExperience } = useAnonPostOnboarding(); + const showAuthBanner = shouldShowAuthBanner && !isBuildFeedExperience; const isLaptop = useViewSize(ViewSize.Laptop); const { post, isError, isLoading } = usePostById({ id, @@ -278,7 +284,7 @@ export const PostPage = ({ backToSquad={!!router?.query?.squad} shouldOnboardAuthor={!!router.query?.author} origin={Origin.ArticlePage} - isBannerVisible={shouldShowAuthBanner && !isLaptop} + isBannerVisible={showAuthBanner && !isLaptop} className={{ container: containerClass, fixedNavigation: { container: 'flex laptop:hidden' }, @@ -288,7 +294,7 @@ export const PostPage = ({ }, }} /> - {shouldShowAuthBanner && isLaptop && } + {showAuthBanner && isLaptop && }