From dbd903a90fd79ac50b9ff743a4ee1699f156e791 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 20 Jul 2026 10:16:31 +0300 Subject: [PATCH 01/19] feat: add post-signup feed activation --- .../shared/src/components/LoginButton.tsx | 5 +- .../components/auth/AuthenticationBanner.tsx | 5 +- .../src/components/auth/CustomAuthBanner.tsx | 2 + .../src/components/auth/PostAuthBanner.tsx | 32 ++- .../personalized/GeoPersonalizedBanner.tsx | 8 +- .../personalized/SocialPersonalizedBanner.tsx | 8 +- .../personalized/UserPersonalizedBanner.tsx | 15 +- .../post/PostOnboardingActivation.spec.tsx | 96 +++++++++ .../post/PostOnboardingActivation.tsx | 204 ++++++++++++++++++ packages/shared/src/lib/log.ts | 1 + .../src/lib/postSignupActivation.spec.ts | 43 ++++ .../shared/src/lib/postSignupActivation.ts | 81 +++++++ packages/webapp/pages/posts/[id]/index.tsx | 2 + 13 files changed, 492 insertions(+), 10 deletions(-) create mode 100644 packages/shared/src/components/post/PostOnboardingActivation.spec.tsx create mode 100644 packages/shared/src/components/post/PostOnboardingActivation.tsx create mode 100644 packages/shared/src/lib/postSignupActivation.spec.ts create mode 100644 packages/shared/src/lib/postSignupActivation.ts diff --git a/packages/shared/src/components/LoginButton.tsx b/packages/shared/src/components/LoginButton.tsx index 4d29121e4a9..da4daff2cf0 100644 --- a/packages/shared/src/components/LoginButton.tsx +++ b/packages/shared/src/components/LoginButton.tsx @@ -5,7 +5,7 @@ import { Button, ButtonVariant } from './buttons/Button'; import { useLogContext } from '../contexts/LogContext'; import type { LogEvent } from '../hooks/log/useLogQueue'; import { TargetType } from '../lib/log'; -import { useAuthContext } from '../contexts/AuthContext'; +import { type LoginState, useAuthContext } from '../contexts/AuthContext'; import { AuthTriggers } from '../lib/auth'; interface ClassName { @@ -15,6 +15,7 @@ interface ClassName { interface LoginButtonProps { className?: ClassName; + onRegistrationSuccess?: LoginState['onRegistrationSuccess']; } enum ButtonCopy { @@ -32,6 +33,7 @@ const getLogEvent = (copy: ButtonCopy): LogEvent => ({ export default function LoginButton({ className = {}, + onRegistrationSuccess, }: LoginButtonProps): ReactElement { const { logEvent } = useLogContext(); const { showLogin } = useAuthContext(); @@ -41,6 +43,7 @@ export default function LoginButton({ trigger: AuthTriggers.MainButton, options: { isLogin: copy === ButtonCopy.Login, + onRegistrationSuccess, }, }); }; diff --git a/packages/shared/src/components/auth/AuthenticationBanner.tsx b/packages/shared/src/components/auth/AuthenticationBanner.tsx index ab8b74e6c58..2178b43dbdd 100644 --- a/packages/shared/src/components/auth/AuthenticationBanner.tsx +++ b/packages/shared/src/components/auth/AuthenticationBanner.tsx @@ -5,7 +5,7 @@ import classed from '../../lib/classed'; import { OnboardingHeadline } from './OnboardingHeadline'; import AuthOptions from './AuthOptions'; import { AuthTriggers } from '../../lib/auth'; -import { useAuthContext } from '../../contexts/AuthContext'; +import { type LoginState, useAuthContext } from '../../contexts/AuthContext'; import { authGradientBg, BottomBannerContainer } from '../marketing/banners'; import { ButtonVariant } from '../buttons/common'; import { Image } from '../image/Image'; @@ -20,11 +20,13 @@ const Section = classed('div', 'flex flex-col'); interface AuthenticationBannerProps extends PropsWithChildren { compact?: boolean; + onRegistrationSuccess?: LoginState['onRegistrationSuccess']; } export function AuthenticationBanner({ children, compact, + onRegistrationSuccess, }: AuthenticationBannerProps): ReactElement { const { showLogin } = useAuthContext(); @@ -89,6 +91,7 @@ export function AuthenticationBanner({ isLogin: !!props.isLoginFlow, defaultDisplay: props.defaultDisplay, formValues: props.email ? { email: props.email } : undefined, + onRegistrationSuccess, }, }); }} diff --git a/packages/shared/src/components/auth/CustomAuthBanner.tsx b/packages/shared/src/components/auth/CustomAuthBanner.tsx index 0ab2d48cdc8..ca4894fe97a 100644 --- a/packages/shared/src/components/auth/CustomAuthBanner.tsx +++ b/packages/shared/src/components/auth/CustomAuthBanner.tsx @@ -6,6 +6,7 @@ import { useAuthContext } from '../../contexts/AuthContext'; import { useViewSize, ViewSize } from '../../hooks'; import LoginButton from '../LoginButton'; import { authGradientBg } from '../marketing/banners'; +import { markPostSignupActivation } from '../../lib/postSignupActivation'; const CustomAuthBanner = (): ReactElement | null => { const { shouldShowAuthBanner } = useOnboardingActions(); @@ -21,6 +22,7 @@ const CustomAuthBanner = (): ReactElement | null => { return ( @@ -36,17 +37,40 @@ export const PostAuthBanner = ({ const userId = searchParams?.get('userid'); if (userId) { - return ; + return ( + + ); } const social = getSocialReferrer(); if (social) { - return ; + return ( + + ); } if (geo?.region) { - return ; + return ( + + ); } - return ; + return ( + + ); }; diff --git a/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx b/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx index 5bbb9a15ce8..e3ef1e28c36 100644 --- a/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx +++ b/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx @@ -2,19 +2,25 @@ import type { ReactElement } from 'react'; import React from 'react'; import { geoToCountry, geoToEmoji } from '../../../../lib/geo'; import { AuthenticationBanner, OnboardingHeadline } from '../../../auth'; +import type { LoginState } from '../../../../contexts/AuthContext'; const GeoPersonalizedBanner = ({ geo, compact, + onRegistrationSuccess, }: { geo: string; compact?: boolean; + onRegistrationSuccess?: LoginState['onRegistrationSuccess']; }): ReactElement => { const emoji = geoToEmoji(geo); const country = geoToCountry(geo); return ( - + { const Icon = socialIcon[site]; const gradient = socialGradient[site]; return ( - + { const key = generateQueryKey(RequestKey.ReferringUser); const { data: user, isError } = useQuery({ @@ -20,13 +23,21 @@ const UserPersonalizedBanner = ({ }); if (isError) { - return ; + return ( + + ); } const name = user?.name ? user?.name.split(' ')[0] : user?.username; return ( - + {user?.image && } ({ + useRouter: () => ({ + asPath: '/posts/post-1?ref=share', + push: mockPush, + }), +})); + +jest.mock('../../contexts/AuthContext', () => ({ + useAuthContext: () => ({ user: { id: 'new-user' } }), +})); + +jest.mock('../../hooks/auth/useOnboardingActions', () => ({ + useOnboardingActions: () => ({ + isOnboardingActionsReady: true, + isOnboardingComplete: false, + }), +})); + +jest.mock('../../contexts/LogContext', () => ({ + useLogContext: () => ({ logEvent: mockLogEvent }), +})); + +jest.mock('../../lib/postSignupActivation', () => ({ + clearPostSignupActivation: jest.fn(), + hasPostSignupActivation: jest.fn(() => true), +})); + +const mockHasPostSignupActivation = jest.mocked(hasPostSignupActivation); +const mockClearPostSignupActivation = jest.mocked(clearPostSignupActivation); + +describe('PostOnboardingActivation', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockHasPostSignupActivation.mockReturnValue(true); + }); + + it('connects the current post topics to the feed value proposition', async () => { + render( + , + ); + + expect( + await screen.findByText( + 'One good post is luck. Build a feed full of them.', + ), + ).toBeInTheDocument(); + expect(screen.getByText('#react')).toBeInTheDocument(); + expect(screen.getByText('#typescript')).toBeInTheDocument(); + expect(screen.getByText('#webdev')).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole('button', { name: 'Build my feed' }), + ); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: '/onboarding', + query: { after_auth: '/posts/post-1?ref=share' }, + }); + }); + + it('dismisses the prompt without completing onboarding', async () => { + render(); + + await screen.findByRole('complementary', { + name: 'Personalize your feed', + }); + await userEvent.click( + screen.getByRole('button', { + name: 'Dismiss feed personalization', + }), + ); + + expect(mockClearPostSignupActivation).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect( + screen.queryByRole('complementary', { + name: 'Personalize your feed', + }), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/shared/src/components/post/PostOnboardingActivation.tsx b/packages/shared/src/components/post/PostOnboardingActivation.tsx new file mode 100644 index 00000000000..cea01f8789f --- /dev/null +++ b/packages/shared/src/components/post/PostOnboardingActivation.tsx @@ -0,0 +1,204 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { useRouter } from 'next/router'; +import type { Post } from '../../graphql/posts'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useOnboardingActions } from '../../hooks/auth/useOnboardingActions'; +import { useLogContext } from '../../contexts/LogContext'; +import { TargetType } from '../../lib/log'; +import { + clearPostSignupActivation, + hasPostSignupActivation, +} from '../../lib/postSignupActivation'; +import { AFTER_AUTH_PARAM } from '../auth/common'; +import { + Button, + ButtonIconPosition, + ButtonSize, + ButtonVariant, +} from '../buttons/Button'; +import CloseButton from '../CloseButton'; +import { ArrowIcon, SparkleIcon } from '../icons'; +import { IconSize } from '../Icon'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../typography/Typography'; + +interface PostOnboardingActivationProps { + post: Pick; + inline?: boolean; +} + +const getSupportingCopy = (tags: string[]): string => { + if (!tags.length) { + return 'Pick what you care about. We’ll bring you the tech worth knowing — minus the noise.'; + } + + const topics = tags.slice(0, 2).join(' and '); + return `Start with ${topics}, then pick what else you care about. We’ll do the filtering.`; +}; + +export const PostOnboardingActivation = ({ + post, + inline = false, +}: PostOnboardingActivationProps): ReactElement | null => { + const router = useRouter(); + const { user } = useAuthContext(); + const { logEvent } = useLogContext(); + const { isOnboardingActionsReady, isOnboardingComplete } = + useOnboardingActions(); + const [isVisible, setIsVisible] = useState(false); + const hasLoggedImpression = useRef(false); + const tags = useMemo(() => post.tags?.slice(0, 3) ?? [], [post.tags]); + + useEffect(() => { + if (!user?.id || !isOnboardingActionsReady) { + return; + } + + if (isOnboardingComplete) { + clearPostSignupActivation(); + return; + } + + setIsVisible(hasPostSignupActivation(user.id)); + }, [isOnboardingActionsReady, isOnboardingComplete, user?.id]); + + const shouldShow = + !!user?.id && + isOnboardingActionsReady && + !isOnboardingComplete && + isVisible; + + useEffect(() => { + if (!shouldShow || hasLoggedImpression.current) { + return; + } + + hasLoggedImpression.current = true; + logEvent({ + event_name: 'impression', + target_type: TargetType.PostSignupActivation, + target_id: 'post signup activation', + }); + }, [logEvent, shouldShow]); + + if (!shouldShow) { + return null; + } + + const onDismiss = (): void => { + clearPostSignupActivation(); + setIsVisible(false); + logEvent({ + event_name: 'click', + target_type: TargetType.PostSignupActivation, + target_id: 'dismiss post signup activation', + }); + }; + + const onBuildFeed = (): void => { + logEvent({ + event_name: 'click', + target_type: TargetType.PostSignupActivation, + target_id: 'build feed from post signup activation', + extra: JSON.stringify({ tags }), + }); + + router.push({ + pathname: '/onboarding', + query: { [AFTER_AUTH_PARAM]: router.asPath }, + }); + }; + + return ( + + ); +}; diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 93c6b2c9e80..85d318d2773 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -532,6 +532,7 @@ export enum TargetType { SpotlightCommand = 'spotlight command', MyFeedModal = 'my feed modal', ArticleAnonymousCTA = 'article anonymous cta', + PostSignupActivation = 'post signup activation', EnableNotifications = 'enable notifications', OnboardingChecklist = 'onboarding checklist', LoginButton = 'login button', diff --git a/packages/shared/src/lib/postSignupActivation.spec.ts b/packages/shared/src/lib/postSignupActivation.spec.ts new file mode 100644 index 00000000000..6320ca701dd --- /dev/null +++ b/packages/shared/src/lib/postSignupActivation.spec.ts @@ -0,0 +1,43 @@ +import { + clearPostSignupActivation, + hasPostSignupActivation, + markPostSignupActivation, +} from './postSignupActivation'; + +describe('post signup activation', () => { + beforeEach(() => { + clearPostSignupActivation(); + window.history.replaceState({}, '', '/posts/original-post'); + }); + + it('marks the newly registered user on the originating post', () => { + markPostSignupActivation({ id: 'new-user' }); + + expect(hasPostSignupActivation('new-user')).toBe(true); + expect(hasPostSignupActivation('another-user')).toBe(false); + }); + + it('does not carry the activation prompt to another post', () => { + markPostSignupActivation({ id: 'new-user' }); + + window.history.replaceState({}, '', '/posts/another-post'); + + expect(hasPostSignupActivation('new-user')).toBe(false); + }); + + it('does not mark registrations outside a post page', () => { + window.history.replaceState({}, '', '/sources/javascript'); + + markPostSignupActivation({ id: 'new-user' }); + + expect(hasPostSignupActivation('new-user')).toBe(false); + }); + + it('clears the activation prompt', () => { + markPostSignupActivation({ id: 'new-user' }); + + clearPostSignupActivation(); + + expect(hasPostSignupActivation('new-user')).toBe(false); + }); +}); diff --git a/packages/shared/src/lib/postSignupActivation.ts b/packages/shared/src/lib/postSignupActivation.ts new file mode 100644 index 00000000000..6f778102296 --- /dev/null +++ b/packages/shared/src/lib/postSignupActivation.ts @@ -0,0 +1,81 @@ +import { generateStorageKey, StorageTopic } from './storage'; +import { storageWrapper as storage } from './storageWrapper'; + +const activationStorageKey = generateStorageKey( + StorageTopic.Onboarding, + 'postSignupActivation', +); + +const activationTtl = 7 * 24 * 60 * 60 * 1000; + +interface PostSignupActivation { + userId: string; + postPath: string; + createdAt: number; +} + +interface ActivationUser { + id?: string; +} + +const getCurrentPostPath = (): string | null => { + if (typeof window === 'undefined') { + return null; + } + + const { pathname } = window.location; + return pathname.startsWith('/posts/') ? pathname : null; +}; + +const readPostSignupActivation = (): PostSignupActivation | null => { + const value = storage.getItem(activationStorageKey); + if (!value) { + return null; + } + + try { + return JSON.parse(value) as PostSignupActivation; + } catch { + storage.removeItem(activationStorageKey); + return null; + } +}; + +export const markPostSignupActivation = (user?: ActivationUser): void => { + const postPath = getCurrentPostPath(); + if (!user?.id || !postPath) { + return; + } + + storage.setItem( + activationStorageKey, + JSON.stringify({ + userId: user.id, + postPath, + createdAt: Date.now(), + } satisfies PostSignupActivation), + ); +}; + +export const hasPostSignupActivation = (userId?: string): boolean => { + const postPath = getCurrentPostPath(); + if (!userId || !postPath) { + return false; + } + + const activation = readPostSignupActivation(); + if (!activation) { + return false; + } + + if (Date.now() - activation.createdAt > activationTtl) { + storage.removeItem(activationStorageKey); + return false; + } + + return activation.userId === userId && activation.postPath === postPath; +}; + +export const clearPostSignupActivation = (): void => { + storage.removeItem(activationStorageKey); +}; diff --git a/packages/webapp/pages/posts/[id]/index.tsx b/packages/webapp/pages/posts/[id]/index.tsx index 8fb970f07ea..8bd69fa0a3c 100644 --- a/packages/webapp/pages/posts/[id]/index.tsx +++ b/packages/webapp/pages/posts/[id]/index.tsx @@ -52,6 +52,7 @@ import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditio import { isPostRedesignEligible } from '@dailydotdev/shared/src/hooks/post/usePostRedesign'; import { featurePostRedesign } from '@dailydotdev/shared/src/lib/featureManagement'; import { PostFocusCard } from '@dailydotdev/shared/src/components/post/focus/PostFocusCard'; +import { PostOnboardingActivation } from '@dailydotdev/shared/src/components/post/PostOnboardingActivation'; import { getPageSeoTitles } from '../../../components/layouts/utils'; import { getLayout } from '../../../components/layouts/MainLayout'; import FooterNavBarLayout from '../../../components/layouts/FooterNavBarLayout'; @@ -312,6 +313,7 @@ export const PostPage = ({ {!showRedesign && shouldShowAuthBanner && isLaptop && ( )} + From c43ad5d83166179ead013fa98728ca23f6eed281 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 20 Jul 2026 10:30:02 +0300 Subject: [PATCH 02/19] chore: preview post activation banner locally --- .../post/PostOnboardingActivation.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/post/PostOnboardingActivation.tsx b/packages/shared/src/components/post/PostOnboardingActivation.tsx index cea01f8789f..78d097032c8 100644 --- a/packages/shared/src/components/post/PostOnboardingActivation.tsx +++ b/packages/shared/src/components/post/PostOnboardingActivation.tsx @@ -7,6 +7,7 @@ import { useAuthContext } from '../../contexts/AuthContext'; import { useOnboardingActions } from '../../hooks/auth/useOnboardingActions'; import { useLogContext } from '../../contexts/LogContext'; import { TargetType } from '../../lib/log'; +import { isDevelopment } from '../../lib/constants'; import { clearPostSignupActivation, hasPostSignupActivation, @@ -52,6 +53,7 @@ export const PostOnboardingActivation = ({ const { isOnboardingActionsReady, isOnboardingComplete } = useOnboardingActions(); const [isVisible, setIsVisible] = useState(false); + const [isLocalPreviewDismissed, setIsLocalPreviewDismissed] = useState(false); const hasLoggedImpression = useRef(false); const tags = useMemo(() => post.tags?.slice(0, 3) ?? [], [post.tags]); @@ -69,10 +71,11 @@ export const PostOnboardingActivation = ({ }, [isOnboardingActionsReady, isOnboardingComplete, user?.id]); const shouldShow = - !!user?.id && - isOnboardingActionsReady && - !isOnboardingComplete && - isVisible; + (isDevelopment && !isLocalPreviewDismissed) || + (!!user?.id && + isOnboardingActionsReady && + !isOnboardingComplete && + isVisible); useEffect(() => { if (!shouldShow || hasLoggedImpression.current) { @@ -92,7 +95,11 @@ export const PostOnboardingActivation = ({ } const onDismiss = (): void => { - clearPostSignupActivation(); + if (isDevelopment) { + setIsLocalPreviewDismissed(true); + } else { + clearPostSignupActivation(); + } setIsVisible(false); logEvent({ event_name: 'click', From c8a1c5b16aafed2ec7a56cecb02237f2d9cef2ef Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 20 Jul 2026 10:36:27 +0300 Subject: [PATCH 03/19] chore: add hosted activation banner preview --- .../components/post/PostOnboardingActivation.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/post/PostOnboardingActivation.tsx b/packages/shared/src/components/post/PostOnboardingActivation.tsx index 78d097032c8..53cc8e7a6a6 100644 --- a/packages/shared/src/components/post/PostOnboardingActivation.tsx +++ b/packages/shared/src/components/post/PostOnboardingActivation.tsx @@ -34,6 +34,13 @@ interface PostOnboardingActivationProps { inline?: boolean; } +const previewQueryKey = 'postOnboardingPreview'; + +const isPreviewQueryEnabled = (value?: string | string[]): boolean => + value === '1' || + value === 'true' || + (Array.isArray(value) && (value.includes('1') || value.includes('true'))); + const getSupportingCopy = (tags: string[]): string => { if (!tags.length) { return 'Pick what you care about. We’ll bring you the tech worth knowing — minus the noise.'; @@ -56,6 +63,8 @@ export const PostOnboardingActivation = ({ const [isLocalPreviewDismissed, setIsLocalPreviewDismissed] = useState(false); const hasLoggedImpression = useRef(false); const tags = useMemo(() => post.tags?.slice(0, 3) ?? [], [post.tags]); + const isPreviewMode = + isDevelopment || isPreviewQueryEnabled(router.query?.[previewQueryKey]); useEffect(() => { if (!user?.id || !isOnboardingActionsReady) { @@ -71,7 +80,7 @@ export const PostOnboardingActivation = ({ }, [isOnboardingActionsReady, isOnboardingComplete, user?.id]); const shouldShow = - (isDevelopment && !isLocalPreviewDismissed) || + (isPreviewMode && !isLocalPreviewDismissed) || (!!user?.id && isOnboardingActionsReady && !isOnboardingComplete && @@ -95,7 +104,7 @@ export const PostOnboardingActivation = ({ } const onDismiss = (): void => { - if (isDevelopment) { + if (isPreviewMode) { setIsLocalPreviewDismissed(true); } else { clearPostSignupActivation(); From 23cb0cc52416538402b21f5f1e787644e776748a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 20 Jul 2026 10:56:14 +0300 Subject: [PATCH 04/19] feat: redesign post onboarding activation --- .../src/components/auth/CustomAuthBanner.tsx | 20 ++- .../post/PostOnboardingActivation.spec.tsx | 23 +-- .../post/PostOnboardingActivation.tsx | 161 ++++++------------ .../src/lib/postSignupActivation.spec.ts | 8 + .../shared/src/lib/postSignupActivation.ts | 9 + packages/webapp/pages/posts/[id]/index.tsx | 19 ++- 6 files changed, 113 insertions(+), 127 deletions(-) diff --git a/packages/shared/src/components/auth/CustomAuthBanner.tsx b/packages/shared/src/components/auth/CustomAuthBanner.tsx index ca4894fe97a..41ae2e1feb2 100644 --- a/packages/shared/src/components/auth/CustomAuthBanner.tsx +++ b/packages/shared/src/components/auth/CustomAuthBanner.tsx @@ -1,20 +1,36 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; +import { useRouter } from 'next/router'; import { useOnboardingActions } from '../../hooks/auth'; import { useAuthContext } from '../../contexts/AuthContext'; import { useViewSize, ViewSize } from '../../hooks'; import LoginButton from '../LoginButton'; import { authGradientBg } from '../marketing/banners'; -import { markPostSignupActivation } from '../../lib/postSignupActivation'; +import { + isPostOnboardingPreviewEnabled, + markPostSignupActivation, + POST_ONBOARDING_PREVIEW_QUERY, +} from '../../lib/postSignupActivation'; +import { isDevelopment } from '../../lib/constants'; const CustomAuthBanner = (): ReactElement | null => { const { shouldShowAuthBanner } = useOnboardingActions(); const { shouldShowLogin } = useAuthContext(); + const router = useRouter(); const isLaptop = useViewSize(ViewSize.Laptop); const isTablet = useViewSize(ViewSize.Tablet); + const isActivationPreview = + router.pathname === '/posts/[id]' && + (isDevelopment || + isPostOnboardingPreviewEnabled( + router.query?.[POST_ONBOARDING_PREVIEW_QUERY], + )); const isValid = - shouldShowAuthBanner && !isLaptop && (isTablet || !shouldShowLogin); + !isActivationPreview && + shouldShowAuthBanner && + !isLaptop && + (isTablet || !shouldShowLogin); if (!isValid) { return null; diff --git a/packages/shared/src/components/post/PostOnboardingActivation.spec.tsx b/packages/shared/src/components/post/PostOnboardingActivation.spec.tsx index 48c4ab0c4ef..5e70fef832f 100644 --- a/packages/shared/src/components/post/PostOnboardingActivation.spec.tsx +++ b/packages/shared/src/components/post/PostOnboardingActivation.spec.tsx @@ -35,6 +35,8 @@ jest.mock('../../contexts/LogContext', () => ({ jest.mock('../../lib/postSignupActivation', () => ({ clearPostSignupActivation: jest.fn(), hasPostSignupActivation: jest.fn(() => true), + isPostOnboardingPreviewEnabled: jest.fn(() => false), + POST_ONBOARDING_PREVIEW_QUERY: 'postOnboardingPreview', })); const mockHasPostSignupActivation = jest.mocked(hasPostSignupActivation); @@ -46,24 +48,17 @@ describe('PostOnboardingActivation', () => { mockHasPostSignupActivation.mockReturnValue(true); }); - it('connects the current post topics to the feed value proposition', async () => { - render( - , - ); + it('frames feed setup as the final step with a clear value proposition', async () => { + render(); expect( - await screen.findByText( - 'One good post is luck. Build a feed full of them.', - ), + await screen.findByText("Your account is ready. Your feed isn't."), ).toBeInTheDocument(); - expect(screen.getByText('#react')).toBeInTheDocument(); - expect(screen.getByText('#typescript')).toBeInTheDocument(); - expect(screen.getByText('#webdev')).toBeInTheDocument(); + expect(screen.getByText('Account created')).toBeInTheDocument(); + expect(screen.getByText('One step left')).toBeInTheDocument(); await userEvent.click( - screen.getByRole('button', { name: 'Build my feed' }), + screen.getByRole('button', { name: 'Choose my topics' }), ); expect(mockPush).toHaveBeenCalledWith({ @@ -73,7 +68,7 @@ describe('PostOnboardingActivation', () => { }); it('dismisses the prompt without completing onboarding', async () => { - render(); + render(); await screen.findByRole('complementary', { name: 'Personalize your feed', diff --git a/packages/shared/src/components/post/PostOnboardingActivation.tsx b/packages/shared/src/components/post/PostOnboardingActivation.tsx index 53cc8e7a6a6..3e6ee640d0f 100644 --- a/packages/shared/src/components/post/PostOnboardingActivation.tsx +++ b/packages/shared/src/components/post/PostOnboardingActivation.tsx @@ -1,8 +1,6 @@ import type { ReactElement } from 'react'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import classNames from 'classnames'; +import React, { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/router'; -import type { Post } from '../../graphql/posts'; import { useAuthContext } from '../../contexts/AuthContext'; import { useOnboardingActions } from '../../hooks/auth/useOnboardingActions'; import { useLogContext } from '../../contexts/LogContext'; @@ -11,16 +9,13 @@ import { isDevelopment } from '../../lib/constants'; import { clearPostSignupActivation, hasPostSignupActivation, + isPostOnboardingPreviewEnabled, + POST_ONBOARDING_PREVIEW_QUERY, } from '../../lib/postSignupActivation'; import { AFTER_AUTH_PARAM } from '../auth/common'; -import { - Button, - ButtonIconPosition, - ButtonSize, - ButtonVariant, -} from '../buttons/Button'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import CloseButton from '../CloseButton'; -import { ArrowIcon, SparkleIcon } from '../icons'; +import { VIcon } from '../icons/V'; import { IconSize } from '../Icon'; import { Typography, @@ -29,31 +24,7 @@ import { TypographyType, } from '../typography/Typography'; -interface PostOnboardingActivationProps { - post: Pick; - inline?: boolean; -} - -const previewQueryKey = 'postOnboardingPreview'; - -const isPreviewQueryEnabled = (value?: string | string[]): boolean => - value === '1' || - value === 'true' || - (Array.isArray(value) && (value.includes('1') || value.includes('true'))); - -const getSupportingCopy = (tags: string[]): string => { - if (!tags.length) { - return 'Pick what you care about. We’ll bring you the tech worth knowing — minus the noise.'; - } - - const topics = tags.slice(0, 2).join(' and '); - return `Start with ${topics}, then pick what else you care about. We’ll do the filtering.`; -}; - -export const PostOnboardingActivation = ({ - post, - inline = false, -}: PostOnboardingActivationProps): ReactElement | null => { +export const PostOnboardingActivation = (): ReactElement | null => { const router = useRouter(); const { user } = useAuthContext(); const { logEvent } = useLogContext(); @@ -62,9 +33,11 @@ export const PostOnboardingActivation = ({ const [isVisible, setIsVisible] = useState(false); const [isLocalPreviewDismissed, setIsLocalPreviewDismissed] = useState(false); const hasLoggedImpression = useRef(false); - const tags = useMemo(() => post.tags?.slice(0, 3) ?? [], [post.tags]); const isPreviewMode = - isDevelopment || isPreviewQueryEnabled(router.query?.[previewQueryKey]); + isDevelopment || + isPostOnboardingPreviewEnabled( + router.query?.[POST_ONBOARDING_PREVIEW_QUERY], + ); useEffect(() => { if (!user?.id || !isOnboardingActionsReady) { @@ -122,7 +95,6 @@ export const PostOnboardingActivation = ({ event_name: 'click', target_type: TargetType.PostSignupActivation, target_id: 'build feed from post signup activation', - extra: JSON.stringify({ tags }), }); router.push({ @@ -134,78 +106,55 @@ export const PostOnboardingActivation = ({ return (