diff --git a/packages/shared/src/components/cards/highlight/HighlightCardOptions.spec.tsx b/packages/shared/src/components/cards/highlight/HighlightCardOptions.spec.tsx
new file mode 100644
index 0000000000..d7ffe74270
--- /dev/null
+++ b/packages/shared/src/components/cards/highlight/HighlightCardOptions.spec.tsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { HighlightCardOptions } from './HighlightCardOptions';
+
+const mockSubscribe = jest.fn().mockResolvedValue(undefined);
+const mockUnsubscribe = jest.fn().mockResolvedValue(undefined);
+const mockDisplayToast = jest.fn();
+const mockUseAuth = jest.fn();
+const mockUseConditionalFeature = jest.fn();
+const mockUseMajorHeadlinesSubscription = jest.fn();
+const mockRouterPush = jest.fn();
+
+jest.mock('next/router', () => ({
+ useRouter: () => ({ push: mockRouterPush }),
+}));
+
+jest.mock('../../../contexts/AuthContext', () => ({
+ useAuthContext: () => mockUseAuth(),
+}));
+
+jest.mock('../../../hooks/useConditionalFeature', () => ({
+ useConditionalFeature: () => mockUseConditionalFeature(),
+}));
+
+jest.mock('../../../hooks/notifications/useMajorHeadlinesSubscription', () => ({
+ useMajorHeadlinesSubscription: () => mockUseMajorHeadlinesSubscription(),
+}));
+
+jest.mock('../../../hooks/useToastNotification', () => ({
+ useToastNotification: () => ({ displayToast: mockDisplayToast }),
+}));
+
+jest.mock('../../tooltip/Tooltip', () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+const renderComponent = () => render( );
+
+describe('HighlightCardOptions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseAuth.mockReturnValue({ user: { id: '1' } });
+ mockUseConditionalFeature.mockReturnValue({ value: true });
+ mockUseMajorHeadlinesSubscription.mockReturnValue({
+ isSubscribed: false,
+ isLoading: false,
+ subscribe: mockSubscribe,
+ unsubscribe: mockUnsubscribe,
+ });
+ });
+
+ it('should render bell button when feature is on and user is logged in', () => {
+ renderComponent();
+
+ expect(
+ screen.getByRole('button', { name: 'Get real-time alerts' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should not render for guests', () => {
+ mockUseAuth.mockReturnValue({ user: undefined });
+
+ renderComponent();
+
+ expect(
+ screen.queryByRole('button', { name: 'Get real-time alerts' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should not render when feature is off', () => {
+ mockUseConditionalFeature.mockReturnValue({ value: false });
+
+ renderComponent();
+
+ expect(
+ screen.queryByRole('button', { name: 'Get real-time alerts' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should subscribe and show toast with settings action when not subscribed', async () => {
+ renderComponent();
+
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Get real-time alerts' }),
+ );
+
+ await waitFor(() => {
+ expect(mockSubscribe).toHaveBeenCalledWith('feed_card');
+ });
+ await waitFor(() => {
+ expect(mockDisplayToast).toHaveBeenCalledWith(
+ "You'll be the first to know when news breaks.",
+ expect.objectContaining({
+ action: expect.objectContaining({ copy: 'Settings' }),
+ }),
+ );
+ });
+ });
+
+ it('should navigate to notification settings when toast action is clicked', async () => {
+ renderComponent();
+
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Get real-time alerts' }),
+ );
+
+ await waitFor(() => {
+ expect(mockDisplayToast).toHaveBeenCalled();
+ });
+
+ const toastArgs = mockDisplayToast.mock.calls[0][1];
+ toastArgs.action.onClick();
+
+ expect(mockRouterPush).toHaveBeenCalledWith('/settings/notifications');
+ });
+
+ it('should unsubscribe when subscribed', async () => {
+ mockUseMajorHeadlinesSubscription.mockReturnValue({
+ isSubscribed: true,
+ isLoading: false,
+ subscribe: mockSubscribe,
+ unsubscribe: mockUnsubscribe,
+ });
+
+ renderComponent();
+
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Turn off real-time alerts' }),
+ );
+
+ await waitFor(() => {
+ expect(mockUnsubscribe).toHaveBeenCalledWith('feed_card');
+ });
+ await waitFor(() => {
+ expect(mockDisplayToast).toHaveBeenCalledWith(
+ 'Real-time alerts turned off.',
+ );
+ });
+ });
+});
diff --git a/packages/shared/src/components/cards/highlight/HighlightCardOptions.tsx b/packages/shared/src/components/cards/highlight/HighlightCardOptions.tsx
new file mode 100644
index 0000000000..a0d9416689
--- /dev/null
+++ b/packages/shared/src/components/cards/highlight/HighlightCardOptions.tsx
@@ -0,0 +1,93 @@
+import type { ReactElement } from 'react';
+import React, { useState } from 'react';
+import classNames from 'classnames';
+import { useRouter } from 'next/router';
+import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button';
+import { BellAddIcon, BellSubscribedIcon } from '../../icons';
+import { Tooltip } from '../../tooltip/Tooltip';
+import { useAuthContext } from '../../../contexts/AuthContext';
+import { useMajorHeadlinesSubscription } from '../../../hooks/notifications/useMajorHeadlinesSubscription';
+import { useConditionalFeature } from '../../../hooks/useConditionalFeature';
+import { featureMajorHeadlinesPush } from '../../../lib/featureManagement';
+import { useToastNotification } from '../../../hooks/useToastNotification';
+
+const NOTIFICATION_SETTINGS_PATH = '/settings/notifications';
+
+interface HighlightCardOptionsProps {
+ className?: string;
+}
+
+const HighlightCardOptionsContent = ({
+ className,
+}: HighlightCardOptionsProps): ReactElement => {
+ const router = useRouter();
+ const [isPending, setIsPending] = useState(false);
+ const { displayToast } = useToastNotification();
+ const { isSubscribed, isLoading, subscribe, unsubscribe } =
+ useMajorHeadlinesSubscription();
+
+ const handleToggle = async () => {
+ if (isPending || isLoading) {
+ return;
+ }
+ setIsPending(true);
+ try {
+ if (isSubscribed) {
+ await unsubscribe('feed_card');
+ displayToast('Real-time alerts turned off.');
+ return;
+ }
+ await subscribe('feed_card');
+ displayToast("You'll be the first to know when news breaks.", {
+ action: {
+ copy: 'Settings',
+ onClick: () => router.push(NOTIFICATION_SETTINGS_PATH),
+ },
+ });
+ } finally {
+ setIsPending(false);
+ }
+ };
+
+ const label = isSubscribed
+ ? 'Turn off real-time alerts'
+ : 'Get real-time alerts';
+ const Icon = isSubscribed ? BellSubscribedIcon : BellAddIcon;
+
+ return (
+
+ }
+ className={classNames(
+ 'invisible my-auto group-hover:visible',
+ className,
+ )}
+ aria-label={label}
+ onClick={handleToggle}
+ disabled={isPending || isLoading}
+ />
+
+ );
+};
+
+export const HighlightCardOptions = ({
+ className,
+}: HighlightCardOptionsProps): ReactElement | null => {
+ const auth = useAuthContext();
+ const user = auth?.user;
+ const { value: isFeatureEnabled } = useConditionalFeature({
+ feature: featureMajorHeadlinesPush,
+ shouldEvaluate: !!user,
+ });
+
+ if (!isFeatureEnabled || !user) {
+ return null;
+ }
+
+ return ;
+};
+
+export default HighlightCardOptions;
diff --git a/packages/shared/src/components/cards/highlight/common.tsx b/packages/shared/src/components/cards/highlight/common.tsx
index ca3ec83c1e..c64a8552a1 100644
--- a/packages/shared/src/components/cards/highlight/common.tsx
+++ b/packages/shared/src/components/cards/highlight/common.tsx
@@ -5,6 +5,7 @@ import type { PostHighlight } from '../../../graphql/highlights';
import { webappUrl } from '../../../lib/constants';
import { RelativeTime } from '../../utilities/RelativeTime';
import Link from '../../utilities/Link';
+import { HighlightCardOptions } from './HighlightCardOptions';
export interface HighlightCardProps {
highlights: PostHighlight[];
@@ -117,6 +118,7 @@ export const HighlightCardContent = ({
>
Happening Now
+
{highlights.map((highlight, index) => (
diff --git a/packages/shared/src/components/highlights/EnableHighlightsAlerts.spec.tsx b/packages/shared/src/components/highlights/EnableHighlightsAlerts.spec.tsx
new file mode 100644
index 0000000000..76a28fa3f4
--- /dev/null
+++ b/packages/shared/src/components/highlights/EnableHighlightsAlerts.spec.tsx
@@ -0,0 +1,189 @@
+import React from 'react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { EnableHighlightsAlerts } from './EnableHighlightsAlerts';
+import { LogEvent } from '../../lib/log';
+import { ActionType } from '../../graphql/actions';
+
+const mockLogEvent = jest.fn();
+const mockSubscribe = jest.fn().mockResolvedValue(undefined);
+const mockCompleteAction = jest.fn().mockResolvedValue(undefined);
+const mockCheckHasCompleted = jest.fn();
+const mockDisplayToast = jest.fn();
+const mockUseAuth = jest.fn();
+const mockUseConditionalFeature = jest.fn();
+const mockUseMajorHeadlinesSubscription = jest.fn();
+const mockRouterPush = jest.fn();
+const mockUsePushNotificationContext = jest.fn();
+
+jest.mock('next/router', () => ({
+ useRouter: () => ({ push: mockRouterPush }),
+}));
+
+jest.mock('../../contexts/PushNotificationContext', () => ({
+ usePushNotificationContext: () => mockUsePushNotificationContext(),
+}));
+
+jest.mock('../../contexts/LogContext', () => ({
+ useLogContext: () => ({ logEvent: mockLogEvent }),
+}));
+
+jest.mock('../../contexts/AuthContext', () => ({
+ useAuthContext: () => mockUseAuth(),
+}));
+
+jest.mock('../../hooks/useActions', () => ({
+ useActions: () => ({
+ checkHasCompleted: mockCheckHasCompleted,
+ completeAction: mockCompleteAction,
+ isActionsFetched: true,
+ }),
+}));
+
+jest.mock('../../hooks/useConditionalFeature', () => ({
+ useConditionalFeature: () => mockUseConditionalFeature(),
+}));
+
+jest.mock('../../hooks/notifications/useMajorHeadlinesSubscription', () => ({
+ useMajorHeadlinesSubscription: () => mockUseMajorHeadlinesSubscription(),
+}));
+
+jest.mock('../../hooks/useToastNotification', () => ({
+ useToastNotification: () => ({ displayToast: mockDisplayToast }),
+}));
+
+const renderComponent = () => render(
);
+
+describe('EnableHighlightsAlerts', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseAuth.mockReturnValue({ user: { id: '1' } });
+ mockUseConditionalFeature.mockReturnValue({ value: true });
+ mockUseMajorHeadlinesSubscription.mockReturnValue({
+ isSubscribed: false,
+ isLoading: false,
+ subscribe: mockSubscribe,
+ unsubscribe: jest.fn(),
+ });
+ mockCheckHasCompleted.mockReturnValue(false);
+ mockUsePushNotificationContext.mockReturnValue({ isSubscribed: false });
+ });
+
+ it('should render banner when feature is on, user is logged in, not subscribed and not dismissed', () => {
+ renderComponent();
+
+ expect(screen.getByText('Never miss a major headline')).toBeInTheDocument();
+ expect(screen.getByText('Notify me')).toBeInTheDocument();
+ });
+
+ it('should not render when feature is off', () => {
+ mockUseConditionalFeature.mockReturnValue({ value: false });
+
+ renderComponent();
+
+ expect(
+ screen.queryByText('Never miss a major headline'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should not render for guests', () => {
+ mockUseAuth.mockReturnValue({ user: undefined });
+
+ renderComponent();
+
+ expect(
+ screen.queryByText('Never miss a major headline'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should not render when already subscribed', () => {
+ mockUseMajorHeadlinesSubscription.mockReturnValue({
+ isSubscribed: true,
+ isLoading: false,
+ subscribe: mockSubscribe,
+ unsubscribe: jest.fn(),
+ });
+
+ renderComponent();
+
+ expect(
+ screen.queryByText('Never miss a major headline'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should not render when dismissed', () => {
+ mockCheckHasCompleted.mockReturnValue(true);
+
+ renderComponent();
+
+ expect(
+ screen.queryByText('Never miss a major headline'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should log impression on render', () => {
+ renderComponent();
+
+ expect(mockLogEvent).toHaveBeenCalledWith({
+ event_name: LogEvent.ImpressionMajorHeadlinesAlertsBanner,
+ extra: JSON.stringify({ origin: 'highlights_page' }),
+ });
+ });
+
+ it('should subscribe and show toast with settings action on CTA click when push is not yet enabled', async () => {
+ renderComponent();
+
+ fireEvent.click(screen.getByText('Notify me'));
+
+ await waitFor(() => {
+ expect(mockSubscribe).toHaveBeenCalledWith('highlights_page');
+ });
+
+ await waitFor(() => {
+ expect(mockDisplayToast).toHaveBeenCalledWith(
+ "You'll be the first to know when news breaks.",
+ expect.objectContaining({
+ action: expect.objectContaining({ copy: 'Settings' }),
+ }),
+ );
+ });
+
+ const toastArgs = mockDisplayToast.mock.calls[0][1];
+ toastArgs.action.onClick();
+ expect(mockRouterPush).toHaveBeenCalledWith('/settings/notifications');
+ });
+
+ it('should show enabled state inline when push was already granted', async () => {
+ mockUsePushNotificationContext.mockReturnValue({ isSubscribed: true });
+
+ renderComponent();
+
+ fireEvent.click(screen.getByText('Notify me'));
+
+ await waitFor(() => {
+ expect(mockSubscribe).toHaveBeenCalledWith('highlights_page');
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("You're in the loop")).toBeInTheDocument();
+ });
+
+ expect(mockDisplayToast).not.toHaveBeenCalled();
+ });
+
+ it('should complete dismiss action and log dismiss on close', async () => {
+ renderComponent();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
+
+ expect(mockLogEvent).toHaveBeenCalledWith({
+ event_name: LogEvent.DismissMajorHeadlinesAlertsBanner,
+ extra: JSON.stringify({ origin: 'highlights_page' }),
+ });
+
+ await waitFor(() => {
+ expect(mockCompleteAction).toHaveBeenCalledWith(
+ ActionType.DismissedMajorHeadlinesAlertsBanner,
+ );
+ });
+ });
+});
diff --git a/packages/shared/src/components/highlights/EnableHighlightsAlerts.tsx b/packages/shared/src/components/highlights/EnableHighlightsAlerts.tsx
new file mode 100644
index 0000000000..4055f22693
--- /dev/null
+++ b/packages/shared/src/components/highlights/EnableHighlightsAlerts.tsx
@@ -0,0 +1,176 @@
+import type { ReactElement } from 'react';
+import React, { useCallback, useState } from 'react';
+import classNames from 'classnames';
+import { useRouter } from 'next/router';
+import {
+ Button,
+ ButtonColor,
+ ButtonSize,
+ ButtonVariant,
+} from '../buttons/Button';
+import CloseButton from '../CloseButton';
+import {
+ cloudinaryNotificationsBrowser,
+ cloudinaryNotificationsBrowserEnabled,
+} from '../../lib/image';
+import { VIcon } from '../icons';
+import { useAuthContext } from '../../contexts/AuthContext';
+import { useActions } from '../../hooks/useActions';
+import { ActionType } from '../../graphql/actions';
+import { useMajorHeadlinesSubscription } from '../../hooks/notifications/useMajorHeadlinesSubscription';
+import { useConditionalFeature } from '../../hooks/useConditionalFeature';
+import { featureMajorHeadlinesPush } from '../../lib/featureManagement';
+import { useLogContext } from '../../contexts/LogContext';
+import useLogEventOnce from '../../hooks/log/useLogEventOnce';
+import { LogEvent } from '../../lib/log';
+import { useToastNotification } from '../../hooks/useToastNotification';
+import { usePushNotificationContext } from '../../contexts/PushNotificationContext';
+
+interface EnableHighlightsAlertsProps {
+ className?: string;
+}
+
+const ORIGIN = 'highlights_page';
+const NOTIFICATION_SETTINGS_PATH = '/settings/notifications';
+
+export const EnableHighlightsAlerts = ({
+ className,
+}: EnableHighlightsAlertsProps): ReactElement | null => {
+ const router = useRouter();
+ const { user } = useAuthContext();
+ const { isSubscribed: isPushEnabled } = usePushNotificationContext();
+ const { checkHasCompleted, completeAction, isActionsFetched } = useActions();
+ const { logEvent } = useLogContext();
+ const { displayToast } = useToastNotification();
+ const { isSubscribed, isLoading, subscribe } =
+ useMajorHeadlinesSubscription();
+ const [isPending, setIsPending] = useState(false);
+ const [acceptedJustNow, setAcceptedJustNow] = useState(false);
+
+ const { value: isFeatureEnabled } = useConditionalFeature({
+ feature: featureMajorHeadlinesPush,
+ shouldEvaluate: !!user,
+ });
+
+ const isDismissed = checkHasCompleted(
+ ActionType.DismissedMajorHeadlinesAlertsBanner,
+ );
+
+ const shouldRender =
+ isFeatureEnabled &&
+ !!user &&
+ isActionsFetched &&
+ !isSubscribed &&
+ !isDismissed;
+
+ useLogEventOnce(
+ () => ({
+ event_name: LogEvent.ImpressionMajorHeadlinesAlertsBanner,
+ extra: JSON.stringify({ origin: ORIGIN }),
+ }),
+ { condition: shouldRender },
+ );
+
+ const handleEnable = useCallback(async () => {
+ if (isPending || isLoading) {
+ return;
+ }
+ setIsPending(true);
+ try {
+ await subscribe(ORIGIN);
+ if (isPushEnabled) {
+ setAcceptedJustNow(true);
+ return;
+ }
+ displayToast("You'll be the first to know when news breaks.", {
+ action: {
+ copy: 'Settings',
+ onClick: () => router.push(NOTIFICATION_SETTINGS_PATH),
+ },
+ });
+ } finally {
+ setIsPending(false);
+ }
+ }, [isPending, isLoading, subscribe, isPushEnabled, displayToast, router]);
+
+ const handleDismiss = useCallback(() => {
+ completeAction(ActionType.DismissedMajorHeadlinesAlertsBanner);
+ logEvent({
+ event_name: LogEvent.DismissMajorHeadlinesAlertsBanner,
+ extra: JSON.stringify({ origin: ORIGIN }),
+ });
+ }, [completeAction, logEvent]);
+
+ if (!shouldRender) {
+ return null;
+ }
+
+ return (
+
+
+ {acceptedJustNow && }
+ {acceptedJustNow ? "You're in the loop" : 'Never miss a major headline'}
+
+
+
+ {acceptedJustNow ? (
+ <>
+ You can change your{' '}
+ router.push(NOTIFICATION_SETTINGS_PATH)}
+ >
+ notification settings
+ {' '}
+ anytime.
+ >
+ ) : (
+ 'Be the first to know when something major happens in the developer world.'
+ )}
+
+
+
+
+ {!acceptedJustNow && (
+
+ Notify me
+
+ )}
+
+
+
+ );
+};
+
+export default EnableHighlightsAlerts;
diff --git a/packages/shared/src/components/highlights/HighlightsPage.tsx b/packages/shared/src/components/highlights/HighlightsPage.tsx
index 784d9d1cd6..f3616cfaa0 100644
--- a/packages/shared/src/components/highlights/HighlightsPage.tsx
+++ b/packages/shared/src/components/highlights/HighlightsPage.tsx
@@ -13,6 +13,7 @@ import {
import { Tab, TabContainer } from '../tabs/TabContainer';
import { DigestCTA } from './DigestCTA';
import { HighlightItem } from './HighlightItem';
+import { EnableHighlightsAlerts } from './EnableHighlightsAlerts';
const MAJOR_HEADLINES_LABEL = 'Headlines';
const SKELETON_COUNT = 5;
@@ -148,6 +149,7 @@ export const HighlightsPage = (): ReactElement => {
Happening Now
+
{
const { logEvent } = useLogContext();
@@ -47,12 +50,17 @@ const InAppNotificationsTab = (): ReactElement => {
const { isSubscribed, isInitialized, isPushSupported } =
usePushNotificationContext();
const { openModal } = useLazyModal();
+ const { user } = useAuthContext();
const {
notificationSettings: ns,
toggleSetting,
toggleGroup,
getGroupStatus,
} = useNotificationSettings();
+ const { value: isMajorHeadlinesEnabled } = useConditionalFeature({
+ feature: featureMajorHeadlinesPush,
+ shouldEvaluate: !!user,
+ });
const onTogglePush = async () => {
logEvent({
@@ -117,6 +125,30 @@ const InAppNotificationsTab = (): ReactElement => {
}
/>
+ {isMajorHeadlinesEnabled && (
+ <>
+
+
+ Happening Now
+
+
+
+ toggleSetting(NotificationType.MajorHeadlineAdded, 'inApp')
+ }
+ />
+
+
+
+ >
+ )}
Activity
diff --git a/packages/shared/src/components/notifications/utils.ts b/packages/shared/src/components/notifications/utils.ts
index cfc0bc6f08..13e1fb1bdf 100644
--- a/packages/shared/src/components/notifications/utils.ts
+++ b/packages/shared/src/components/notifications/utils.ts
@@ -94,6 +94,7 @@ export enum NotificationType {
NewOpportunityMatch = 'new_opportunity_match',
WarmIntro = 'warm_intro',
ExperienceCompanyEnriched = 'experience_company_enriched',
+ MajorHeadlineAdded = 'major_headline_added',
}
export enum NotificationIconType {
@@ -206,6 +207,7 @@ export const notificationTypeTheme: Partial> =
[NotificationType.BriefingReady]: 'text-brand-default',
[NotificationType.DigestReady]: 'text-brand-default',
[NotificationType.UserFollow]: 'text-brand-default',
+ [NotificationType.MajorHeadlineAdded]: 'text-brand-default',
};
export const notificationTypeNotClickable: Partial<
diff --git a/packages/shared/src/graphql/actions.ts b/packages/shared/src/graphql/actions.ts
index 2cddf5b0a5..86d600cf86 100644
--- a/packages/shared/src/graphql/actions.ts
+++ b/packages/shared/src/graphql/actions.ts
@@ -63,6 +63,7 @@ export enum ActionType {
DismissBriefCard = 'dismiss_brief_card',
DigestUpsell = 'digest_upsell',
AskUpsellSearch = 'ask_upsell_search',
+ DismissedMajorHeadlinesAlertsBanner = 'dismissed_major_headlines_alerts_banner',
}
export const cvActions = [
diff --git a/packages/shared/src/hooks/notifications/useMajorHeadlinesSubscription.ts b/packages/shared/src/hooks/notifications/useMajorHeadlinesSubscription.ts
new file mode 100644
index 0000000000..611a0d2034
--- /dev/null
+++ b/packages/shared/src/hooks/notifications/useMajorHeadlinesSubscription.ts
@@ -0,0 +1,100 @@
+import { useCallback } from 'react';
+import { NotificationType } from '../../components/notifications/utils';
+import { NotificationPreferenceStatus } from '../../graphql/notifications';
+import useNotificationSettings from './useNotificationSettings';
+import { usePushNotificationMutation } from './usePushNotificationMutation';
+import { usePushNotificationContext } from '../../contexts/PushNotificationContext';
+import { useAuthContext } from '../../contexts/AuthContext';
+import { useLogContext } from '../../contexts/LogContext';
+import { LogEvent, NotificationPromptSource } from '../../lib/log';
+
+type MajorHeadlinesOrigin = 'settings' | 'highlights_page' | 'feed_card';
+
+type UseMajorHeadlinesSubscriptionResult = {
+ isSubscribed: boolean;
+ isPushEnabled: boolean;
+ isLoading: boolean;
+ subscribe: (origin: MajorHeadlinesOrigin) => Promise;
+ unsubscribe: (origin: MajorHeadlinesOrigin) => Promise;
+};
+
+const ORIGIN_TO_PROMPT_SOURCE: Record<
+ MajorHeadlinesOrigin,
+ NotificationPromptSource
+> = {
+ settings: NotificationPromptSource.MajorHeadlinesSettings,
+ highlights_page: NotificationPromptSource.MajorHeadlinesPage,
+ feed_card: NotificationPromptSource.MajorHeadlinesCard,
+};
+
+export const useMajorHeadlinesSubscription =
+ (): UseMajorHeadlinesSubscriptionResult => {
+ const { user } = useAuthContext();
+ const { logEvent } = useLogContext();
+ const { isSubscribed: isPushEnabled } = usePushNotificationContext();
+ const { onEnablePush } = usePushNotificationMutation();
+ const {
+ notificationSettings,
+ isLoadingPreferences,
+ setNotificationStatusBulk,
+ } = useNotificationSettings();
+
+ const settings =
+ notificationSettings?.[NotificationType.MajorHeadlineAdded];
+ const isSubscribed =
+ settings?.inApp === NotificationPreferenceStatus.Subscribed;
+
+ const subscribe = useCallback(
+ async (origin: MajorHeadlinesOrigin) => {
+ if (!user) {
+ return;
+ }
+
+ await onEnablePush(ORIGIN_TO_PROMPT_SOURCE[origin]);
+
+ setNotificationStatusBulk([
+ {
+ type: NotificationType.MajorHeadlineAdded,
+ channel: 'inApp',
+ status: NotificationPreferenceStatus.Subscribed,
+ },
+ ]);
+
+ logEvent({
+ event_name: LogEvent.EnableMajorHeadlinesAlerts,
+ extra: JSON.stringify({ origin }),
+ });
+ },
+ [user, onEnablePush, setNotificationStatusBulk, logEvent],
+ );
+
+ const unsubscribe = useCallback(
+ async (origin: MajorHeadlinesOrigin) => {
+ if (!user) {
+ return;
+ }
+
+ setNotificationStatusBulk([
+ {
+ type: NotificationType.MajorHeadlineAdded,
+ channel: 'inApp',
+ status: NotificationPreferenceStatus.Muted,
+ },
+ ]);
+
+ logEvent({
+ event_name: LogEvent.DisableMajorHeadlinesAlerts,
+ extra: JSON.stringify({ origin }),
+ });
+ },
+ [user, setNotificationStatusBulk, logEvent],
+ );
+
+ return {
+ isSubscribed,
+ isPushEnabled,
+ isLoading: isLoadingPreferences,
+ subscribe,
+ unsubscribe,
+ };
+ };
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index 5ef53dfa2a..c5d8199a1e 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -33,6 +33,10 @@ export const discussedFeedVersion = new Feature('discussed_feed_version', 2);
export const latestFeedVersion = new Feature('latest_feed_version', 2);
export const customFeedVersion = new Feature('custom_feed_version', 2);
export const featureFeedV2Highlights = new Feature('feed_v2_highlights', false);
+export const featureMajorHeadlinesPush = new Feature(
+ 'major_headlines_push',
+ false,
+);
export const featurePostPageHighlights = new Feature(
'post_page_highlights',
false,
diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts
index f313fc4e87..2a392afef6 100644
--- a/packages/shared/src/lib/log.ts
+++ b/packages/shared/src/lib/log.ts
@@ -128,6 +128,10 @@ export enum LogEvent {
EnableNotification = 'enable notification',
DisableNotification = 'disable notification',
ScheduleDigest = 'schedule digest',
+ EnableMajorHeadlinesAlerts = 'enable major headlines alerts',
+ DisableMajorHeadlinesAlerts = 'disable major headlines alerts',
+ ImpressionMajorHeadlinesAlertsBanner = 'impression major headlines alerts banner',
+ DismissMajorHeadlinesAlertsBanner = 'dismiss major headlines alerts banner',
// notifications - end
// squads - start
ViewSquadInvitation = 'view squad invitation',
@@ -624,6 +628,9 @@ export enum NotificationPromptSource {
SquadChecklist = 'squad checklist',
SourceSubscribe = 'source subscribe',
ReadingReminder = 'reading reminder',
+ MajorHeadlinesSettings = 'major headlines settings',
+ MajorHeadlinesPage = 'major headlines page',
+ MajorHeadlinesCard = 'major headlines card',
}
export enum ShortcutsSourceType {