MNTOR-3556: Front end integration (#4995)

* feat: some example code for FE

* extract data from the right table

* Unsubscription from monthly report confirmation page (#4998)

* fix: gurantee boolean value

* fix: update should upsert too

* fix: delete primary_email field

* fix: remove dup primary_email from new table

* fix: generate the link for the unsub page instead

* feat: some example code for FE

* extract data from the right table

* Unsubscription from monthly report confirmation page

* pass in the right function

* fix unit tests

* fix unit tests

---------

Co-authored-by: Joey Zhou <jozhou@mozilla.com>

* pass subscriber data

* return void

* add toast

* correct api endpoint

* remove debugging comments

* add try again link

* add feature flag

* rename file

* override css variables

* wrap in try catch

* add loader

* fixup

* add toat if catch error

* remove original css override styles

* move to /unsubscribe-email folder

* lint

* add try again styling

* use plus language for header

* unit test

---------

Co-authored-by: Kaitlyn <kandres@mozilla.com>
This commit is contained in:
mansaj 2024-09-13 08:19:20 -07:00 коммит произвёл GitHub
Родитель b0d848befd
Коммит 3e6611491f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
19 изменённых файлов: 369 добавлений и 17 удалений

Просмотреть файл

@ -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, whats 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 = Youll 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

Просмотреть файл

@ -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, whats been fixed, and what needs your attention.

Просмотреть файл

@ -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 = Youll no longer receive the monthly { -brand-monitor } report, which tells you how many new exposures youve had each month and how many are fixed.
unsubscribe-cta = Unsubscribe
# Success Unsubscription State
unsubscribe-success-from-monthly-report-header = Youre 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_link>Try again.</try_again_link>

14
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",

Просмотреть файл

@ -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"

Просмотреть файл

@ -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) => {
</AlertAddressRadio>
</AlertAddressContext.Provider>
)}
{props.enabledFeatureFlags.includes("MonthlyActivityEmail") &&
hasPremium(props.user) && (
monthlyFreeUserReportEnabled && (
<ActivateEmailsCheckbox
isSelected={activateMonthlyMonitorReport}
onChange={handleMonthlyMonitorReportToggle}
>
<div>
<b>
{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",
)}
</b>
<p>
{l10n.getString(
"settings-alert-preferences-allow-monthly-monitor-plus-report-subtitle",
"settings-alert-preferences-allow-monthly-monitor-report-subtitle",
)}
</p>
</div>

Просмотреть файл

@ -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(
<TestComponentWrapper>
<SettingsView
data={mockedPlusSubscriberEmailPreferences}
l10n={getL10n()}
user={mockedUser}
breachCountByEmailAddress={{
@ -273,6 +307,7 @@ it("Add email address button is not shown when email limit of five reached", ()
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -317,6 +352,7 @@ it("Add email address button is shown when fewer than five emails", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -495,6 +534,7 @@ it("preselects primary email alert option", () => {
enabledFeatureFlags={["UpdatedEmailPreferencesOption"]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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(
<TestComponentWrapper>
<SettingsView
l10n={getL10n()}
user={{
...mockedFreeUser,
}}
subscriber={{
...mockedSubscriber,
}}
breachCountByEmailAddress={{
[mockedUser.email]: 42,
[mockedSecondaryVerifiedEmail.email]: 42,
}}
emailAddresses={[mockedSecondaryVerifiedEmail]}
fxaSettingsUrl=""
fxaSubscriptionsUrl=""
yearlySubscriptionUrl=""
monthlySubscriptionUrl=""
subscriptionBillingAmount={mockedSubscriptionBillingAmount}
enabledFeatureFlags={[
"UpdatedEmailPreferencesOption",
"MonthlyActivityEmail",
"MonthlyReportFreeUser",
]}
experimentData={defaultExperimentData}
isMonthlySubscriber={false}
data={mockedFreeSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
const monthlyMonitorReportBtn = screen.getByLabelText(
"Monthly Monitor report",
{ exact: false },
);
expect(monthlyMonitorReportBtn).toHaveAttribute("aria-checked", "true");
});
it("checks that monthly monitor report is enabled", () => {
render(
<TestComponentWrapper>
@ -698,6 +781,7 @@ it("checks that monthly monitor report is enabled", () => {
]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -745,6 +829,7 @@ it("sends an API call to disable monthly monitor reports", async () => {
]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -791,6 +876,7 @@ it("refreshes the session token after changing email alert preferences, to ensur
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -827,6 +913,7 @@ it("marks unverified email addresses as such", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -864,6 +951,7 @@ it("calls the API to resend a verification email if requested to", async () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -911,6 +999,7 @@ it("calls the 'remove' action when clicking the rubbish bin icon", async () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -948,6 +1037,7 @@ it("hides the Plus cancellation link if the user doesn't have Plus", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -984,6 +1074,7 @@ it("shows the Plus cancellation link if the user has Plus", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -1162,6 +1255,7 @@ it("closes the cancellation dialog", async () => {
enabledFeatureFlags={["CancellationFlow"]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1212,6 +1306,7 @@ it("shows the account deletion button if the user does not have Plus", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1253,6 +1348,7 @@ it("warns about the consequences before deleting a free user's account", async (
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1296,6 +1392,7 @@ it("shows a loading state while account deletion is in progress", async () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1340,6 +1437,7 @@ it("shows the account deletion button if the user has Plus", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1381,6 +1479,7 @@ it("warns about the consequences before deleting a Plus user's account", async (
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1433,6 +1532,7 @@ it.skip("calls the 'add' action when adding another email address", async () =>
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1468,6 +1568,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1512,6 +1613,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1556,6 +1658,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1601,6 +1704,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1645,6 +1749,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1694,6 +1799,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1739,6 +1845,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1788,6 +1895,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1832,6 +1940,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -1881,6 +1990,7 @@ describe("to learn about usage", () => {
enabledFeatureFlags={[]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);
@ -2028,6 +2139,7 @@ it("shows error message if the applying the coupon code function was unsuccessfu
]}
experimentData={defaultExperimentData}
isMonthlySubscriber={true}
data={mockedPlusSubscriberEmailPreferences}
/>
</TestComponentWrapper>,
);
@ -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}
/>
</TestComponentWrapper>,
);

Просмотреть файл

@ -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) => {
<AlertAddressForm
user={props.user}
subscriber={props.subscriber}
data={props.data}
enabledFeatureFlags={props.enabledFeatureFlags}
/>
{hasPremium(props.user) && (

Просмотреть файл

@ -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 (
<SettingsView
l10n={getL10n()}
user={session.user}
subscriber={userData}
data={settingsData}
emailAddresses={emailAddresses}
breachCountByEmailAddress={breachCountByEmailAddress}
fxaSettingsUrl={fxaSettingsUrl}

Просмотреть файл

@ -4,6 +4,22 @@
display: flex;
flex-direction: column;
height: 100%;
// See https://fkhadra.github.io/react-toastify/how-to-style#override-css-variabless
--toastify-color-light: #{$color-white};
--toastify-color-dark: #{$color-grey-50};
--toastify-color-info: #{$color-blue-50};
--toastify-color-success: #{$color-green-50};
--toastify-color-warning: #{$color-yellow-50};
--toastify-color-error: #{$color-red-60};
--toastify-text-color-info: #{$color-white};
--toastify-text-color-success: #{$color-grey-50};
--toastify-text-color-warning: #{$color-grey-50};
--toastify-text-color-error: #{$color-white};
--toastify-toast-min-height: $layout-md;
--toastify-toast-max-height: $layout-md;
}
.nav {

Просмотреть файл

@ -10,6 +10,8 @@ import MonitorLogo from "../../images/monitor-logo.svg";
import { ExtendedReactLocalization } from "../../../functions/l10n";
import { SignInButton } from "../../../components/client/SignInButton";
import { Footer } from "../Footer";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export type Props = {
children: ReactNode;
@ -20,6 +22,12 @@ export type Props = {
export const PublicShell = (props: Props) => {
return (
<div className={styles.wrapper}>
<ToastContainer
toastClassName={styles.toastBody}
position="top-center"
theme="colored"
autoClose={false}
/>
<header>
<nav className={styles.nav}>
<h1>

Просмотреть файл

@ -0,0 +1,35 @@
@import "../../../../../tokens";
.unSubscribeMonthlyReportContainer {
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-lg $spacing-sm;
gap: $spacing-lg;
text-align: center;
@media screen and (min-width: $screen-md) {
max-width: $content-lg;
}
h1 {
font: $text-title-md;
}
.cta {
max-width: $content-xs;
}
}
.tryAgain {
color: $color-white;
font-weight: 600;
text-decoration: underline;
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}

Просмотреть файл

@ -0,0 +1,87 @@
/* 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/. */
"use client";
import { useState } from "react";
import { Button } from "../../../../../components/client/Button";
import styles from "./UnsubscribeMonthlyReport.module.scss";
import UnsubscriptionImage from "./images/confirm-unsubscribe.svg";
import Image from "next/image";
import { useL10n } from "../../../../../hooks/l10n";
import { toast } from "react-toastify";
export const UnsubscribeMonthlyReportView = ({ token }: { token: string }) => {
const [unsubscribeSuccess, setUnsubscribeSuccess] = useState(false);
const l10n = useL10n();
const copy = {
confirmation: {
header: l10n.getString("unsubscribe-from-monthly-report-header"),
body: l10n.getString("unsubscribe-from-monthly-report-body"),
},
success: {
header: l10n.getString("unsubscribe-success-from-monthly-report-header"),
body: l10n.getString("unsubscribe-success-from-monthly-report-body"),
},
};
const { header, body } = unsubscribeSuccess
? copy.success
: copy.confirmation;
const handleUnsubscription = async () => {
try {
const response = await fetch(`/api/unsubscribe-email?token=${token}`, {
method: "GET",
});
if (!response.ok) {
toast.error(
l10n.getFragment("unsubscription-failed", {
elems: {
try_again_link: (
<button
className={styles.tryAgain}
onClick={() => void handleUnsubscription()}
/>
),
},
}),
);
} else {
setUnsubscribeSuccess(true);
}
} catch (error) {
toast.error(
l10n.getFragment("unsubscription-failed", {
elems: {
try_again_link: (
<button
className={styles.tryAgain}
onClick={() => void handleUnsubscription()}
/>
),
},
}),
);
console.error("Error unsubscribing from Monthly monitor report", error);
}
};
return (
<main className={styles.unSubscribeMonthlyReportContainer}>
<Image src={UnsubscriptionImage} alt="" />
<h1>{header}</h1>
<p>{body}</p>
<Button
className={styles.cta}
variant="primary"
onPress={() => void handleUnsubscription()}
>
{l10n.getString("unsubscribe-cta")}
</Button>
</main>
);
};

Просмотреть файл

@ -0,0 +1,15 @@
<svg width="584" height="129" viewBox="0 0 584 129" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25443 128C0.951567 126.047 -0.243336 123.744 1.25443 122C2.13957 120.969 3.18053 121.264 4.25443 122C5.32851 122.735 6.53944 123.797 7.25443 125C6.10696 122.221 4.33591 119.093 4.25443 116C4.17296 112.906 9.56838 101.098 26.2544 114C29.8711 116.796 32.3969 125.48 45.2544 128C44.8571 128 45.6518 129 45.2544 129L2.25443 128Z" fill="#C9EFFD"/>
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M440.558 87.9991C442.882 84.3797 445.229 79.2329 442.558 75.9991C440.98 74.088 438.473 73.6352 436.558 74.9991C434.643 76.363 432.833 78.7695 431.558 80.9991C433.604 75.8473 435.413 70.7342 435.558 64.9991C435.703 59.2641 427.316 37.0809 397.558 60.9991C391.108 66.1834 385.488 83.3281 362.558 87.9991C363.267 87.9991 361.849 88.9991 362.558 88.9991L440.558 87.9991Z" fill="#C9EFFD"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M157.558 106.999C155.235 103.38 152.887 98.2329 155.558 94.9991C157.137 93.088 159.643 92.6352 161.558 93.9991C163.474 95.363 165.283 97.7695 166.558 99.9991C164.512 94.8473 162.703 89.7342 162.558 83.9991C162.413 78.2641 170.8 56.0809 200.558 79.9991C207.008 85.1834 212.628 102.328 235.558 106.999C234.849 106.999 236.267 107.999 235.558 107.999L157.558 106.999Z" fill="#C9EFFD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M581.559 120C583.115 117.61 584.348 114.135 582.559 112C581.502 110.738 579.842 111.099 578.559 112C577.276 112.901 576.413 114.528 575.559 116C576.929 112.598 578.462 108.787 578.559 105C578.656 101.213 572.488 86.2052 552.559 102C548.239 105.423 544.915 116.915 529.559 120C530.033 120 529.084 121 529.559 121L581.559 120Z" fill="#C9EFFD"/>
<mask id="mask0_247_3049" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="223" y="23" width="150" height="83">
<path fill-rule="evenodd" clip-rule="evenodd" d="M223.559 23H372.559V106H223.559V23Z" fill="white"/>
</mask>
<g mask="url(#mask0_247_3049)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M362.559 106H233.559C228.457 106 223.559 101.111 223.559 96V33C223.559 27.8889 228.457 23 233.559 23H362.559C367.66 23 372.559 27.8889 372.559 33V96C372.559 101.111 367.66 106 362.559 106Z" fill="#F770FF"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M300.558 67.0003C295.543 67.0003 290.983 65.895 286.558 63.0003L226.558 24.0003C226.317 23.8424 226.402 23.2444 226.558 23.0003C226.714 22.7559 227.317 22.8421 227.558 23.0003L286.558 62.0003C295.316 67.7305 306 67.0292 314.558 61.0003L367.558 24.0003C367.794 23.8339 368.393 23.7615 368.558 24.0003C368.723 24.2388 368.794 24.8339 368.558 25.0003L314.558 62.0003C309.975 65.2288 305.873 67.0003 300.558 67.0003Z" fill="#26154D"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.7 KiB

Просмотреть файл

@ -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/. */
import { UnsubscribeMonthlyReportView } from "./UnsubscribeMonthlyReportView";
export default function Page({
searchParams,
}: {
searchParams: { token?: string };
}) {
const token = searchParams.token ?? "";
if (!token) {
console.error("Unsubscription token not provided");
}
return <UnsubscribeMonthlyReportView token={token} />;
}

Просмотреть файл

@ -71,7 +71,7 @@ export async function generateUnsubscribeLinkForSubscriber(
},
["unsubscribe_token"],
);
return `${process.env.SERVER_URL}/unsubscribe-from-monthly-report?token=${sub.unsubscribe_token}`;
return `${process.env.SERVER_URL}/unsubscribe-email/monthly-report-free?token=${sub.unsubscribe_token}`;
} catch (e) {
console.error("generate_unsubscribe_link", {
exception: e as string,

Просмотреть файл

@ -31,12 +31,14 @@ export async function POST(req: NextRequest) {
monthlyMonitorReport,
}: EmailUpdateCommOptionRequest = await req.json();
const subscriber = await getSubscriberByFxaUid(token.subscriber?.fxa_uid);
if (!subscriber) {
throw new Error("No subscriber found for current session.");
}
// "null" = Do not send instant notifications. Newly added in MNTOR-1368
// "affected" = Send breach alerts to the corresponding affected emails.
// "primary" = Send all breach alerts to user's primary email address.
let allEmailsToPrimary;
switch (instantBreachAlerts) {
case "primary":

Просмотреть файл

@ -48,6 +48,7 @@ export const featureFlagNames = [
"AutomaticRemovalCsatSurvey",
"AdditionalRemovalStatuses",
"PetitionBannerCsatSurvey",
"MonthlyReportFreeUser",
] as const;
export type FeatureFlagName = (typeof featureFlagNames)[number];

Просмотреть файл

@ -21,7 +21,7 @@ interface SubscriberPlusEmailPreferencesInput {
monthly_monitor_report_at?: Date;
}
interface SubscriberEmailPreferencesOutput {
export interface SubscriberEmailPreferencesOutput {
id?: number;
primary_email?: string;
unsubscribe_token?: string;