diff --git a/packages/shared/src/components/MainFeedLayout.tsx b/packages/shared/src/components/MainFeedLayout.tsx
index 35097198f94..ffba4ce4fc3 100644
--- a/packages/shared/src/components/MainFeedLayout.tsx
+++ b/packages/shared/src/components/MainFeedLayout.tsx
@@ -30,7 +30,10 @@ import { useFeeds } from '../hooks/feed/useFeeds';
import { WebappShortcutsRow } from '../features/shortcuts/components/WebappShortcutsRow';
import { AskSearchBanner } from './marketing/banners/AskSearchBanner';
import { FeedEngagementBanner } from './brand/FeedEngagementBanner';
-import { ExploreSignupStrip } from './auth/ExploreSignupStrip';
+import {
+ PublicPageSignupBanner,
+ usePublicPageSignupBannerGate,
+} from './auth/PublicPageSignupBanner';
import FeedContext from '../contexts/FeedContext';
import AuthContext from '../contexts/AuthContext';
import type { LoggedUser } from '../lib/user';
@@ -850,12 +853,22 @@ export default function MainFeedLayout({
chipsTopContent
);
+ // Both pin to the window's bottom edge, so an anonymous visitor gets the
+ // signup banner or the sponsor dock, never both. Auth unknown counts as
+ // "banner may show": the boot cache readies GrowthBook before the remote
+ // boot answers, and the dock must not enroll a visitor it is about to
+ // leave.
+ const signupBannerGate = usePublicPageSignupBannerGate();
+ const hasSignupBannerSlot = !isExtension && isExploreHub;
+ const mayShowSignupBanner = hasSignupBannerSlot && signupBannerGate.mayShow;
+ const showSignupBanner = hasSignupBannerSlot && signupBannerGate.shouldShow;
// Read here rather than inside the feed or the strip: this is the one place
// that owns both, so the card can only ever go missing on a surface that is
// mounting the strip — with headlines in it — in the card's place.
const sponsorStrip = useSponsorStripFeed({
feedName,
disableAds: feedProps?.disableAds,
+ suppressed: mayShowSignupBanner,
});
const v2ActionButtons = feedProps?.actionButtons;
const showFeedV2PageHeader =
@@ -887,16 +900,6 @@ export default function MainFeedLayout({
- {!isExtension && isExploreHub && (
-
-
-
- )}
{isAnyExplore && !showExploreV2PageHeader && }
{isSearchOn && !isSearchPageLaptop && search}
{isSearchOn && !isSearchPageLaptop && (
@@ -953,6 +956,7 @@ export default function MainFeedLayout({
)
)}
{children}
+ {showSignupBanner && }
{/* Docked outside the page container so it spans the feed column and
pins to the window, and mounted here rather than in each app's
diff --git a/packages/shared/src/components/auth/AuthenticationBanner.tsx b/packages/shared/src/components/auth/AuthenticationBanner.tsx
index b71b15efacc..c51ae141a38 100644
--- a/packages/shared/src/components/auth/AuthenticationBanner.tsx
+++ b/packages/shared/src/components/auth/AuthenticationBanner.tsx
@@ -19,11 +19,13 @@ const Section = classed('div', 'flex flex-col');
interface AuthenticationBannerProps extends PropsWithChildren {
compact?: boolean;
+ targetId?: string;
}
export function AuthenticationBanner({
children,
compact,
+ targetId,
}: AuthenticationBannerProps): ReactElement {
const { showLogin } = useAuthContext();
@@ -75,6 +77,7 @@ export function AuthenticationBanner({
ignoreMessages
formRef={null as unknown as React.MutableRefObject}
trigger={AuthTriggers.Onboarding}
+ targetId={targetId}
simplified
defaultDisplay={AuthDisplay.OnboardingSignup}
forceDefaultDisplay
diff --git a/packages/shared/src/components/auth/ExploreSignupStrip.spec.tsx b/packages/shared/src/components/auth/ExploreSignupStrip.spec.tsx
deleted file mode 100644
index afbc7f3ba88..00000000000
--- a/packages/shared/src/components/auth/ExploreSignupStrip.spec.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import React from 'react';
-import { QueryClient } from '@tanstack/react-query';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { TestBootProvider } from '../../../__tests__/helpers/boot';
-import { ExploreSignupStrip } from './ExploreSignupStrip';
-import { hijackingCoverStripMinHeight } from './HijackingCoverStrip';
-import { useViewSize } from '../../hooks/useViewSize';
-import { AuthTriggers } from '../../lib/auth';
-import { LogEvent, TargetId, TargetType } from '../../lib/log';
-
-jest.mock('../../hooks/useViewSize', () => ({
- ...jest.requireActual('../../hooks/useViewSize'),
- useViewSize: jest.fn(),
-}));
-
-const mockUseViewSize = useViewSize as jest.Mock;
-const logEvent = jest.fn();
-const showLogin = jest.fn();
-
-const tree = (auth = {}) => (
-
-
-
-);
-
-const renderComponent = (auth = {}) => render(tree(auth));
-
-const impression = {
- event_name: LogEvent.Impression,
- target_type: TargetType.SignupButton,
- target_id: TargetId.ExploreStrip,
-};
-
-beforeEach(() => {
- jest.clearAllMocks();
- mockUseViewSize.mockReturnValue(true);
-});
-
-describe('ExploreSignupStrip', () => {
- it('should render the strip for anonymous visitors', () => {
- renderComponent();
-
- expect(
- screen.getByRole('heading', {
- name: 'Unlock the full daily.dev experience',
- }),
- ).toBeInTheDocument();
- expect(logEvent).toHaveBeenCalledWith(impression);
- });
-
- it('should render nothing for logged-in users', () => {
- const { container } = renderComponent({
- isLoggedIn: true,
- user: { id: 'u1' },
- });
-
- expect(container).toBeEmptyDOMElement();
- expect(logEvent).not.toHaveBeenCalled();
- });
-
- it('should hold the slot at the strip height until boot answers', () => {
- const { container } = renderComponent({ isAuthReady: false });
-
- expect(screen.queryByRole('heading')).not.toBeInTheDocument();
- expect(container.firstElementChild?.firstElementChild).toHaveClass(
- hijackingCoverStripMinHeight,
- );
- expect(logEvent).not.toHaveBeenCalled();
- });
-
- it('should not hold the slot for a member known from the boot cache', () => {
- const { container } = renderComponent({
- isAuthReady: false,
- user: { id: 'u1' },
- });
-
- expect(container).toBeEmptyDOMElement();
- });
-
- it('should render nothing and log nothing below the tablet breakpoint', () => {
- mockUseViewSize.mockReturnValue(false);
- const { container } = renderComponent();
-
- expect(container).toBeEmptyDOMElement();
- expect(logEvent).not.toHaveBeenCalled();
- });
-
- it('should log one impression once the viewport reaches tablet', () => {
- mockUseViewSize.mockReturnValue(false);
- const { rerender } = renderComponent();
-
- mockUseViewSize.mockReturnValue(true);
- rerender(tree());
- rerender(tree());
-
- expect(logEvent).toHaveBeenCalledTimes(1);
- expect(logEvent).toHaveBeenCalledWith(impression);
- });
-
- it('should open signup inline and log the click', async () => {
- renderComponent();
-
- await userEvent.click(screen.getByRole('button', { name: /Sign up/ }));
-
- expect(logEvent).toHaveBeenCalledWith({
- event_name: LogEvent.Click,
- target_type: TargetType.SignupButton,
- target_id: TargetId.ExploreStrip,
- });
- expect(showLogin).toHaveBeenCalledWith({
- trigger: AuthTriggers.Onboarding,
- options: { isLogin: false },
- });
- });
-
- it('should open login inline and log the click', async () => {
- renderComponent();
-
- await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
-
- expect(logEvent).toHaveBeenCalledWith({
- event_name: LogEvent.Click,
- target_type: TargetType.LoginButton,
- target_id: TargetId.ExploreStrip,
- });
- expect(showLogin).toHaveBeenCalledWith({
- trigger: AuthTriggers.Onboarding,
- options: { isLogin: true },
- });
- });
-});
diff --git a/packages/shared/src/components/auth/ExploreSignupStrip.tsx b/packages/shared/src/components/auth/ExploreSignupStrip.tsx
deleted file mode 100644
index 6e1c442036c..00000000000
--- a/packages/shared/src/components/auth/ExploreSignupStrip.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { ReactElement } from 'react';
-import React from 'react';
-import classNames from 'classnames';
-import type { HijackingCoverCopy } from './HijackingCoverStrip';
-import {
- HijackingCoverAuthActions,
- HijackingCoverStrip,
- HijackingCoverStripPlaceholder,
-} from './HijackingCoverStrip';
-import { useAuthContext } from '../../contexts/AuthContext';
-import { useLogContext } from '../../contexts/LogContext';
-import useLogEventOnce from '../../hooks/log/useLogEventOnce';
-import { useViewSize, ViewSize } from '../../hooks/useViewSize';
-import { AuthTriggers } from '../../lib/auth';
-import { LogEvent, TargetId, TargetType } from '../../lib/log';
-
-// The new tab control strip's signed-out copy.
-const copy: HijackingCoverCopy = {
- heading: 'Unlock the full daily.dev experience',
- body: 'Log in to pick up where you left off.',
-};
-
-// The new tab's cover strip for anonymous visitors, tablet and up.
-export function ExploreSignupStrip({
- className,
-}: {
- className?: string;
-}): ReactElement | null {
- const { isAuthReady, user, showLogin } = useAuthContext();
- const { logEvent } = useLogContext();
- const isTablet = useViewSize(ViewSize.Tablet);
- const isAnonymous = isAuthReady && !user;
-
- useLogEventOnce(
- () => ({
- event_name: LogEvent.Impression,
- target_type: TargetType.SignupButton,
- target_id: TargetId.ExploreStrip,
- }),
- { condition: isAnonymous && isTablet },
- );
-
- if (user) {
- return null;
- }
-
- // Holds the strip's slot in the server HTML until boot answers.
- if (!isAuthReady) {
- return (
-
- );
- }
-
- if (!isTablet) {
- return null;
- }
-
- const onAuthClick = (isLogin: boolean) => (): void => {
- logEvent({
- event_name: LogEvent.Click,
- target_type: isLogin ? TargetType.LoginButton : TargetType.SignupButton,
- target_id: TargetId.ExploreStrip,
- });
-
- showLogin({ trigger: AuthTriggers.Onboarding, options: { isLogin } });
- };
-
- return (
-
- }
- />
- );
-}
diff --git a/packages/shared/src/components/auth/HijackingCoverStrip.tsx b/packages/shared/src/components/auth/HijackingCoverStrip.tsx
index c87a7631091..5fe887b448a 100644
--- a/packages/shared/src/components/auth/HijackingCoverStrip.tsx
+++ b/packages/shared/src/components/auth/HijackingCoverStrip.tsx
@@ -1,7 +1,6 @@
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import classNames from 'classnames';
-import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { cloudinaryHijackingCoverArt } from '../../lib/image';
export const hijackingPrimaryCta =
@@ -15,54 +14,10 @@ export interface HijackingCoverCopy {
body: string;
}
-interface HijackingCoverAuthActionsProps {
- signup: string;
- login: string;
- onSignupClick: () => void;
- onLoginClick: () => void;
-}
-
-// The sign up / log in pair for surfaces that offer both.
-export function HijackingCoverAuthActions({
- signup,
- login,
- onSignupClick,
- onLoginClick,
-}: HijackingCoverAuthActionsProps): ReactElement {
- return (
- <>
-
-
- >
- );
-}
-
const coverArtPosition = { objectPosition: '50% 62%' };
// The card's height without a sizer.
-export const hijackingCoverStripMinHeight = 'min-h-[14rem]';
+const hijackingCoverStripMinHeight = 'min-h-[14rem]';
interface HijackingCoverCardProps {
children: ReactNode;
@@ -152,16 +107,3 @@ export function HijackingCoverStrip({
);
}
-
-// The same box as the strip without a sizer, empty: holds its slot.
-export function HijackingCoverStripPlaceholder({
- className,
-}: {
- className?: string;
-}): ReactElement {
- return (
-
-
-
- );
-}
diff --git a/packages/shared/src/components/auth/PostAuthBanner.tsx b/packages/shared/src/components/auth/PostAuthBanner.tsx
index 69907d17d28..86d4757026c 100644
--- a/packages/shared/src/components/auth/PostAuthBanner.tsx
+++ b/packages/shared/src/components/auth/PostAuthBanner.tsx
@@ -25,10 +25,12 @@ const GeoPersonalizedBanner = dynamic(
interface PostAuthBannerProps {
compact?: boolean;
+ targetId?: string;
}
export const PostAuthBanner = ({
compact,
+ targetId,
}: PostAuthBannerProps = {}): ReactElement => {
const searchParams = useSearchParams();
const { geo } = useAuthContext();
@@ -36,17 +38,35 @@ 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/auth/PublicPageSignupBanner.spec.tsx b/packages/shared/src/components/auth/PublicPageSignupBanner.spec.tsx
new file mode 100644
index 00000000000..0e4cdbf8faf
--- /dev/null
+++ b/packages/shared/src/components/auth/PublicPageSignupBanner.spec.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { QueryClient } from '@tanstack/react-query';
+import { render, screen } from '@testing-library/react';
+import { TestBootProvider } from '../../../__tests__/helpers/boot';
+import { PublicPageSignupBanner } from './PublicPageSignupBanner';
+import { useViewSize } from '../../hooks/useViewSize';
+import { LogEvent, TargetId, TargetType } from '../../lib/log';
+
+jest.mock('../../hooks/useViewSize', () => ({
+ ...jest.requireActual('../../hooks/useViewSize'),
+ useViewSize: jest.fn(),
+}));
+
+jest.mock('./PostAuthBanner', () => ({
+ PostAuthBanner: ({ targetId }: { targetId?: string }) => (
+
+ ),
+}));
+
+// The banner loads lazily; the suite-wide dynamic mock resolves a tick late.
+jest.mock('next/dynamic', () => () => {
+ const { PostAuthBanner } = jest.requireMock('./PostAuthBanner');
+
+ return PostAuthBanner;
+});
+
+const mockUseViewSize = useViewSize as jest.Mock;
+const logEvent = jest.fn();
+
+const impression = {
+ event_name: LogEvent.Impression,
+ target_type: TargetType.SignupButton,
+ target_id: TargetId.PublicPageSignupBanner,
+};
+
+const renderComponent = (auth = {}) =>
+ render(
+
+
+ ,
+ );
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseViewSize.mockReturnValue(true);
+});
+
+describe('PublicPageSignupBanner', () => {
+ it('should render the banner and its clearance for anonymous laptop visitors', async () => {
+ const { container } = renderComponent();
+
+ expect(await screen.findByTestId('post-auth-banner')).toHaveAttribute(
+ 'data-target-id',
+ TargetId.PublicPageSignupBanner,
+ );
+ expect(container.firstElementChild).toHaveClass('h-72');
+ expect(logEvent).toHaveBeenCalledTimes(1);
+ expect(logEvent).toHaveBeenCalledWith(impression);
+ });
+
+ it('should render nothing for logged-in users', () => {
+ const { container } = renderComponent({
+ isLoggedIn: true,
+ user: { id: 'u1' },
+ });
+
+ expect(container).toBeEmptyDOMElement();
+ expect(logEvent).not.toHaveBeenCalled();
+ });
+
+ it('should render nothing until boot answers', () => {
+ const { container } = renderComponent({ isAuthReady: false });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render nothing below the laptop breakpoint', () => {
+ mockUseViewSize.mockReturnValue(false);
+ const { container } = renderComponent();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/packages/shared/src/components/auth/PublicPageSignupBanner.tsx b/packages/shared/src/components/auth/PublicPageSignupBanner.tsx
index 7b5e585ae45..72f46767e64 100644
--- a/packages/shared/src/components/auth/PublicPageSignupBanner.tsx
+++ b/packages/shared/src/components/auth/PublicPageSignupBanner.tsx
@@ -1,22 +1,56 @@
import type { ReactElement } from 'react';
import React from 'react';
-import { useConditionalFeature, useViewSize, ViewSize } from '../../hooks';
-import { useOnboardingActions } from '../../hooks/auth';
-import { featurePublicSignupBanner } from '../../lib/featureManagement';
-import { PostAuthBanner } from './PostAuthBanner';
+import dynamic from 'next/dynamic';
+import { useViewSize, ViewSize } from '../../hooks/useViewSize';
+import { useAuthContext } from '../../contexts/AuthContext';
+import useLogEventOnce from '../../hooks/log/useLogEventOnce';
+import { LogEvent, TargetId, TargetType } from '../../lib/log';
-export function PublicPageSignupBanner(): ReactElement | null {
+const PostAuthBanner = dynamic(() =>
+ import(/* webpackChunkName: "postAuthBanner" */ './PostAuthBanner').then(
+ (mod) => mod.PostAuthBanner,
+ ),
+);
+
+interface PublicPageSignupBannerGate {
+ /** Auth is unknown or anonymous on a laptop: the banner may end up showing. */
+ mayShow: boolean;
+ shouldShow: boolean;
+}
+
+// The one gate for the banner and for whatever else has to yield the
+// window's bottom edge to it, so the two can never drift apart.
+export const usePublicPageSignupBannerGate = (): PublicPageSignupBannerGate => {
const isLaptop = useViewSize(ViewSize.Laptop);
- const { shouldShowAuthBanner } = useOnboardingActions();
- const shouldEvaluate = shouldShowAuthBanner && isLaptop;
- const { value: isEnabled } = useConditionalFeature({
- feature: featurePublicSignupBanner,
- shouldEvaluate,
- });
-
- if (!shouldEvaluate || !isEnabled) {
+ const { isAuthReady, user } = useAuthContext();
+ const mayShow = isLaptop && (!isAuthReady || !user);
+
+ return { mayShow, shouldShow: mayShow && isAuthReady };
+};
+
+// The post page's bottom banner on the public pages. It pins to the window,
+// so it goes at the end of the page where its spacer keeps the last of the
+// content reachable above it.
+export function PublicPageSignupBanner(): ReactElement | null {
+ const { shouldShow } = usePublicPageSignupBannerGate();
+
+ useLogEventOnce(
+ () => ({
+ event_name: LogEvent.Impression,
+ target_type: TargetType.SignupButton,
+ target_id: TargetId.PublicPageSignupBanner,
+ }),
+ { condition: shouldShow },
+ );
+
+ if (!shouldShow) {
return null;
}
- 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 4c54902ad0c..af49ea8e02b 100644
--- a/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx
+++ b/packages/shared/src/components/marketing/banners/personalized/GeoPersonalizedBanner.tsx
@@ -6,14 +6,16 @@ import { AuthenticationBanner, OnboardingHeadline } from '../../../auth';
const GeoPersonalizedBanner = ({
geo,
compact,
+ targetId,
}: {
geo: string;
compact?: boolean;
+ targetId?: string;
}): ReactElement => {
const country = geoToCountry(geo);
return (
-
+ {
const gradient = socialGradient[site];
return (
-
+ {
const key = generateQueryKey(RequestKey.ReferringUser);
const { data: user, isError } = useQuery({
@@ -19,13 +21,13 @@ const UserPersonalizedBanner = ({
});
if (isError) {
- return ;
+ return ;
}
const name = user?.name ? user?.name.split(' ')[0] : user?.username;
return (
-
+
)}
-
-
{children}
+
>
);
diff --git a/packages/shared/src/components/tags/TagTopicPage.tsx b/packages/shared/src/components/tags/TagTopicPage.tsx
index 2b5329ee404..6d6d41e20f6 100644
--- a/packages/shared/src/components/tags/TagTopicPage.tsx
+++ b/packages/shared/src/components/tags/TagTopicPage.tsx
@@ -73,7 +73,6 @@ import { EntitySectionHeading } from '../entity/EntitySectionHeading';
import { EntityRailWithFade } from '../entity/EntityRailWithFade';
import { TagPageNavbar } from './TagPageNavbar';
import { PublicPageSignupBanner } from '../auth/PublicPageSignupBanner';
-import { ExploreSignupStrip } from '../auth/ExploreSignupStrip';
import { largeNumberFormat } from '../../lib/numberFormat';
import { webappUrl } from '../../lib/constants';
import {
@@ -399,7 +398,6 @@ export const TagTopicPage = ({
/>
-
{/* Hero cover — centered on the page; content below spans full width. */}
{!engagementStripCreative && }
diff --git a/packages/shared/src/components/tags/TagsDirectoryPage.tsx b/packages/shared/src/components/tags/TagsDirectoryPage.tsx
index 1b7753e8142..c2d6d7f9c3d 100644
--- a/packages/shared/src/components/tags/TagsDirectoryPage.tsx
+++ b/packages/shared/src/components/tags/TagsDirectoryPage.tsx
@@ -13,7 +13,6 @@ import { TagPageNavbar } from './TagPageNavbar';
import { TagDirectory } from './TagDirectory';
import { TagDirectoryFilter } from './TagDirectoryFilter';
import { PublicPageSignupBanner } from '../auth/PublicPageSignupBanner';
-import { ExploreSignupStrip } from '../auth/ExploreSignupStrip';
import {
Typography,
TypographyColor,
@@ -118,7 +117,6 @@ export function TagsDirectoryPage({
/>
-
{/* Hero */}
- renderHook(() => useSponsorStrip({ feedName, disableAds }), {
+}: {
+ feedName?: string;
+ disableAds?: boolean;
+ suppressed?: boolean;
+ isPlus?: boolean;
+} = {}) =>
+ renderHook(() => useSponsorStrip({ feedName, disableAds, suppressed }), {
wrapper: ({ children }) => (
{
mockViewSize.mockReturnValue(true);
});
+it('should not evaluate the flag while another surface may take the bottom edge', () => {
+ expect(render({ suppressed: true }).result.current).toBe(false);
+ expect(mockFeature).toHaveBeenCalledWith(
+ expect.objectContaining({ shouldEvaluate: false }),
+ );
+});
+
it('should be on for a feed that can show the Happening Now card', () => {
expect(render().result.current).toBe(true);
});
diff --git a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStrip.ts b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStrip.ts
index 70bb46f55c6..5cf26342f57 100644
--- a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStrip.ts
+++ b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStrip.ts
@@ -9,6 +9,12 @@ interface UseSponsorStripProps {
feedName?: string;
/** The feed's own ads switch (feed previews, squads) — never inventory. */
disableAds?: boolean;
+ /**
+ * Another surface may take the window's bottom edge (the signup banner).
+ * The flag is not evaluated while set, so nobody who could end up with
+ * that surface is enrolled in the strip's experiment.
+ */
+ suppressed?: boolean;
}
/**
@@ -22,6 +28,7 @@ const isSponsorStripFeed = (feedName?: string): boolean =>
export const useSponsorStrip = ({
feedName,
disableAds,
+ suppressed,
}: UseSponsorStripProps = {}): boolean => {
const { isPlus } = usePlusSubscription();
// `useViewSizeClient`, not `useViewSize`: the latter reads matchMedia in its
@@ -31,7 +38,11 @@ export const useSponsorStrip = ({
const isTablet = useViewSizeClient(ViewSize.Tablet);
// A logo wall on a phone costs more feed than it can hold logos.
const isEligible =
- !isPlus && !disableAds && isTablet && isSponsorStripFeed(feedName);
+ !isPlus &&
+ !disableAds &&
+ !suppressed &&
+ isTablet &&
+ isSponsorStripFeed(feedName);
const { value: isOn } = useConditionalFeature({
feature: featureSponsorStrip,
shouldEvaluate: isEligible,
diff --git a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.spec.tsx b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.spec.tsx
index dab23fff9e0..5d9a7272e5d 100644
--- a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.spec.tsx
+++ b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.spec.tsx
@@ -17,8 +17,8 @@ const settled = (headlines: StatuslineItem[]) => ({
isSettled: true,
});
-const render = () =>
- renderHook(() => useSponsorStripFeed({ feedName: 'my-feed' }));
+const render = (props: { suppressed?: boolean } = {}) =>
+ renderHook(() => useSponsorStripFeed({ feedName: 'my-feed', ...props }));
beforeEach(() => {
jest.clearAllMocks();
@@ -26,6 +26,14 @@ beforeEach(() => {
mockHeadlines.mockReturnValue(settled([headline]));
});
+it('should hand the suppression to the strip gate', () => {
+ render({ suppressed: true });
+
+ expect(mockStrip).toHaveBeenCalledWith(
+ expect.objectContaining({ suppressed: true }),
+ );
+});
+
it('should not query headlines when the strip is off', () => {
mockStrip.mockReturnValue(false);
render();
diff --git a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.ts b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.ts
index cc10d7b583e..d665d7ab62b 100644
--- a/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.ts
+++ b/packages/shared/src/features/monetization/sponsorStrip/useSponsorStripFeed.ts
@@ -5,6 +5,7 @@ import { useStripHeadlines } from './useStripHeadlines';
interface UseSponsorStripFeedProps {
feedName?: string;
disableAds?: boolean;
+ suppressed?: boolean;
}
interface UseSponsorStripFeed {
@@ -26,8 +27,9 @@ interface UseSponsorStripFeed {
export const useSponsorStripFeed = ({
feedName,
disableAds,
+ suppressed,
}: UseSponsorStripFeedProps): UseSponsorStripFeed => {
- const isEnabled = useSponsorStrip({ feedName, disableAds });
+ const isEnabled = useSponsorStrip({ feedName, disableAds, suppressed });
const { headlines, isSettled } = useStripHeadlines(isEnabled);
return {
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index 82865d3b410..fb4a71ce0f2 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -306,11 +306,6 @@ export const featureFeedContentVisibility = new Feature(
false,
);
-export const featurePublicSignupBanner = new Feature(
- 'public_signup_banner',
- false,
-);
-
// Experiment: the post page's full-width signup banner against the cover
// card pinned over the bottom of the window at the content's width.
export const featurePostSignupStrip = new Feature('post_signup_strip', false);
diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts
index 31e3541cf52..c24e5adfb6b 100644
--- a/packages/shared/src/lib/log.ts
+++ b/packages/shared/src/lib/log.ts
@@ -656,8 +656,8 @@ export enum TargetId {
FeedbackOpen = 'feedback open',
SearchReferralBadge = 'search referral badge',
InviteBanner = 'invite banner',
- ExploreStrip = 'explore strip',
PostStrip = 'post strip',
+ PublicPageSignupBanner = 'public page signup banner',
MobileHeader = 'mobile header',
InviteProfileMenu = 'invite in profile menu',
SearchActivation = 'search activation',
diff --git a/packages/webapp/__tests__/ExplorePage.tsx b/packages/webapp/__tests__/ExplorePage.tsx
new file mode 100644
index 00000000000..43f351a9703
--- /dev/null
+++ b/packages/webapp/__tests__/ExplorePage.tsx
@@ -0,0 +1,106 @@
+import nock from 'nock';
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { QueryClient } from '@tanstack/react-query';
+import type { NextRouter } from 'next/router';
+import { useRouter } from 'next/router';
+import type { AuthContextData } from '@dailydotdev/shared/src/contexts/AuthContext';
+import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature';
+import {
+ useViewSize,
+ useViewSizeClient,
+} from '@dailydotdev/shared/src/hooks/useViewSize';
+import { checkIsExtension } from '@dailydotdev/shared/src/lib/func';
+import { TestBootProvider } from '@dailydotdev/shared/__tests__/helpers/boot';
+import defaultUser from '@dailydotdev/shared/__tests__/fixture/loggedUser';
+import Posts from '../pages/posts/index';
+
+jest.setTimeout(30000);
+
+jest.mock('@dailydotdev/shared/src/hooks/useConditionalFeature', () => ({
+ __esModule: true,
+ useConditionalFeature: jest.fn(),
+}));
+
+jest.mock('@dailydotdev/shared/src/lib/func', () => ({
+ ...jest.requireActual('@dailydotdev/shared/src/lib/func'),
+ checkIsExtension: jest.fn(() => false),
+}));
+
+jest.mock('@dailydotdev/shared/src/hooks/useViewSize', () => ({
+ ...jest.requireActual('@dailydotdev/shared/src/hooks/useViewSize'),
+ useViewSize: jest.fn(),
+ useViewSizeClient: jest.fn(),
+}));
+
+const mockFeature = jest.mocked(useConditionalFeature);
+
+const sponsorStripEvaluations = () =>
+ mockFeature.mock.calls
+ .filter(([args]) => args.feature.id === 'sponsor_strip')
+ .map(([args]) => args.shouldEvaluate);
+
+beforeAll(async () => {
+ await import('@dailydotdev/shared/src/components/MainFeedLayout');
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ nock.cleanAll();
+ jest.mocked(checkIsExtension).mockReturnValue(false);
+ jest.mocked(useViewSize).mockReturnValue(true);
+ jest.mocked(useViewSizeClient).mockReturnValue(true);
+ mockFeature.mockImplementation(({ feature }) => ({
+ value: feature.defaultValue,
+ isLoading: false,
+ }));
+ jest.mocked(useRouter).mockImplementation(
+ () =>
+ ({
+ pathname: '/posts',
+ query: {},
+ replace: jest.fn(),
+ push: jest.fn(),
+ isReady: true,
+ } as unknown as NextRouter),
+ );
+});
+
+const tree = (auth: Partial) => (
+
+ {Posts.getLayout(, {}, Posts.layoutProps)}
+
+);
+
+const bannerHeadline = 'Where developers suffer together';
+
+it('should give an anonymous laptop visitor the signup banner and never the sponsor dock', async () => {
+ const { rerender } = render(tree({ isAuthReady: false }));
+
+ expect(screen.queryByText(bannerHeadline)).not.toBeInTheDocument();
+ expect(sponsorStripEvaluations()).not.toContain(true);
+
+ rerender(tree({ isAuthReady: true }));
+
+ expect(await screen.findByText(bannerHeadline)).toBeInTheDocument();
+ expect(screen.queryByTestId('sponsorStrip')).not.toBeInTheDocument();
+ expect(sponsorStripEvaluations()).not.toContain(true);
+});
+
+it('should leave the extension new tab alone', async () => {
+ jest.mocked(checkIsExtension).mockReturnValue(true);
+ render(tree({ isAuthReady: true }));
+
+ expect(screen.queryByText(bannerHeadline)).not.toBeInTheDocument();
+ expect(sponsorStripEvaluations()).toContain(true);
+});
+
+it('should leave the sponsor dock to members', async () => {
+ render(tree({ isAuthReady: true, isLoggedIn: true, user: defaultUser }));
+
+ expect(screen.queryByText(bannerHeadline)).not.toBeInTheDocument();
+ expect(sponsorStripEvaluations()).toContain(true);
+});
diff --git a/packages/webapp/components/layouts/ProfileLayout/index.tsx b/packages/webapp/components/layouts/ProfileLayout/index.tsx
index 00b92eef294..460ebe9facf 100644
--- a/packages/webapp/components/layouts/ProfileLayout/index.tsx
+++ b/packages/webapp/components/layouts/ProfileLayout/index.tsx
@@ -22,7 +22,6 @@ import { useProfile } from '@dailydotdev/shared/src/hooks/profile/useProfile';
import { useTrackQuestClientEvent } from '@dailydotdev/shared/src/hooks/useTrackQuestClientEvent';
import CustomAuthBanner from '@dailydotdev/shared/src/components/auth/CustomAuthBanner';
import { PublicPageSignupBanner } from '@dailydotdev/shared/src/components/auth/PublicPageSignupBanner';
-import { ExploreSignupStrip } from '@dailydotdev/shared/src/components/auth/ExploreSignupStrip';
import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log';
@@ -158,7 +157,6 @@ export default function ProfileLayout({
)}
-
{children}
+
>
);
diff --git a/packages/webapp/pages/sources/index.tsx b/packages/webapp/pages/sources/index.tsx
index 630c75f044b..10fa3f205cc 100644
--- a/packages/webapp/pages/sources/index.tsx
+++ b/packages/webapp/pages/sources/index.tsx
@@ -25,7 +25,6 @@ import { ExploreHubHeader } from '@dailydotdev/shared/src/components/header/Expl
import { PageWrapperLayout } from '@dailydotdev/shared/src/components/layout/PageWrapperLayout';
import { SourceTopList } from '@dailydotdev/shared/src/components/cards/Leaderboard';
import { PublicPageSignupBanner } from '@dailydotdev/shared/src/components/auth/PublicPageSignupBanner';
-import { ExploreSignupStrip } from '@dailydotdev/shared/src/components/auth/ExploreSignupStrip';
import { getLayout } from '../../components/layouts/MainLayout';
import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout';
import { defaultOpenGraph } from '../../next-seo';
@@ -133,7 +132,6 @@ const SourcesPage = ({
}}
/>
-
{!isV2Laptop && (
diff --git a/packages/webapp/pages/users.tsx b/packages/webapp/pages/users.tsx
index 1a46ccbbdf8..6264db9764e 100644
--- a/packages/webapp/pages/users.tsx
+++ b/packages/webapp/pages/users.tsx
@@ -24,7 +24,6 @@ import { CompanyTopList } from '@dailydotdev/shared/src/components/cards/Leaderb
import type { PopularHotTakes } from '@dailydotdev/shared/src/components/cards/Leaderboard/PopularHotTakesList';
import { PopularHotTakesList } from '@dailydotdev/shared/src/components/cards/Leaderboard/PopularHotTakesList';
import { PublicPageSignupBanner } from '@dailydotdev/shared/src/components/auth/PublicPageSignupBanner';
-import { ExploreSignupStrip } from '@dailydotdev/shared/src/components/auth/ExploreSignupStrip';
import { getLayout as getFooterNavBarLayout } from '../components/layouts/FooterNavBarLayout';
import { getLayout } from '../components/layouts/MainLayout';
import { defaultOpenGraph } from '../next-seo';
@@ -93,7 +92,6 @@ const LeaderboardPage = ({
<>
{isV2Laptop && }
-
{!isV2Laptop && (
diff --git a/packages/webapp/pages/watercooler.tsx b/packages/webapp/pages/watercooler.tsx
index 1d0a9128afe..11f01273dbc 100644
--- a/packages/webapp/pages/watercooler.tsx
+++ b/packages/webapp/pages/watercooler.tsx
@@ -3,7 +3,7 @@ import React, { useMemo } from 'react';
import type { NextSeoProps } from 'next-seo';
import dynamic from 'next/dynamic';
import Feed from '@dailydotdev/shared/src/components/Feed';
-import { ExploreSignupStrip } from '@dailydotdev/shared/src/components/auth/ExploreSignupStrip';
+import { PublicPageSignupBanner } from '@dailydotdev/shared/src/components/auth/PublicPageSignupBanner';
import {
SOURCE_FEED_QUERY,
baseFeedSupportedTypes,
@@ -90,7 +90,6 @@ const WatercoolerPage = (): ReactElement => {
return (