diff --git a/packages/shared/src/components/BookmarkFeedLayout.tsx b/packages/shared/src/components/BookmarkFeedLayout.tsx index dce80cd871..6b8271be53 100644 --- a/packages/shared/src/components/BookmarkFeedLayout.tsx +++ b/packages/shared/src/components/BookmarkFeedLayout.tsx @@ -1,5 +1,11 @@ import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import dynamic from 'next/dynamic'; import classNames from 'classnames'; import { @@ -21,6 +27,7 @@ import { generateQueryKey, OtherFeedPage, RequestKey } from '../lib/query'; import { useFeedLayout, useViewSize, ViewSize } from '../hooks'; import { BookmarkSection } from './sidebar/sections/BookmarkSection'; import PlusMobileEntryBanner from './banners/PlusMobileEntryBanner'; +import { DigestBookmarkBanner } from './notifications/DigestBookmarkBanner'; import { Typography, TypographyTag, @@ -117,6 +124,8 @@ export default function BookmarkFeedLayout({ ], ); const { plusEntryBookmark } = usePlusEntry(); + const [isEmptyFeed, setIsEmptyFeed] = useState(false); + const onEmptyFeed = useCallback(() => setIsEmptyFeed(true), []); const feedProps = useMemo>(() => { if (isSearchResults) { return { @@ -245,8 +254,10 @@ export default function BookmarkFeedLayout({ /> )} + {/* Digest upsell only shown when bookmarks are empty to engage new/inactive users */} + {!plusEntryBookmark && isEmptyFeed && } {tokenRefreshed && (isSearchResults || loadedSort) && ( - + )} ); diff --git a/packages/shared/src/components/notifications/DigestBookmarkBanner.spec.tsx b/packages/shared/src/components/notifications/DigestBookmarkBanner.spec.tsx new file mode 100644 index 0000000000..2d33d2f55b --- /dev/null +++ b/packages/shared/src/components/notifications/DigestBookmarkBanner.spec.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { DigestBookmarkBanner } from './DigestBookmarkBanner'; +import { LogEvent, TargetId } from '../../lib/log'; +import { UserPersonalizedDigestType } from '../../graphql/users'; +import { ActionType } from '../../graphql/actions'; + +const mockLogEvent = jest.fn(); +const mockGetPersonalizedDigest = jest.fn(); +const mockSubscribePersonalizedDigest = jest.fn().mockResolvedValue({}); +const mockCompleteAction = jest.fn().mockResolvedValue(undefined); +const mockCheckHasCompleted = jest.fn(); +const mockUsePlusSubscription = jest.fn(); +const mockSetNotificationStatuses = jest.fn(); +const mockDisplayToast = jest.fn(); + +jest.mock('../../contexts/LogContext', () => ({ + useLogContext: () => ({ logEvent: mockLogEvent }), +})); + +jest.mock('../../contexts/AuthContext', () => ({ + useAuthContext: () => ({ isAuthReady: true }), +})); + +jest.mock('../../hooks/usePlusSubscription', () => ({ + usePlusSubscription: () => mockUsePlusSubscription(), +})); + +jest.mock('../../hooks/usePersonalizedDigest', () => ({ + usePersonalizedDigest: () => ({ + getPersonalizedDigest: mockGetPersonalizedDigest, + subscribePersonalizedDigest: mockSubscribePersonalizedDigest, + }), + SendType: { Workdays: 'workdays', Daily: 'daily', Weekly: 'weekly' }, +})); + +jest.mock('../../hooks/useActions', () => ({ + useActions: () => ({ + checkHasCompleted: mockCheckHasCompleted, + completeAction: mockCompleteAction, + isActionsFetched: true, + }), +})); + +jest.mock('../../hooks/notifications/useNotificationSettings', () => ({ + __esModule: true, + default: () => ({ + setNotificationStatusBulk: mockSetNotificationStatuses, + }), +})); + +jest.mock('../../hooks/useToastNotification', () => ({ + useToastNotification: () => ({ + displayToast: mockDisplayToast, + }), +})); + +const client = new QueryClient(); + +const renderComponent = () => + render( + + + , + ); + +describe('DigestBookmarkBanner', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUsePlusSubscription.mockReturnValue({ isPlus: false }); + mockGetPersonalizedDigest.mockReturnValue(null); + mockCheckHasCompleted.mockReturnValue(false); + }); + + it('should render banner for non-Plus user without digest', () => { + renderComponent(); + + expect( + screen.getByText('Not sure what to read? Let us pick for you'), + ).toBeInTheDocument(); + expect(screen.getByText('Enable digest')).toBeInTheDocument(); + }); + + it('should not render for Plus users', () => { + mockUsePlusSubscription.mockReturnValue({ isPlus: true }); + + renderComponent(); + + expect( + screen.queryByText('Not sure what to read? Let us pick for you'), + ).not.toBeInTheDocument(); + }); + + it('should not render when user has digest subscription', () => { + mockGetPersonalizedDigest.mockImplementation( + (type: UserPersonalizedDigestType) => + type === UserPersonalizedDigestType.Digest + ? { + type: UserPersonalizedDigestType.Digest, + preferredHour: 9, + flags: { sendType: 'workdays' }, + } + : null, + ); + + renderComponent(); + + expect( + screen.queryByText('Not sure what to read? Let us pick for you'), + ).not.toBeInTheDocument(); + }); + + it('should not render when dismissed via action', () => { + mockCheckHasCompleted.mockReturnValue(true); + + renderComponent(); + + expect( + screen.queryByText('Not sure what to read? Let us pick for you'), + ).not.toBeInTheDocument(); + }); + + it('should log impression on render', () => { + renderComponent(); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_id: TargetId.DigestUpsellBookmarks, + }); + }); + + it('should subscribe, complete action, and log click on CTA', async () => { + renderComponent(); + + const ctaButton = screen.getByText('Enable digest'); + fireEvent.click(ctaButton); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsellBookmarks, + }); + + await waitFor(() => { + expect(mockSubscribePersonalizedDigest).toHaveBeenCalledWith({ + hour: 9, + sendType: 'workdays', + type: UserPersonalizedDigestType.Digest, + }); + }); + + await waitFor(() => { + expect(mockSetNotificationStatuses).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(mockCompleteAction).toHaveBeenCalledWith(ActionType.DigestUpsell); + }); + + await waitFor(() => { + expect(mockDisplayToast).toHaveBeenCalledWith( + 'Digest enabled! Check your inbox tomorrow.', + ); + }); + }); + + it('should log dismiss and complete action on dismiss', async () => { + renderComponent(); + + const closeButton = screen.getByRole('button', { name: 'Close' }); + fireEvent.click(closeButton); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsellBookmarks, + extra: JSON.stringify({ action: 'dismiss' }), + }); + await waitFor(() => { + expect(mockCompleteAction).toHaveBeenCalledWith(ActionType.DigestUpsell); + }); + }); + + it('should show error toast when subscribe fails', async () => { + mockSubscribePersonalizedDigest.mockRejectedValueOnce( + new Error('API error'), + ); + + renderComponent(); + + fireEvent.click(screen.getByText('Enable digest')); + + await waitFor(() => { + expect(mockDisplayToast).toHaveBeenCalledWith( + 'Failed to enable digest. Please try again in settings.', + ); + }); + expect(mockCompleteAction).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/components/notifications/DigestBookmarkBanner.tsx b/packages/shared/src/components/notifications/DigestBookmarkBanner.tsx new file mode 100644 index 0000000000..33dd0d560c --- /dev/null +++ b/packages/shared/src/components/notifications/DigestBookmarkBanner.tsx @@ -0,0 +1,127 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useRef } from 'react'; +import { + Button, + ButtonColor, + ButtonSize, + ButtonVariant, +} from '../buttons/Button'; +import CloseButton from '../CloseButton'; +import { MailIcon } from '../icons'; +import { LogEvent, TargetId } from '../../lib/log'; +import { useLogContext } from '../../contexts/LogContext'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { usePlusSubscription } from '../../hooks/usePlusSubscription'; +import { + usePersonalizedDigest, + SendType, +} from '../../hooks/usePersonalizedDigest'; +import { UserPersonalizedDigestType } from '../../graphql/users'; +import { useActions } from '../../hooks/useActions'; +import { ActionType } from '../../graphql/actions'; +import useNotificationSettings from '../../hooks/notifications/useNotificationSettings'; +import { NotificationType } from './utils'; +import { NotificationPreferenceStatus } from '../../graphql/notifications'; +import { useToastNotification } from '../../hooks/useToastNotification'; + +export function DigestBookmarkBanner(): ReactElement | null { + const { logEvent } = useLogContext(); + const { isAuthReady } = useAuthContext(); + const { isPlus } = usePlusSubscription(); + const { getPersonalizedDigest, subscribePersonalizedDigest } = + usePersonalizedDigest(); + const hasDigest = !!getPersonalizedDigest(UserPersonalizedDigestType.Digest); + const { checkHasCompleted, completeAction, isActionsFetched } = useActions(); + const { setNotificationStatusBulk } = useNotificationSettings(); + const { displayToast } = useToastNotification(); + const dismissed = checkHasCompleted(ActionType.DigestUpsell); + const impressionLogged = useRef(false); + + const showBanner = + isAuthReady && !isPlus && !hasDigest && !dismissed && isActionsFetched; + + useEffect(() => { + if (showBanner && !impressionLogged.current) { + impressionLogged.current = true; + logEvent({ + event_name: LogEvent.Impression, + target_id: TargetId.DigestUpsellBookmarks, + }); + } + }, [showBanner, logEvent]); + + if (!showBanner) { + return null; + } + + const onEnable = async () => { + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsellBookmarks, + }); + + try { + await subscribePersonalizedDigest({ + hour: 9, + sendType: SendType.Workdays, + type: UserPersonalizedDigestType.Digest, + }); + + setNotificationStatusBulk([ + { + type: NotificationType.BriefingReady, + channel: 'email', + status: NotificationPreferenceStatus.Subscribed, + }, + { + type: NotificationType.DigestReady, + channel: 'inApp', + status: NotificationPreferenceStatus.Subscribed, + }, + ]); + + await completeAction(ActionType.DigestUpsell); + + displayToast('Digest enabled! Check your inbox tomorrow.'); + } catch { + displayToast('Failed to enable digest. Please try again in settings.'); + } + }; + + const onDismiss = async () => { + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsellBookmarks, + extra: JSON.stringify({ action: 'dismiss' }), + }); + await completeAction(ActionType.DigestUpsell); + }; + + return ( +
+ + + Not sure what to read? Let us pick for you + +

+ Get a daily digest with the best posts from your favorite topics, + straight to your inbox. +

+
+ +
+ +
+ ); +} diff --git a/packages/shared/src/components/notifications/DigestUpsellBanner.spec.tsx b/packages/shared/src/components/notifications/DigestUpsellBanner.spec.tsx new file mode 100644 index 0000000000..f493cb7f82 --- /dev/null +++ b/packages/shared/src/components/notifications/DigestUpsellBanner.spec.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { DigestUpsellBanner } from './DigestUpsellBanner'; +import { LogEvent, TargetId } from '../../lib/log'; +import { UserPersonalizedDigestType } from '../../graphql/users'; +import { ActionType } from '../../graphql/actions'; + +const mockLogEvent = jest.fn(); +const mockGetPersonalizedDigest = jest.fn(); +const mockSubscribePersonalizedDigest = jest.fn().mockResolvedValue({}); +const mockCompleteAction = jest.fn().mockResolvedValue(undefined); +const mockCheckHasCompleted = jest.fn(); +const mockUsePlusSubscription = jest.fn(); +const mockSetNotificationStatuses = jest.fn(); +const mockDisplayToast = jest.fn(); + +jest.mock('../../contexts/LogContext', () => ({ + useLogContext: () => ({ logEvent: mockLogEvent }), +})); + +jest.mock('../../contexts/AuthContext', () => ({ + useAuthContext: () => ({ isAuthReady: true }), +})); + +jest.mock('../../hooks/usePlusSubscription', () => ({ + usePlusSubscription: () => mockUsePlusSubscription(), +})); + +jest.mock('../../hooks/usePersonalizedDigest', () => ({ + usePersonalizedDigest: () => ({ + getPersonalizedDigest: mockGetPersonalizedDigest, + subscribePersonalizedDigest: mockSubscribePersonalizedDigest, + }), + SendType: { Workdays: 'workdays', Daily: 'daily', Weekly: 'weekly' }, +})); + +jest.mock('../../hooks/useActions', () => ({ + useActions: () => ({ + checkHasCompleted: mockCheckHasCompleted, + completeAction: mockCompleteAction, + isActionsFetched: true, + }), +})); + +jest.mock('../../hooks/notifications/useNotificationSettings', () => ({ + __esModule: true, + default: () => ({ + setNotificationStatusBulk: mockSetNotificationStatuses, + }), +})); + +jest.mock('../../hooks/useToastNotification', () => ({ + useToastNotification: () => ({ + displayToast: mockDisplayToast, + }), +})); + +const client = new QueryClient(); + +const renderComponent = () => + render( + + + , + ); + +describe('DigestUpsellBanner', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUsePlusSubscription.mockReturnValue({ isPlus: false }); + mockGetPersonalizedDigest.mockReturnValue(null); + mockCheckHasCompleted.mockReturnValue(false); + }); + + it('should render banner for non-Plus user without digest', () => { + renderComponent(); + + expect( + screen.getByText('Get the must-read posts delivered daily'), + ).toBeInTheDocument(); + expect(screen.getByText('Enable digest')).toBeInTheDocument(); + }); + + it('should not render for Plus users', () => { + mockUsePlusSubscription.mockReturnValue({ isPlus: true }); + + renderComponent(); + + expect( + screen.queryByText('Get the must-read posts delivered daily'), + ).not.toBeInTheDocument(); + }); + + it('should not render when user has digest subscription', () => { + mockGetPersonalizedDigest.mockImplementation( + (type: UserPersonalizedDigestType) => + type === UserPersonalizedDigestType.Digest + ? { + type: UserPersonalizedDigestType.Digest, + preferredHour: 9, + flags: { sendType: 'workdays' }, + } + : null, + ); + + renderComponent(); + + expect( + screen.queryByText('Get the must-read posts delivered daily'), + ).not.toBeInTheDocument(); + }); + + it('should not render when dismissed via action', () => { + mockCheckHasCompleted.mockReturnValue(true); + + renderComponent(); + + expect( + screen.queryByText('Get the must-read posts delivered daily'), + ).not.toBeInTheDocument(); + }); + + it('should log impression on render', () => { + renderComponent(); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Impression, + target_id: TargetId.DigestUpsell, + }); + }); + + it('should subscribe, complete action, and log click on CTA', async () => { + renderComponent(); + + const ctaButton = screen.getByText('Enable digest'); + fireEvent.click(ctaButton); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsell, + }); + + await waitFor(() => { + expect(mockSubscribePersonalizedDigest).toHaveBeenCalledWith({ + hour: 9, + sendType: 'workdays', + type: UserPersonalizedDigestType.Digest, + }); + }); + + await waitFor(() => { + expect(mockSetNotificationStatuses).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(mockCompleteAction).toHaveBeenCalledWith(ActionType.DigestUpsell); + }); + + await waitFor(() => { + expect(mockDisplayToast).toHaveBeenCalledWith( + 'Digest enabled! Check your inbox tomorrow.', + ); + }); + }); + + it('should log dismiss and complete action on dismiss', async () => { + renderComponent(); + + const closeButton = screen.getByRole('button', { name: 'Close' }); + fireEvent.click(closeButton); + + expect(mockLogEvent).toHaveBeenCalledWith({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsell, + extra: JSON.stringify({ action: 'dismiss' }), + }); + await waitFor(() => { + expect(mockCompleteAction).toHaveBeenCalledWith(ActionType.DigestUpsell); + }); + }); + + it('should show error toast when subscribe fails', async () => { + mockSubscribePersonalizedDigest.mockRejectedValueOnce( + new Error('API error'), + ); + + renderComponent(); + + fireEvent.click(screen.getByText('Enable digest')); + + await waitFor(() => { + expect(mockDisplayToast).toHaveBeenCalledWith( + 'Failed to enable digest. Please try again in settings.', + ); + }); + expect(mockCompleteAction).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/components/notifications/DigestUpsellBanner.tsx b/packages/shared/src/components/notifications/DigestUpsellBanner.tsx new file mode 100644 index 0000000000..d025c35d59 --- /dev/null +++ b/packages/shared/src/components/notifications/DigestUpsellBanner.tsx @@ -0,0 +1,127 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useRef } from 'react'; +import { + Button, + ButtonColor, + ButtonSize, + ButtonVariant, +} from '../buttons/Button'; +import CloseButton from '../CloseButton'; +import { MailIcon } from '../icons'; +import { LogEvent, TargetId } from '../../lib/log'; +import { useLogContext } from '../../contexts/LogContext'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { usePlusSubscription } from '../../hooks/usePlusSubscription'; +import { + usePersonalizedDigest, + SendType, +} from '../../hooks/usePersonalizedDigest'; +import { UserPersonalizedDigestType } from '../../graphql/users'; +import { useActions } from '../../hooks/useActions'; +import { ActionType } from '../../graphql/actions'; +import useNotificationSettings from '../../hooks/notifications/useNotificationSettings'; +import { NotificationType } from './utils'; +import { NotificationPreferenceStatus } from '../../graphql/notifications'; +import { useToastNotification } from '../../hooks/useToastNotification'; + +export function DigestUpsellBanner(): ReactElement | null { + const { logEvent } = useLogContext(); + const { isAuthReady } = useAuthContext(); + const { isPlus } = usePlusSubscription(); + const { getPersonalizedDigest, subscribePersonalizedDigest } = + usePersonalizedDigest(); + const hasDigest = !!getPersonalizedDigest(UserPersonalizedDigestType.Digest); + const { checkHasCompleted, completeAction, isActionsFetched } = useActions(); + const { setNotificationStatusBulk } = useNotificationSettings(); + const { displayToast } = useToastNotification(); + const dismissed = checkHasCompleted(ActionType.DigestUpsell); + const impressionLogged = useRef(false); + + const showBanner = + isAuthReady && !isPlus && !hasDigest && !dismissed && isActionsFetched; + + useEffect(() => { + if (showBanner && !impressionLogged.current) { + impressionLogged.current = true; + logEvent({ + event_name: LogEvent.Impression, + target_id: TargetId.DigestUpsell, + }); + } + }, [showBanner, logEvent]); + + if (!showBanner) { + return null; + } + + const onEnable = async () => { + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsell, + }); + + try { + await subscribePersonalizedDigest({ + hour: 9, + sendType: SendType.Workdays, + type: UserPersonalizedDigestType.Digest, + }); + + setNotificationStatusBulk([ + { + type: NotificationType.BriefingReady, + channel: 'email', + status: NotificationPreferenceStatus.Subscribed, + }, + { + type: NotificationType.DigestReady, + channel: 'inApp', + status: NotificationPreferenceStatus.Subscribed, + }, + ]); + + await completeAction(ActionType.DigestUpsell); + + displayToast('Digest enabled! Check your inbox tomorrow.'); + } catch { + displayToast('Failed to enable digest. Please try again in settings.'); + } + }; + + const onDismiss = async () => { + logEvent({ + event_name: LogEvent.Click, + target_id: TargetId.DigestUpsell, + extra: JSON.stringify({ action: 'dismiss' }), + }); + await completeAction(ActionType.DigestUpsell); + }; + + return ( +
+ + + Get the must-read posts delivered daily + +

+ A personalized digest with top posts from your favorite topics, straight + to your inbox. +

+
+ +
+ +
+ ); +} diff --git a/packages/shared/src/graphql/actions.ts b/packages/shared/src/graphql/actions.ts index 3d8dee4a5a..5bba6dd7d5 100644 --- a/packages/shared/src/graphql/actions.ts +++ b/packages/shared/src/graphql/actions.ts @@ -60,6 +60,7 @@ export enum ActionType { DisableAchievementCompletion = 'disable_achievement_completion', DismissInstallExtension = 'dismiss_install_extension', DismissBriefCard = 'dismiss_brief_card', + DigestUpsell = 'digest_upsell', } export const cvActions = [ diff --git a/packages/shared/src/hooks/notifications/useNotificationSettings.ts b/packages/shared/src/hooks/notifications/useNotificationSettings.ts index 2393a2423d..4d98616dae 100644 --- a/packages/shared/src/hooks/notifications/useNotificationSettings.ts +++ b/packages/shared/src/hooks/notifications/useNotificationSettings.ts @@ -160,6 +160,23 @@ const useNotificationSettings = () => { mutate(updatedSettings); }; + const setNotificationStatusBulk = ( + entries: Array<{ + type: NotificationType; + channel: NotificationChannel; + status: NotificationPreferenceStatus; + }>, + ) => { + const updatedSettings: NotificationSettings = { ...settings }; + entries.forEach(({ type, channel, status }) => { + updatedSettings[type] = { + ...updatedSettings[type], + [channel]: status, + }; + }); + mutate(updatedSettings); + }; + const unsubscribeAllEmail = () => { const updatedSettings: NotificationSettings = Object.keys( settings, @@ -201,6 +218,7 @@ const useNotificationSettings = () => { toggleGroup, getGroupStatus, setNotificationStatus, + setNotificationStatusBulk, unsubscribeAllEmail, emailsDisabled, notificationSettings, diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index becd22cc68..86f97019d4 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -508,6 +508,8 @@ export enum TargetId { Fullscreen = 'fullscreen', Popover = 'popover', Navigation = 'navigation', + DigestUpsell = 'digest upsell', + DigestUpsellBookmarks = 'digest upsell bookmarks', } export enum NotificationChannel { diff --git a/packages/webapp/pages/notifications.tsx b/packages/webapp/pages/notifications.tsx index 4e1aae4ce3..0b0ad58cb9 100644 --- a/packages/webapp/pages/notifications.tsx +++ b/packages/webapp/pages/notifications.tsx @@ -19,16 +19,22 @@ import { import NotificationItem from '@dailydotdev/shared/src/components/notifications/NotificationItem'; import FirstNotification from '@dailydotdev/shared/src/components/notifications/FirstNotification'; import EnableNotification from '@dailydotdev/shared/src/components/notifications/EnableNotification'; +import { DigestUpsellBanner } from '@dailydotdev/shared/src/components/notifications/DigestUpsellBanner'; import { useNotificationContext } from '@dailydotdev/shared/src/contexts/NotificationsContext'; import InfiniteScrolling, { checkFetchMore, } from '@dailydotdev/shared/src/components/containers/InfiniteScrolling'; import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; -import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log'; +import { + LogEvent, + NotificationPromptSource, + Origin, +} from '@dailydotdev/shared/src/lib/log'; import { NotificationType } from '@dailydotdev/shared/src/components/notifications/utils'; import { usePromotionModal } from '@dailydotdev/shared/src/hooks/notifications/usePromotionModal'; import { useTopReaderModal } from '@dailydotdev/shared/src/hooks/modals/useTopReaderModal'; import { usePushNotificationContext } from '@dailydotdev/shared/src/contexts/PushNotificationContext'; +import { useEnableNotification } from '@dailydotdev/shared/src/hooks/notifications/useEnableNotification'; import { gqlClient } from '@dailydotdev/shared/src/graphql/common'; import { useStreakRecoverModal } from '@dailydotdev/shared/src/hooks/notifications/useStreakRecoverModal'; import { getNextPageParam } from '@dailydotdev/shared/src/lib/query'; @@ -52,6 +58,9 @@ const Notifications = (): ReactElement => { const { logEvent } = useLogContext(); const { clearUnreadCount } = useNotificationContext(); const { isSubscribed } = usePushNotificationContext(); + const { shouldShowCta: showPushBanner } = useEnableNotification({ + source: NotificationPromptSource.NotificationsPage, + }); const { mutateAsync: readNotifications } = useMutation({ mutationFn: () => gqlClient.request(READ_NOTIFICATIONS_MUTATION), @@ -108,6 +117,7 @@ const Notifications = (): ReactElement => { className={classNames(pageBorders, pageContainerClassNames, 'pb-12')} > + {!showPushBanner && }