diff --git a/locales-pending/settings-premium.ftl b/locales-pending/settings-premium.ftl index db666e8ceb4..ecf60f9a6ba 100644 --- a/locales-pending/settings-premium.ftl +++ b/locales-pending/settings-premium.ftl @@ -2,11 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -## Email preferences - -settings-alert-preferences-allow-monthly-monitor-plus-report-title = Monthly { -brand-monitor-plus } report -settings-alert-preferences-allow-monthly-monitor-plus-report-subtitle = A monthly update of new exposures, what’s been fixed, and what needs your attention. - ## Cancel Plus subscription settings-cancel-plus-title = Cancel { -brand-monitor-plus } subscription @@ -59,3 +54,7 @@ settings-delete-monitor-plus-account-dialog-lead-p1-2 = All of your { -brand-mon settings-delete-monitor-plus-account-dialog-lead-p2-2 = You’ll regain access to { -brand-monitor-plus } features if you sign back in during any remaining time of your paid subscription. settings-delete-monitor-plus-account-dialog-cta-label = Delete account settings-delete-monitor-plus-account-dialog-cancel-button-label = Never mind, take me back + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-plus-report-title = Monthly { -brand-monitor-plus } report diff --git a/locales/en/settings.ftl b/locales/en/settings.ftl index 0433c420f0f..efaec1b6780 100644 --- a/locales/en/settings.ftl +++ b/locales/en/settings.ftl @@ -58,3 +58,8 @@ settings-delete-monitor-free-account-dialog-cta-label = Delete account settings-delete-monitor-free-account-dialog-cancel-button-label = Never mind, take me back settings-delete-monitor-account-confirmation-toast-label-2 = Your { -brand-monitor } account is now deleted. settings-delete-monitor-account-confirmation-toast-dismiss-label = Dismiss + +## Monthly Monitor Report + +settings-alert-preferences-allow-monthly-monitor-report-title = Monthly { -brand-monitor } report +settings-alert-preferences-allow-monthly-monitor-report-subtitle = A monthly update of new exposures, what’s been fixed, and what needs your attention. diff --git a/locales/en/unsubscribe.ftl b/locales/en/unsubscribe.ftl new file mode 100644 index 00000000000..ab8be5db662 --- /dev/null +++ b/locales/en/unsubscribe.ftl @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Confirm Unsubscription State + +unsubscribe-from-monthly-report-header = Unsubscribe from this email? +unsubscribe-from-monthly-report-body = You’ll no longer receive the monthly { -brand-monitor } report, which tells you how many new exposures you’ve had each month and how many are fixed. +unsubscribe-cta = Unsubscribe + +# Success Unsubscription State + +unsubscribe-success-from-monthly-report-header = You’re now unsubscribed +unsubscribe-success-from-monthly-report-body = You can resubscribe or update your email preferences anytime from your { -brand-monitor } settings. + +# Error warning + +unsubscription-failed = Unsubscribe failed. Try again. diff --git a/package-lock.json b/package-lock.json index 73ef93d5848..d54b11aa1ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "react-dom": "^18.3.1", "react-intersection-observer": "^9.13.0", "react-stately": "^3.32.2", + "react-toastify": "^10.0.5", "server-only": "^0.0.1", "uuid": "^10.0.0", "winston": "^3.14.2" @@ -23503,6 +23504,19 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index 3804ca8b36b..1eab99b6850 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "react-dom": "^18.3.1", "react-intersection-observer": "^9.13.0", "react-stately": "^3.32.2", + "react-toastify": "^10.0.5", "server-only": "^0.0.1", "uuid": "^10.0.0", "winston": "^3.14.2" diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/AlertAddressForm.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/AlertAddressForm.tsx index b4f0a6074b6..484d16ca545 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/AlertAddressForm.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/AlertAddressForm.tsx @@ -29,13 +29,15 @@ import { VisuallyHidden } from "../../../../../../components/server/VisuallyHidd import { useSession } from "next-auth/react"; import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags"; import { Session } from "next-auth"; -import { hasPremium } from "../../../../../../functions/universal/user"; import { useRouter } from "next/navigation"; import { SubscriberRow } from "knex/types/tables"; +import { SubscriberEmailPreferencesOutput } from "../../../../../../../db/tables/subscriber_email_preferences"; +import { hasPremium } from "../../../../../../functions/universal/user"; export type Props = { user: Session["user"]; subscriber: SubscriberRow; + data: SubscriberEmailPreferencesOutput; enabledFeatureFlags: FeatureFlagName[]; }; @@ -48,7 +50,15 @@ export const AlertAddressForm = (props: Props) => { const breachAlertsEmailsAllowed = props.subscriber.all_emails_to_primary; - const monitorReportAllowed = props.subscriber.monthly_monitor_report; + // Extract monthly report preference from the right column + const monitorReportAllowed = hasPremium(props.user) + ? props.data.monthly_monitor_report + : props.data.monthly_monitor_report_free; + + // TODO: Deprecate this when monthly report for free users has been created + const monthlyFreeUserReportEnabled = + props.enabledFeatureFlags.includes("MonthlyReportFreeUser") || + hasPremium(props.user); const defaultActivateAlertEmail = typeof breachAlertsEmailsAllowed === "boolean"; @@ -175,22 +185,25 @@ export const AlertAddressForm = (props: Props) => { )} - {props.enabledFeatureFlags.includes("MonthlyActivityEmail") && - hasPremium(props.user) && ( + monthlyFreeUserReportEnabled && (
- {l10n.getString( - "settings-alert-preferences-allow-monthly-monitor-plus-report-title", - )} + {hasPremium(props.user) + ? l10n.getString( + "settings-alert-preferences-allow-monthly-monitor-plus-report-title", + ) + : l10n.getString( + "settings-alert-preferences-allow-monthly-monitor-report-title", + )}

{l10n.getString( - "settings-alert-preferences-allow-monthly-monitor-plus-report-subtitle", + "settings-alert-preferences-allow-monthly-monitor-report-subtitle", )}

diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsPage.test.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsPage.test.tsx index 89d9d21842e..2db9a5390a1 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsPage.test.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsPage.test.tsx @@ -57,6 +57,7 @@ jest.mock("next/navigation", () => ({ import { SettingsView } from "./View"; import { sanitizeEmailRow } from "../../../../../../functions/server/sanitize"; import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments"; +import { SubscriberEmailPreferencesOutput } from "../../../../../../../db/tables/subscriber_email_preferences"; const subscriberId = 7; const mockedSerializedSubscriber: SerializedSubscriber = { @@ -151,6 +152,19 @@ const mockedUser: Session["user"] = { }, }; +const mockedFreeUser: Session["user"] = { + email: "primary@example.com", + subscriber: undefined, + fxa: { + subscriptions: [], + avatar: "", + avatarDefault: false, + locale: "en-GB", + metricsEnabled: false, + twoFactorAuthentication: false, + }, +}; + const mockedSecondaryVerifiedEmail: EmailAddressRow = { id: 1337, email: "secondary_verified@example.com", @@ -207,11 +221,31 @@ const mockedSubscriptionBillingAmount = { yearly: 13.37, monthly: 42.42, }; +const mockedPlusSubscriberEmailPreferences: SubscriberEmailPreferencesOutput = { + id: 1337, + primary_email: "primary@example.com", + unsubscribe_token: "495398jfjvjfdj", + monthly_monitor_report_free: false, + monthly_monitor_report_free_at: new Date("1337-04-02T04:02:42.000Z"), + monthly_monitor_report: true, + monthly_monitor_report_at: new Date("1337-04-02T04:02:42.000Z"), +}; + +const mockedFreeSubscriberEmailPreferences: SubscriberEmailPreferencesOutput = { + id: 1337, + primary_email: "primary@example.com", + unsubscribe_token: "495398jfjvjfdj", + monthly_monitor_report_free: true, + monthly_monitor_report_free_at: new Date("1337-04-02T04:02:42.000Z"), + monthly_monitor_report: false, + monthly_monitor_report_at: new Date("1337-04-02T04:02:42.000Z"), +}; it("passes the axe accessibility audit", async () => { const { container } = render( , ); @@ -317,6 +352,7 @@ it("Add email address button is shown when fewer than five emails", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -353,6 +389,7 @@ it("preselects 'Send all breach alerts to the primary email address' if that's t enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -397,6 +434,7 @@ it("preselects 'Send breach alerts to the affected email address' if that's the enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -441,6 +479,7 @@ it("disables breach alert notification options if a user opts out of breach aler enabledFeatureFlags={["UpdatedEmailPreferencesOption"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -495,6 +534,7 @@ it("preselects primary email alert option", () => { enabledFeatureFlags={["UpdatedEmailPreferencesOption"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -535,6 +575,7 @@ it("unselects the breach alerts checkbox and sends a null value to the API", asy enabledFeatureFlags={["UpdatedEmailPreferencesOption"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -591,6 +632,7 @@ it("preselects the affected email comms option after a user decides to enable br enabledFeatureFlags={["UpdatedEmailPreferencesOption"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -638,6 +680,7 @@ it("sends a call to the API to change the email alert preferences when changing enabledFeatureFlags={["UpdatedEmailPreferencesOption"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -665,6 +708,46 @@ it("sends a call to the API to change the email alert preferences when changing }); }); +it("checks that monthly monitor report is available to free users", () => { + render( + + + , + ); + + const monthlyMonitorReportBtn = screen.getByLabelText( + "Monthly ⁨Monitor⁩ report", + { exact: false }, + ); + expect(monthlyMonitorReportBtn).toHaveAttribute("aria-checked", "true"); +}); + it("checks that monthly monitor report is enabled", () => { render( @@ -698,6 +781,7 @@ it("checks that monthly monitor report is enabled", () => { ]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -745,6 +829,7 @@ it("sends an API call to disable monthly monitor reports", async () => { ]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -791,6 +876,7 @@ it("refreshes the session token after changing email alert preferences, to ensur enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -827,6 +913,7 @@ it("marks unverified email addresses as such", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -864,6 +951,7 @@ it("calls the API to resend a verification email if requested to", async () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -911,6 +999,7 @@ it("calls the 'remove' action when clicking the rubbish bin icon", async () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -948,6 +1037,7 @@ it("hides the Plus cancellation link if the user doesn't have Plus", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -984,6 +1074,7 @@ it("shows the Plus cancellation link if the user has Plus", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1027,6 +1118,7 @@ it("takes you through the cancellation dialog flow all the way to subplat", asyn enabledFeatureFlags={["ConfirmCancellation", "CancellationFlow"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1105,6 +1197,7 @@ it("closes the cancellation survey if the user selects nevermind, take me back", enabledFeatureFlags={["ConfirmCancellation", "CancellationFlow"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1162,6 +1255,7 @@ it("closes the cancellation dialog", async () => { enabledFeatureFlags={["CancellationFlow"]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1212,6 +1306,7 @@ it("shows the account deletion button if the user does not have Plus", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1253,6 +1348,7 @@ it("warns about the consequences before deleting a free user's account", async ( enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1296,6 +1392,7 @@ it("shows a loading state while account deletion is in progress", async () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1340,6 +1437,7 @@ it("shows the account deletion button if the user has Plus", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1381,6 +1479,7 @@ it("warns about the consequences before deleting a Plus user's account", async ( enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1433,6 +1532,7 @@ it.skip("calls the 'add' action when adding another email address", async () => enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1468,6 +1568,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1512,6 +1613,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1556,6 +1658,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1601,6 +1704,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1645,6 +1749,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1694,6 +1799,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1739,6 +1845,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1788,6 +1895,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1832,6 +1940,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1881,6 +1990,7 @@ describe("to learn about usage", () => { enabledFeatureFlags={[]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -1942,6 +2052,7 @@ it("selects the coupon code discount cta and shows the all-set dialog step", asy ]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -2028,6 +2139,7 @@ it("shows error message if the applying the coupon code function was unsuccessfu ]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); @@ -2094,6 +2206,7 @@ it("does not show the coupon code if a user already has a coupon set", async () ]} experimentData={defaultExperimentData} isMonthlySubscriber={true} + data={mockedPlusSubscriberEmailPreferences} /> , ); diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx index bcec42408bc..cbf66ee5a1c 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx @@ -22,11 +22,13 @@ import { DeleteAccountButton } from "./DeleteAccountButton"; import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags"; import { CancelFlow } from "./CancelFlow"; import { ExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments"; +import { SubscriberEmailPreferencesOutput } from "../../../../../../../db/tables/subscriber_email_preferences"; export type Props = { l10n: ExtendedReactLocalization; user: Session["user"]; subscriber: SubscriberRow; + data: SubscriberEmailPreferencesOutput; monthlySubscriptionUrl: string; yearlySubscriptionUrl: string; subscriptionBillingAmount: { @@ -97,6 +99,7 @@ export const SettingsView = (props: Props) => { {hasPremium(props.user) && ( diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx index fccb13f5482..5b7c149fd53 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx @@ -25,7 +25,7 @@ import { getCountryCode } from "../../../../../../functions/server/getCountryCod import { getSubscriberById } from "../../../../../../../db/tables/subscribers"; import { checkSession } from "../../../../../../functions/server/checkSession"; import { checkUserHasMonthlySubscription } from "../../../../../../functions/server/user"; - +import { getEmailPreferenceForPrimaryEmail } from "../../../../../../../db/tables/subscriber_email_preferences"; type Props = { searchParams: { nimbus_web_preview?: string; @@ -34,7 +34,6 @@ type Props = { export default async function SettingsPage({ searchParams }: Props) { const session = await getServerSession(); - console.debug(searchParams); if (!session?.user?.subscriber?.id || !checkSession(session)) { return redirect("/auth/logout"); @@ -92,11 +91,16 @@ export default async function SettingsPage({ searchParams }: Props) { return redirect("/"); } + const settingsData = await getEmailPreferenceForPrimaryEmail( + session.user.email, + ); + return ( { return (
+