From 3fd8b9d7c49b05942c9c5871882b90ecb98e28e6 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Thu, 4 Jun 2026 16:41:07 +0200 Subject: [PATCH 1/3] feat: new login banners --- .../src/newtab/HijackingLoginStrip.spec.tsx | 415 ++++++++++++++-- .../src/newtab/HijackingLoginStrip.tsx | 443 +++++++++++++++++- packages/shared/src/lib/featureManagement.ts | 10 + packages/shared/src/styles/base.css | 51 ++ 4 files changed, 861 insertions(+), 58 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index 087c8a528b7..a545bc6a827 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -1,8 +1,16 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AuthContextData } from '@dailydotdev/shared/src/contexts/AuthContext'; import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext'; +import { useConditionalFeature } from '@dailydotdev/shared/src/hooks'; +import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; +import { + AuthDisplay, + SocialProvider, +} from '@dailydotdev/shared/src/components/auth/common'; +import { HijackingVariant } from '@dailydotdev/shared/src/lib/featureManagement'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; @@ -14,10 +22,62 @@ jest.mock('@dailydotdev/shared/src/contexts/AuthContext', () => ({ useAuthContext: jest.fn(), })); +jest.mock('@dailydotdev/shared/src/hooks', () => ({ + ...jest.requireActual('@dailydotdev/shared/src/hooks'), + useConditionalFeature: jest.fn(), +})); + +jest.mock('@dailydotdev/shared/src/hooks/auth/useSignBack', () => ({ + useSignBack: jest.fn(), +})); + +jest.mock('@dailydotdev/shared/src/components/auth/AuthOptions', () => { + const { AuthDisplay: MockAuthDisplay } = jest.requireActual( + '@dailydotdev/shared/src/components/auth/common', + ); + + return { + __esModule: true, + default: ({ + onAuthStateUpdate, + }: { + onAuthStateUpdate?: (props: { defaultDisplay?: string }) => void; + }) => ( +
+ + + + +

By continuing, you agree to the Terms of Service

+
+ ), + }; +}); + const LogContext = getLogContextStatic(); const mockUseAuthContext = useAuthContext as jest.MockedFunction< typeof useAuthContext >; +const mockUseSignBack = useSignBack as jest.MockedFunction; +const mockUseConditionalFeature = useConditionalFeature as jest.MockedFunction< + typeof useConditionalFeature +>; const logEvent = jest.fn(); const showLogin = jest.fn(); @@ -53,6 +113,16 @@ const defaultAuthContext = { isFunnel: false, } satisfies AuthContextData; +const setVariant = ( + variant: HijackingVariant, + { isLoading = false }: { isLoading?: boolean } = {}, +): void => { + mockUseConditionalFeature.mockReturnValue({ + value: variant, + isLoading, + }); +}; + const renderComponent = ( authContext: Partial = {}, ): ReturnType => { @@ -61,71 +131,320 @@ const renderComponent = ( ...authContext, }); - return render( - - - , + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + const Wrapper = ({ + children, + }: { + children: React.ReactNode; + }): React.ReactElement => ( + + + {children} + + ); + + return render(, { wrapper: Wrapper }); }; beforeEach(() => { jest.clearAllMocks(); + setVariant(HijackingVariant.Default); + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: undefined, + provider: undefined, + onUpdateSignBack: jest.fn(), + }); }); describe('HijackingLoginStrip', () => { - it('shows a login CTA for logged out users', () => { - renderComponent(); - - expect( - screen.getByText('Unlock the full daily.dev experience'), - ).toBeVisible(); - expect( - screen.getByText('Log in to pick up where you left off.'), - ).toBeVisible(); - - const cta = screen.getByRole('button', { name: 'Log in to continue' }); - fireEvent.click(cta); - - expect(logEvent).toHaveBeenCalledWith({ - event_name: LogEvent.Click, - target_type: TargetType.LoginButton, - target_id: 'hijacking', + it('renders nothing while the experiment is loading', () => { + setVariant(HijackingVariant.Default, { isLoading: true }); + + const { container } = renderComponent(); + + expect(container).toBeEmptyDOMElement(); + }); + + describe('default variant', () => { + it('shows the original banner with a log in CTA for logged out users', () => { + renderComponent(); + + expect( + screen.getByRole('heading', { + name: 'Unlock the full daily.dev experience', + }), + ).toBeVisible(); + expect( + screen.getByText('Log in to pick up where you left off.'), + ).toBeVisible(); + + fireEvent.click( + screen.getByRole('button', { name: 'Log in to continue' }), + ); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.LoginButton, + target_id: 'hijacking', + }); + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: true }, + }); + }); + + it('shows an onboarding CTA for logged in users who still need onboarding', () => { + renderComponent({ user: loggedUser, isLoggedIn: true }); + + expect( + screen.getByText(/You still have a few onboarding steps left/), + ).toBeVisible(); + + const cta = screen.getByRole('link', { name: 'Continue onboarding' }); + const expectedUrl = new URL(onboardingUrl); + expectedUrl.searchParams.append('r', 'extension'); + + expect(cta).toHaveAttribute('href', expectedUrl.toString()); + + fireEvent.click(cta); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.LoginButton, + target_id: 'hijacking', + }); + expect(showLogin).not.toHaveBeenCalled(); + }); + }); + + describe('cta variant', () => { + beforeEach(() => { + setVariant(HijackingVariant.CTA); + }); + + it('shows the cat stage hero with sign up and log in CTAs', () => { + renderComponent(); + + expect( + screen.getByRole('heading', { + name: 'Own your new tab. Make it your dev briefing.', + }), + ).toBeVisible(); + + fireEvent.click(screen.getByRole('button', { name: 'Sign up' })); + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + + fireEvent.click(screen.getByRole('button', { name: 'Log in' })); + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: true }, + }); }); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: true }, + + it('logs a signup impression for new visitors', () => { + renderComponent(); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); }); }); - it('shows an onboarding CTA for logged in users who still need onboarding', () => { - renderComponent({ user: loggedUser, isLoggedIn: true }); + describe('auth variant', () => { + beforeEach(() => { + setVariant(HijackingVariant.Auth); + }); + + it('encourages signup for logged out users without a remembered account', () => { + renderComponent(); + + expect( + screen.getByRole('heading', { + name: 'Where developers make every tab count.', + }), + ).toBeVisible(); + + expect( + screen.getByRole('button', { name: 'Continue with Google' }), + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Continue with GitHub' }), + ).toBeVisible(); + expect( + screen.getByRole('button', { name: 'Continue with email' }), + ).toBeVisible(); + expect( + screen.getByText(/By continuing, you agree to the Terms of Service/), + ).toBeVisible(); + + fireEvent.click( + screen.getByRole('button', { name: 'Continue with email' }), + ); + + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { + isLogin: false, + defaultDisplay: AuthDisplay.Registration, + formValues: undefined, + }, + }); + }); + + it('logs a signup impression for new visitors', () => { + renderComponent(); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + }); + + it('waits for remembered account storage before logging the impression', () => { + let signBackState: ReturnType = { + isLoaded: false, + signBack: undefined, + provider: undefined, + onUpdateSignBack: jest.fn(), + }; + + mockUseSignBack.mockImplementation(() => signBackState); + + const { rerender } = renderComponent(); + + expect(logEvent).not.toHaveBeenCalled(); + + signBackState = { + isLoaded: true, + signBack: { + name: 'Tsahi Matsliah', + email: 'tsahi@daily.dev', + image: 'https://daily.dev/tsahi.png', + }, + provider: SocialProvider.Google, + onUpdateSignBack: jest.fn(), + }; + + rerender(); + + expect( + screen.getByRole('heading', { name: /Welcome back, Tsahi/ }), + ).toBeVisible(); + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.LoginButton, + target_id: 'hijacking', + }); + expect(logEvent).not.toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + }); + + it('offers a welcome-back "Continue as" for users with a remembered account', () => { + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: { + name: 'Tsahi Matsliah', + email: 'tsahi@daily.dev', + image: 'https://daily.dev/tsahi.png', + }, + provider: SocialProvider.Google, + onUpdateSignBack: jest.fn(), + }); + + renderComponent(); + + expect( + screen.getByRole('heading', { name: /Welcome back, Tsahi/ }), + ).toBeVisible(); + expect(screen.getByText('tsahi@daily.dev')).toBeVisible(); + + const cta = screen.getByRole('button', { name: /Continue as Tsahi/ }); + fireEvent.click(cta); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_type: TargetType.LoginButton, + target_id: 'hijacking', + }); + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: true }, + }); + }); + + it('lets remembered users create a different account', () => { + mockUseSignBack.mockReturnValue({ + isLoaded: true, + signBack: { + name: 'Tsahi Matsliah', + email: 'tsahi@daily.dev', + image: 'https://daily.dev/tsahi.png', + }, + provider: SocialProvider.Google, + onUpdateSignBack: jest.fn(), + }); + + renderComponent(); + + fireEvent.click( + screen.getByRole('button', { name: 'Create an account' }), + ); + + expect(showLogin).toHaveBeenCalledWith({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + }); + + it('shows an onboarding CTA for logged in users who still need onboarding', () => { + renderComponent({ user: loggedUser, isLoggedIn: true }); - expect( - screen.getByText( - 'You still have a few onboarding steps left. Finish them to unlock the full experience.', - ), - ).toBeVisible(); + expect( + screen.getByText( + 'Finish onboarding to unlock the full daily.dev experience.', + ), + ).toBeVisible(); - const cta = screen.getByRole('link', { name: 'Continue onboarding' }); - const expectedUrl = new URL(onboardingUrl); - expectedUrl.searchParams.append('r', 'extension'); + const cta = screen.getByRole('link', { name: /Continue/ }); + const expectedUrl = new URL(onboardingUrl); + expectedUrl.searchParams.append('r', 'extension'); - expect(cta).toHaveAttribute('href', expectedUrl.toString()); + expect(cta).toHaveAttribute('href', expectedUrl.toString()); - fireEvent.click(cta); + fireEvent.click(cta); - expect(logEvent).toHaveBeenCalledWith({ - event_name: LogEvent.Click, - target_type: TargetType.LoginButton, - target_id: 'hijacking', + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.LoginButton, + target_id: 'hijacking', + }); + expect(showLogin).not.toHaveBeenCalled(); }); - expect(showLogin).not.toHaveBeenCalled(); }); }); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 7185dbc58ac..24e8b57b589 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -1,28 +1,237 @@ -import type { ReactElement } from 'react'; -import React from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { Button, + ButtonSize, ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; +import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText'; +import AuthOptions from '@dailydotdev/shared/src/components/auth/AuthOptions'; +import { + ProfileImageSize, + ProfilePicture, +} from '@dailydotdev/shared/src/components/ProfilePicture'; +import { + AuthDisplay, + type AuthOptionsProps, + providerMap, + type SocialProvider, +} from '@dailydotdev/shared/src/components/auth/common'; +import { onboardingGradientClasses } from '@dailydotdev/shared/src/components/onboarding/common'; import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; +import { useConditionalFeature } from '@dailydotdev/shared/src/hooks'; +import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; +import { + cloudinaryOnboardingFullBackgroundDesktop, + cloudinaryOnboardingFullBackgroundMobile, + cloudinaryReadingReminderCat, +} from '@dailydotdev/shared/src/lib/image'; +import { + featureHijackingVariants, + HijackingVariant, +} from '@dailydotdev/shared/src/lib/featureManagement'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; import feedStyles from '@dailydotdev/shared/src/components/Feed.module.css'; -import { cloudinaryReadingReminderCat } from '@dailydotdev/shared/src/lib/image'; +import LogoIcon from '@dailydotdev/shared/src/svg/LogoIcon'; +import LogoText from '@dailydotdev/shared/src/svg/LogoText'; + +type CoverVariant = 'continue' | 'signin' | 'onboarding'; + +const primaryCta = + 'transition-transform duration-200 ease-out hover:-translate-y-0.5'; + +const glassCta = + '!border-white/20 !bg-white/[0.06] !text-white backdrop-blur-sm transition-colors duration-200 hover:!bg-white/[0.12]'; + +const onboardingHref = (() => { + const base = new URL(onboardingUrl); + base.searchParams.append('r', 'extension'); + + return base.toString(); +})(); + +function BrandLockup(): ReactElement { + return ( + + + + + ); +} + +function CatHeroImage(): ReactElement { + return ( +
+ Sleeping cat on laptop +
+ ); +} + +interface SigninHeroProps { + onSignupClick: () => void; + onLoginClick: () => void; + formRef: AuthOptionsProps['formRef']; + onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate']; +} + +type HeroActionButtonsProps = Pick< + SigninHeroProps, + 'onSignupClick' | 'onLoginClick' +>; + +function HeroActionButtons({ + onSignupClick, + onLoginClick, +}: HeroActionButtonsProps): ReactElement { + return ( +
+ + +
+ ); +} + +function CatStageHero({ + onSignupClick, + onLoginClick, +}: SigninHeroProps): ReactElement { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ +

+ Own your new tab. Make it your dev briefing. +

+
+

+ Sign in and daily.dev will remember the topics, saves, upvotes, + and discussions that matter to you. +

+ +
+
+ +
+
+
+
+ ); +} -export default function HijackingLoginStrip(): ReactElement { +function OnboardingSignupHero({ + formRef, + onAuthStateUpdate, +}: SigninHeroProps): ReactElement { + return ( +
+
+ + + + + +
+
+
+
+
+
+
+

+ Where developers make every tab count. +

+

+ Sign in to turn daily.dev into your personalized feed, reputation, + saves, and community in every new tab. +

+
+
+ +
+
+
+
+ ); +} + +const SigninHeroMap = { + [HijackingVariant.CTA]: CatStageHero, + [HijackingVariant.Auth]: OnboardingSignupHero, +} satisfies Record< + HijackingVariant.CTA | HijackingVariant.Auth, + (props: SigninHeroProps) => ReactElement +>; + +function DefaultHijackingStrip(): ReactElement { const { showLogin, user } = useAuthContext(); const { logEvent } = useLogContext(); const isLoggedOut = !user; - const onboardingHref = (() => { - const base = new URL(onboardingUrl); - base.searchParams.append('r', 'extension'); - - return base.toString(); - })(); const logHijackingClick = (): void => { logEvent({ @@ -93,3 +302,217 @@ export default function HijackingLoginStrip(): ReactElement { ); } + +function HijackingHeroStrip({ + variant: experimentVariant, +}: { + variant: HijackingVariant.CTA | HijackingVariant.Auth; +}): ReactElement { + const { showLogin, user } = useAuthContext(); + const { logEvent } = useLogContext(); + const { signBack, provider, isLoaded: isSignBackLoaded } = useSignBack(); + const hasLoggedImpression = useRef(false); + const authFormRef = useRef( + null, + ) as unknown as AuthOptionsProps['formRef']; + + const isLoggedOut = !user; + const hasContinueAs = isLoggedOut && isSignBackLoaded && !!signBack?.name; + const firstName = signBack?.name?.split(' ')[0] ?? signBack?.name; + const socialProvider = + provider && provider !== 'password' + ? (provider as SocialProvider) + : undefined; + const providerIcon = socialProvider + ? providerMap[socialProvider]?.icon + : undefined; + + const variant: CoverVariant = (() => { + if (!isLoggedOut) { + return 'onboarding'; + } + + return hasContinueAs ? 'continue' : 'signin'; + })(); + const isReadyToLogImpression = !isLoggedOut || isSignBackLoaded; + + const logClick = (targetType: TargetType): void => { + logEvent({ + event_name: LogEvent.Click, + target_type: targetType, + target_id: 'hijacking', + }); + }; + + useEffect(() => { + if (!isReadyToLogImpression) { + return; + } + + if (hasLoggedImpression.current) { + return; + } + hasLoggedImpression.current = true; + + logEvent({ + event_name: LogEvent.Impression, + target_type: + variant === 'signin' ? TargetType.SignupButton : TargetType.LoginButton, + target_id: 'hijacking', + }); + }, [isReadyToLogImpression, variant, logEvent]); + + const onSignupClick = (): void => { + logClick(TargetType.SignupButton); + showLogin({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: false }, + }); + }; + + const onLoginClick = (): void => { + logClick(TargetType.LoginButton); + showLogin({ + trigger: AuthTriggers.Onboarding, + options: { isLogin: true }, + }); + }; + const onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate'] = (props) => { + showLogin({ + trigger: AuthTriggers.Onboarding, + options: { + isLogin: !!props.isLoginFlow, + defaultDisplay: props.defaultDisplay, + formValues: props.email ? { email: props.email } : undefined, + }, + }); + }; + const SigninHero = SigninHeroMap[experimentVariant]; + + const chrome = (children: ReactNode): ReactElement => ( +
+
+
+
{children}
+
+
+ ); + + if (variant === 'onboarding') { + return chrome( +
+ +

+ Let's jump back in! +

+

+ Finish onboarding to unlock the full daily.dev experience. +

+ +
, + ); + } + + if (variant === 'continue' && signBack) { + return chrome( +
+
+ + {!!providerIcon && ( + + {providerIcon} + + )} +
+

+ Welcome back, {firstName}! +

+ {!!signBack?.email && ( +

{signBack.email}

+ )} + +
+ Not you? + + Use another account + +
+
+ New here? + + Create an account + +
+
, + ); + } + + return ( + + ); +} + +export default function HijackingLoginStrip(): ReactElement | null { + const { value, isLoading } = useConditionalFeature({ + feature: featureHijackingVariants, + shouldEvaluate: true, + }); + + if (isLoading) { + return null; + } + + const hijackingVariant = value as HijackingVariant; + + if ( + hijackingVariant === HijackingVariant.CTA || + hijackingVariant === HijackingVariant.Auth + ) { + return ; + } + + return ; +} diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index e34256aeedd..c74d8dd83a4 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -210,6 +210,16 @@ export const featureFeedChips = new Feature( FeedChipsVariant.None, ); +export enum HijackingVariant { + Default = 'default', + CTA = 'cta', + Auth = 'auth', +} +export const featureHijackingVariants = new Feature( + 'hijacking_variants', + HijackingVariant.CTA, +); + export const featureLayoutV2 = new Feature('layout_v2', false); export const featureEngagementBarV2 = new Feature('engagement_bar_v2', false); diff --git a/packages/shared/src/styles/base.css b/packages/shared/src/styles/base.css index c0d54034400..37eb23d2944 100644 --- a/packages/shared/src/styles/base.css +++ b/packages/shared/src/styles/base.css @@ -1080,6 +1080,57 @@ meter::-webkit-meter-bar { background: rgb(255 0 168 / 35%); } + .top-hero-aurora { + background: + radial-gradient( + 80% 95% at 50% 122%, + color-mix(in srgb, var(--theme-accent-cabbage-default), transparent 18%) + 0%, + transparent 62% + ), + radial-gradient( + 64% 82% at 16% 128%, + color-mix(in srgb, var(--theme-accent-onion-default), transparent 32%) 0%, + transparent 58% + ), + radial-gradient( + 64% 82% at 84% 128%, + color-mix(in srgb, var(--theme-accent-onion-default), transparent 32%) 0%, + transparent 58% + ); + } + + .top-hero-stage { + background: + linear-gradient( + 118deg, + transparent 0%, + rgb(255 255 255 / 5%) 42%, + transparent 58% + ), + radial-gradient( + 78% 88% at 76% 112%, + color-mix(in srgb, var(--theme-accent-cabbage-default), transparent 6%) + 0%, + transparent 58% + ), + radial-gradient( + 70% 84% at 16% 112%, + color-mix(in srgb, var(--theme-accent-onion-default), transparent 20%) 0%, + transparent 62% + ), + radial-gradient( + 62% 72% at 92% 4%, + color-mix(in srgb, var(--theme-accent-bacon-default), transparent 54%) 0%, + transparent 58% + ), + radial-gradient( + 42% 48% at 26% 16%, + rgb(255 255 255 / 8%) 0%, + transparent 64% + ); + } + @keyframes enable-notification-bell-ring { 0%, 100% { From 26c4d91f8f5ea58d910de9cf34b84bb32efdda2a Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Fri, 5 Jun 2026 15:57:31 +0200 Subject: [PATCH 2/3] lead to login instead --- .../src/newtab/HijackingLoginStrip.spec.tsx | 151 ++++++++---------- .../src/newtab/HijackingLoginStrip.tsx | 114 +++++-------- 2 files changed, 101 insertions(+), 164 deletions(-) diff --git a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx index a545bc6a827..90aa7d28ba2 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.spec.tsx @@ -6,10 +6,7 @@ import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext'; import { useConditionalFeature } from '@dailydotdev/shared/src/hooks'; import { useSignBack } from '@dailydotdev/shared/src/hooks/auth/useSignBack'; -import { - AuthDisplay, - SocialProvider, -} from '@dailydotdev/shared/src/components/auth/common'; +import { SocialProvider } from '@dailydotdev/shared/src/components/auth/common'; import { HijackingVariant } from '@dailydotdev/shared/src/lib/featureManagement'; import { AuthTriggers } from '@dailydotdev/shared/src/lib/auth'; import { onboardingUrl } from '@dailydotdev/shared/src/lib/constants'; @@ -31,44 +28,20 @@ jest.mock('@dailydotdev/shared/src/hooks/auth/useSignBack', () => ({ useSignBack: jest.fn(), })); -jest.mock('@dailydotdev/shared/src/components/auth/AuthOptions', () => { - const { AuthDisplay: MockAuthDisplay } = jest.requireActual( - '@dailydotdev/shared/src/components/auth/common', - ); +const signupHref = (() => { + const url = new URL(onboardingUrl); + url.searchParams.append('r', 'extension'); - return { - __esModule: true, - default: ({ - onAuthStateUpdate, - }: { - onAuthStateUpdate?: (props: { defaultDisplay?: string }) => void; - }) => ( -
- - - - -

By continuing, you agree to the Terms of Service

-
- ), - }; -}); + return url.toString(); +})(); + +const loginHref = (() => { + const url = new URL(onboardingUrl); + url.searchParams.append('r', 'extension'); + url.searchParams.append('action', 'login'); + + return url.toString(); +})(); const LogContext = getLogContextStatic(); const mockUseAuthContext = useAuthContext as jest.MockedFunction< @@ -80,6 +53,7 @@ const mockUseConditionalFeature = useConditionalFeature as jest.MockedFunction< >; const logEvent = jest.fn(); const showLogin = jest.fn(); +const assignMock = jest.fn(); const defaultAuthContext = { user: undefined, @@ -157,6 +131,13 @@ const renderComponent = ( return render(, { wrapper: Wrapper }); }; +beforeAll(() => { + Object.defineProperty(window, 'location', { + configurable: true, + value: { ...window.location, assign: assignMock }, + }); +}); + beforeEach(() => { jest.clearAllMocks(); setVariant(HijackingVariant.Default); @@ -213,10 +194,8 @@ describe('HijackingLoginStrip', () => { ).toBeVisible(); const cta = screen.getByRole('link', { name: 'Continue onboarding' }); - const expectedUrl = new URL(onboardingUrl); - expectedUrl.searchParams.append('r', 'extension'); - expect(cta).toHaveAttribute('href', expectedUrl.toString()); + expect(cta).toHaveAttribute('href', signupHref); fireEvent.click(cta); @@ -234,7 +213,7 @@ describe('HijackingLoginStrip', () => { setVariant(HijackingVariant.CTA); }); - it('shows the cat stage hero with sign up and log in CTAs', () => { + it('redirects to the webapp onboarding from the cat stage hero CTAs', () => { renderComponent(); expect( @@ -249,16 +228,15 @@ describe('HijackingLoginStrip', () => { target_type: TargetType.SignupButton, target_id: 'hijacking', }); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: false }, - }); + expect(assignMock).toHaveBeenCalledWith(signupHref); fireEvent.click(screen.getByRole('button', { name: 'Log in' })); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: true }, + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.LoginButton, + target_id: 'hijacking', }); + expect(assignMock).toHaveBeenCalledWith(loginHref); }); it('logs a signup impression for new visitors', () => { @@ -277,7 +255,7 @@ describe('HijackingLoginStrip', () => { setVariant(HijackingVariant.Auth); }); - it('encourages signup for logged out users without a remembered account', () => { + it('redirects social and email sign-up options to the webapp onboarding', () => { renderComponent(); expect( @@ -286,31 +264,33 @@ describe('HijackingLoginStrip', () => { }), ).toBeVisible(); - expect( - screen.getByRole('button', { name: 'Continue with Google' }), - ).toBeVisible(); - expect( + fireEvent.click( screen.getByRole('button', { name: 'Continue with GitHub' }), - ).toBeVisible(); - expect( - screen.getByRole('button', { name: 'Continue with email' }), - ).toBeVisible(); - expect( - screen.getByText(/By continuing, you agree to the Terms of Service/), - ).toBeVisible(); + ); + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.SignupButton, + target_id: 'hijacking', + }); + expect(assignMock).toHaveBeenCalledWith(signupHref); fireEvent.click( - screen.getByRole('button', { name: 'Continue with email' }), + screen.getByRole('button', { name: 'Signup using email' }), ); + expect(assignMock).toHaveBeenCalledWith(signupHref); + }); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { - isLogin: false, - defaultDisplay: AuthDisplay.Registration, - formValues: undefined, - }, + it('redirects the log in link to the webapp onboarding login flow', () => { + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: 'Log in' })); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_type: TargetType.LoginButton, + target_id: 'hijacking', }); + expect(assignMock).toHaveBeenCalledWith(loginHref); }); it('logs a signup impression for new visitors', () => { @@ -335,7 +315,9 @@ describe('HijackingLoginStrip', () => { const { rerender } = renderComponent(); - expect(logEvent).not.toHaveBeenCalled(); + expect(logEvent).not.toHaveBeenCalledWith( + expect.objectContaining({ event_name: LogEvent.Impression }), + ); signBackState = { isLoaded: true, @@ -365,7 +347,7 @@ describe('HijackingLoginStrip', () => { }); }); - it('offers a welcome-back "Continue as" for users with a remembered account', () => { + it('offers a welcome-back "Continue as" that redirects to the login flow', () => { mockUseSignBack.mockReturnValue({ isLoaded: true, signBack: { @@ -384,18 +366,16 @@ describe('HijackingLoginStrip', () => { ).toBeVisible(); expect(screen.getByText('tsahi@daily.dev')).toBeVisible(); - const cta = screen.getByRole('button', { name: /Continue as Tsahi/ }); - fireEvent.click(cta); + fireEvent.click( + screen.getByRole('button', { name: /Continue as Tsahi/ }), + ); expect(logEvent).toHaveBeenCalledWith({ - event_name: LogEvent.Impression, + event_name: LogEvent.Click, target_type: TargetType.LoginButton, target_id: 'hijacking', }); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: true }, - }); + expect(assignMock).toHaveBeenCalledWith(loginHref); }); it('lets remembered users create a different account', () => { @@ -416,10 +396,7 @@ describe('HijackingLoginStrip', () => { screen.getByRole('button', { name: 'Create an account' }), ); - expect(showLogin).toHaveBeenCalledWith({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: false }, - }); + expect(assignMock).toHaveBeenCalledWith(signupHref); }); it('shows an onboarding CTA for logged in users who still need onboarding', () => { @@ -432,10 +409,8 @@ describe('HijackingLoginStrip', () => { ).toBeVisible(); const cta = screen.getByRole('link', { name: /Continue/ }); - const expectedUrl = new URL(onboardingUrl); - expectedUrl.searchParams.append('r', 'extension'); - expect(cta).toHaveAttribute('href', expectedUrl.toString()); + expect(cta).toHaveAttribute('href', signupHref); fireEvent.click(cta); diff --git a/packages/extension/src/newtab/HijackingLoginStrip.tsx b/packages/extension/src/newtab/HijackingLoginStrip.tsx index 24e8b57b589..ed8562ee063 100644 --- a/packages/extension/src/newtab/HijackingLoginStrip.tsx +++ b/packages/extension/src/newtab/HijackingLoginStrip.tsx @@ -7,14 +7,13 @@ import { ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; import { ClickableText } from '@dailydotdev/shared/src/components/buttons/ClickableText'; -import AuthOptions from '@dailydotdev/shared/src/components/auth/AuthOptions'; +import { OnboardingRegistrationForm } from '@dailydotdev/shared/src/components/auth/OnboardingRegistrationForm'; import { ProfileImageSize, ProfilePicture, } from '@dailydotdev/shared/src/components/ProfilePicture'; import { - AuthDisplay, - type AuthOptionsProps, + OnboardingActions, providerMap, type SocialProvider, } from '@dailydotdev/shared/src/components/auth/common'; @@ -47,12 +46,21 @@ const primaryCta = const glassCta = '!border-white/20 !bg-white/[0.06] !text-white backdrop-blur-sm transition-colors duration-200 hover:!bg-white/[0.12]'; -const onboardingHref = (() => { +// The extension can't run social OAuth from its own origin (the API rejects it +// with a 403). Instead we hand auth off to the webapp onboarding flow, which +// runs on a trusted origin and auto-triggers the relevant auth screen. +const buildOnboardingHref = (action?: OnboardingActions): string => { const base = new URL(onboardingUrl); base.searchParams.append('r', 'extension'); + if (action) { + base.searchParams.append('action', action); + } return base.toString(); -})(); +}; + +const onboardingHref = buildOnboardingHref(); +const loginHref = buildOnboardingHref(OnboardingActions.Login); function BrandLockup(): ReactElement { return ( @@ -76,20 +84,15 @@ function CatHeroImage(): ReactElement { } interface SigninHeroProps { - onSignupClick: () => void; - onLoginClick: () => void; - formRef: AuthOptionsProps['formRef']; - onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate']; + onSignup: () => void; + onLogin: () => void; } -type HeroActionButtonsProps = Pick< - SigninHeroProps, - 'onSignupClick' | 'onLoginClick' ->; +type HeroActionButtonsProps = SigninHeroProps; function HeroActionButtons({ - onSignupClick, - onLoginClick, + onSignup, + onLogin, }: HeroActionButtonsProps): ReactElement { return (
@@ -98,7 +101,7 @@ function HeroActionButtons({ variant={ButtonVariant.Primary} size={ButtonSize.Large} className={classNames('flex-1', primaryCta)} - onClick={onSignupClick} + onClick={onSignup} > Sign up @@ -107,7 +110,7 @@ function HeroActionButtons({ variant={ButtonVariant.Secondary} size={ButtonSize.Large} className={classNames('flex-1', glassCta)} - onClick={onLoginClick} + onClick={onLogin} > Log in @@ -115,10 +118,7 @@ function HeroActionButtons({ ); } -function CatStageHero({ - onSignupClick, - onLoginClick, -}: SigninHeroProps): ReactElement { +function CatStageHero({ onSignup, onLogin }: SigninHeroProps): ReactElement { return (
@@ -141,10 +141,7 @@ function CatStageHero({ Sign in and daily.dev will remember the topics, saves, upvotes, and discussions that matter to you.

- +
@@ -156,8 +153,8 @@ function CatStageHero({ } function OnboardingSignupHero({ - formRef, - onAuthStateUpdate, + onSignup, + onLogin, }: SigninHeroProps): ReactElement { return (
@@ -195,19 +192,16 @@ function OnboardingSignupHero({

- ( - null, - ) as unknown as AuthOptionsProps['formRef']; const isLoggedOut = !user; const hasContinueAs = isLoggedOut && isSignBackLoaded && !!signBack?.name; @@ -362,30 +353,14 @@ function HijackingHeroStrip({ }); }, [isReadyToLogImpression, variant, logEvent]); - const onSignupClick = (): void => { + const onSignup = (): void => { logClick(TargetType.SignupButton); - showLogin({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: false }, - }); + window.location.assign(onboardingHref); }; - const onLoginClick = (): void => { + const onLogin = (): void => { logClick(TargetType.LoginButton); - showLogin({ - trigger: AuthTriggers.Onboarding, - options: { isLogin: true }, - }); - }; - const onAuthStateUpdate: AuthOptionsProps['onAuthStateUpdate'] = (props) => { - showLogin({ - trigger: AuthTriggers.Onboarding, - options: { - isLogin: !!props.isLoginFlow, - defaultDisplay: props.defaultDisplay, - formValues: props.email ? { email: props.email } : undefined, - }, - }); + window.location.assign(loginHref); }; const SigninHero = SigninHeroMap[experimentVariant]; @@ -459,25 +434,19 @@ function HijackingHeroStrip({ variant={ButtonVariant.Primary} size={ButtonSize.Large} className={classNames('mt-6 w-full max-w-80', primaryCta)} - onClick={onLoginClick} + onClick={onLogin} > Continue as {firstName} ➔
Not you? - + Use another account
New here? - + Create an account
@@ -485,14 +454,7 @@ function HijackingHeroStrip({ ); } - return ( - - ); + return ; } export default function HijackingLoginStrip(): ReactElement | null { From b5d873ef2537e5f643db8ac110f2aa484e1bd060 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Fri, 5 Jun 2026 16:01:14 +0200 Subject: [PATCH 3/3] set default --- packages/shared/src/lib/featureManagement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index c74d8dd83a4..f349e0e8608 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -217,7 +217,7 @@ export enum HijackingVariant { } export const featureHijackingVariants = new Feature( 'hijacking_variants', - HijackingVariant.CTA, + HijackingVariant.Default, ); export const featureLayoutV2 = new Feature('layout_v2', false);