MNTOR-2742 - Implement Formstack iframe for cancellation survey (#4409)
* Add a cancellation survey * Replace our own button by the embedded one * MNTOR-2742 - Use Formstack iframe to handle cancellation survey * add image * increase margins on text * add first unit test * add unit tests for dialog flow * update strings * update strings * more changes requested * add unicode characteR * remove alchemer src * add and ignore test * remove unused strings * remove unused imgs * remove unused string * remove unused strings * add unit test * fix unit tests * add attribution * remove double telemetry unit test check * add attribution * Update src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx Co-authored-by: Vincent <Vinnl@users.noreply.github.com> --------- Co-authored-by: Vincent <git@vincenttunru.com> Co-authored-by: Vincent <Vinnl@users.noreply.github.com>
This commit is contained in:
Родитель
89b6bc8cbe
Коммит
2c9326028f
|
@ -8,6 +8,22 @@ settings-cancel-plus-title = Cancel { -brand-monitor-plus } subscription
|
|||
settings-cancel-plus-details = Your subscription will revert to a free account after the current billing cycle ends. Your data broker scan results will be permanently deleted, and you may be re-added to those sites.
|
||||
settings-cancel-plus-link-label = Cancel from your { -brand-mozilla-account }
|
||||
|
||||
## Cancel Plus subscription - flow with a cancellation survey
|
||||
|
||||
settings-cancel-plus-survey-button-label = Cancel your subscription
|
||||
settings-cancel-plus-step-confirm-heading = Leaving now means data brokers may add you back
|
||||
settings-cancel-plus-step-confirm-content-pt1 = Data brokers regularly scrape the internet and search public records to find new info about you. They’ll often add you back within 3-4 months after removal.
|
||||
settings-cancel-plus-step-confirm-content-pt2 = { -brand-monitor-plus } continually watches for new profiles and removes them for you, no matter how many times you’re re-added.
|
||||
settings-cancel-plus-step-confirm-cta-label = Continue to cancellation
|
||||
settings-cancel-plus-step-confirm-cancel-label = Never mind, take me back
|
||||
settings-cancel-plus-step-survey-heading = We’re sorry to see you go. Will you tell us why you’re leaving?
|
||||
settings-cancel-plus-step-survey-lead = Your experience is important to us. We read every response and take it into consideration.
|
||||
settings-cancel-plus-step-survey-cta-label = Continue to cancellation
|
||||
settings-unsubscribe-dialog-confirmation-redirect-title = Directing you to your { -brand-mozilla-account } to cancel
|
||||
settings-unsubscribe-dialog-confirmation-redirect-description-pt1 = We’ll automatically redirect you to your { -brand-mozilla-account } where you can cancel your { -brand-monitor } subscription.
|
||||
settings-unsubscribe-dialog-confirmation-redirect-description-pt2 = Please note, all of your { -brand-monitor-plus } services will be <b>permanently deleted</b> after your current billing cycle ends.
|
||||
settings-unsubscribe-dialog-cancellation-survey-form-placeholder = What could have gone better?
|
||||
|
||||
## Delete Monitor account
|
||||
|
||||
settings-delete-monitor-plus-account-title = Delete { -brand-monitor } account
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
@import "../../../../../../tokens";
|
||||
|
||||
// The attribute selector makes this more specific that <Button>'s direct styles,
|
||||
// so we can overwrite the font weight. Maybe it should just not be a <Button>…
|
||||
[type="button"].trigger {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
text-align: center;
|
||||
|
||||
.tertiaryCta {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.cancellationIllustrationWrapper {
|
||||
height: 200px; // height of illustration;
|
||||
width: 100%;
|
||||
padding-bottom: $spacing-lg;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.iframeWrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 240px; //height of iframe
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/* 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 { useOverlayTriggerState } from "react-stately";
|
||||
import { useOverlayTrigger } from "react-aria";
|
||||
import Image from "next/image";
|
||||
import styles from "./CancelFlow.module.scss";
|
||||
import CancellationSurveyPlaneIllustration from "./images/CancellationSurveyPlaneIllustration.png";
|
||||
import { useTelemetry } from "../../../../../../hooks/useTelemetry";
|
||||
import { ModalOverlay } from "../../../../../../components/client/dialog/ModalOverlay";
|
||||
import { Dialog } from "../../../../../../components/client/dialog/Dialog";
|
||||
import { Button } from "../../../../../../components/client/Button";
|
||||
import { useL10n } from "../../../../../../hooks/l10n";
|
||||
import { TelemetryButton } from "../../../../../../components/client/TelemetryButton";
|
||||
|
||||
export type Props = {
|
||||
fxaSubscriptionsUrl: string;
|
||||
};
|
||||
|
||||
export const CancelFlow = (props: Props) => {
|
||||
const l10n = useL10n();
|
||||
const recordTelemetry = useTelemetry();
|
||||
const [step, setCurrentStep] = useState<"confirm" | "survey" | "redirecting">(
|
||||
"confirm",
|
||||
);
|
||||
|
||||
const dialogState = useOverlayTriggerState({
|
||||
onOpenChange: (isOpen) => {
|
||||
recordTelemetry("popup", isOpen ? "view" : "exit", {
|
||||
popup_id: "settings-cancel-monitor-plus-dialog",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const dialogTrigger = useOverlayTrigger({ type: "dialog" }, dialogState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onPress={() => dialogState.open()}
|
||||
className={styles.trigger}
|
||||
>
|
||||
{l10n.getString("settings-cancel-plus-survey-button-label")}
|
||||
</Button>
|
||||
{dialogState.isOpen && (
|
||||
<ModalOverlay
|
||||
state={dialogState}
|
||||
{...dialogTrigger.overlayProps}
|
||||
isDismissable={true}
|
||||
>
|
||||
<Dialog
|
||||
title={l10n.getString(
|
||||
step === "confirm"
|
||||
? "settings-cancel-plus-step-confirm-heading"
|
||||
: step === "survey"
|
||||
? "settings-cancel-plus-step-survey-heading"
|
||||
: "settings-unsubscribe-dialog-confirmation-redirect-title",
|
||||
)}
|
||||
illustration={
|
||||
<Image
|
||||
className={styles.cancellationIllustrationWrapper}
|
||||
src={CancellationSurveyPlaneIllustration}
|
||||
alt=""
|
||||
/>
|
||||
}
|
||||
onDismiss={() => dialogState.close()}
|
||||
>
|
||||
<div className={styles.contentWrapper}>
|
||||
{step === "confirm" && (
|
||||
<>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"settings-cancel-plus-step-confirm-content-pt1",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"settings-cancel-plus-step-confirm-content-pt2",
|
||||
)}
|
||||
</p>
|
||||
<TelemetryButton
|
||||
event={{
|
||||
module: "button",
|
||||
name: "click",
|
||||
data: {
|
||||
button_id: "continue_to_cancellation",
|
||||
},
|
||||
}}
|
||||
variant="primary"
|
||||
onPress={() => setCurrentStep("survey")}
|
||||
>
|
||||
{l10n.getString(
|
||||
"settings-cancel-plus-step-confirm-cta-label",
|
||||
)}
|
||||
</TelemetryButton>
|
||||
<TelemetryButton
|
||||
event={{
|
||||
module: "popup",
|
||||
name: "exit",
|
||||
data: {
|
||||
popup_id: "never_mind_take_me_back",
|
||||
},
|
||||
}}
|
||||
variant="tertiary"
|
||||
onPress={() => dialogState.close()}
|
||||
className={styles.tertiaryCta}
|
||||
>
|
||||
{l10n.getString(
|
||||
"settings-cancel-plus-step-confirm-cancel-label",
|
||||
)}
|
||||
</TelemetryButton>
|
||||
</>
|
||||
)}
|
||||
{step === "survey" && (
|
||||
<>
|
||||
<p>
|
||||
{l10n.getString("settings-cancel-plus-step-survey-lead")}
|
||||
</p>
|
||||
|
||||
<div className={styles.iframeWrapper}>
|
||||
<iframe
|
||||
scrolling={"no"}
|
||||
frameBorder={0}
|
||||
src="https://mozilla.formstack.com/forms/mozilla_monitor_plus_cancel"
|
||||
width="800"
|
||||
height="320"
|
||||
aria-label={l10n.getString(
|
||||
"settings-unsubscribe-dialog-cancellation-survey-form-placeholder",
|
||||
)}
|
||||
></iframe>
|
||||
</div>
|
||||
<TelemetryButton
|
||||
event={{
|
||||
module: "button",
|
||||
name: "click",
|
||||
data: {
|
||||
button_id: "continue_to_cancellation",
|
||||
},
|
||||
}}
|
||||
className={styles.tertiaryCta}
|
||||
variant="tertiary"
|
||||
onPress={() => {
|
||||
setCurrentStep("redirecting");
|
||||
setTimeout(() => {
|
||||
/* c8 ignore next */
|
||||
document.location = props.fxaSubscriptionsUrl;
|
||||
}, 5000);
|
||||
}}
|
||||
>
|
||||
{l10n.getString(
|
||||
"settings-cancel-plus-step-survey-cta-label",
|
||||
)}
|
||||
</TelemetryButton>
|
||||
</>
|
||||
)}
|
||||
{step === "redirecting" && (
|
||||
<>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"settings-unsubscribe-dialog-confirmation-redirect-description-pt1",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getFragment(
|
||||
"settings-unsubscribe-dialog-confirmation-redirect-description-pt2",
|
||||
{
|
||||
elems: {
|
||||
b: <b />,
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,7 @@ import {
|
|||
Props as DialogProps,
|
||||
} from "../../../../../../components/client/dialog/Dialog";
|
||||
import { Button } from "../../../../../../components/client/Button";
|
||||
import { TelemetryButton } from "../../../../../../components/client/TelemetryButton";
|
||||
|
||||
export type Props = {
|
||||
triggerLabel: string;
|
||||
|
@ -41,13 +42,20 @@ export const SettingsConfirmationDialog = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<TelemetryButton
|
||||
event={{
|
||||
module: "popup",
|
||||
name: "exit",
|
||||
data: {
|
||||
popup_id: "never_mind_take_me_back",
|
||||
},
|
||||
}}
|
||||
variant="tertiary"
|
||||
onPress={() => dialogState.open()}
|
||||
className={styles.trigger}
|
||||
>
|
||||
{props.triggerLabel}
|
||||
</Button>
|
||||
</TelemetryButton>
|
||||
{dialogState.isOpen && (
|
||||
<ModalOverlay
|
||||
state={dialogState}
|
||||
|
|
|
@ -12,7 +12,6 @@ import { getSpecificL10nSync } from "../../../../../../functions/server/mockL10n
|
|||
import { TestComponentWrapper } from "../../../../../../../TestComponentWrapper";
|
||||
import { SerializedSubscriber } from "../../../../../../../next-auth";
|
||||
import { onAddEmail, onRemoveEmail } from "./actions";
|
||||
import { sanitizeEmailRow } from "../../../../../../functions/server/sanitize";
|
||||
|
||||
const mockedSessionUpdate = jest.fn();
|
||||
const mockedRecordTelemetry = jest.fn();
|
||||
|
@ -40,6 +39,7 @@ jest.mock("./actions", () => {
|
|||
|
||||
import { SettingsView } from "./View";
|
||||
import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags";
|
||||
import { sanitizeEmailRow } from "../../../../../../functions/server/sanitize";
|
||||
|
||||
const subscriberId = 7;
|
||||
const mockedSubscriber: SerializedSubscriber = {
|
||||
|
@ -449,6 +449,177 @@ it("shows the Plus cancellation link if the user has Plus", () => {
|
|||
expect(cancellationHeading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("takes you through the cancellation dialog flow all the way to subplat", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TestComponentWrapper>
|
||||
<SettingsView
|
||||
l10n={getSpecificL10nSync()}
|
||||
user={{
|
||||
...mockedUser,
|
||||
fxa: {
|
||||
...mockedUser.fxa,
|
||||
subscriptions: ["monitor"],
|
||||
} as Session["user"]["fxa"],
|
||||
}}
|
||||
breachCountByEmailAddress={{
|
||||
[mockedUser.email]: 42,
|
||||
}}
|
||||
emailAddresses={[]}
|
||||
fxaSettingsUrl=""
|
||||
fxaSubscriptionsUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
monthlySubscriptionUrl=""
|
||||
subscriptionBillingAmount={mockedSubscriptionBillingAmount}
|
||||
enabledFeatureFlags={["MonitorAccountDeletion", "CancellationSurvey"]}
|
||||
/>
|
||||
</TestComponentWrapper>,
|
||||
);
|
||||
|
||||
const cancellationButton = screen.getByRole("button", {
|
||||
name: "Cancel your subscription",
|
||||
});
|
||||
|
||||
await user.click(cancellationButton);
|
||||
|
||||
expect(mockedRecordTelemetry).toHaveBeenCalledWith(
|
||||
"popup",
|
||||
"view",
|
||||
expect.objectContaining({
|
||||
popup_id: "settings-cancel-monitor-plus-dialog",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("dialog", {
|
||||
name: "Leaving now means data brokers may add you back",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const continueToCancellationButton = screen.getByRole("button", {
|
||||
name: "Continue to cancellation",
|
||||
});
|
||||
await user.click(continueToCancellationButton);
|
||||
|
||||
expect(
|
||||
screen.getByRole("dialog", {
|
||||
name: "We’re sorry to see you go. Will you tell us why you’re leaving?",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const continueToCancellationButton2 = screen.getByRole("button", {
|
||||
name: "Continue to cancellation",
|
||||
});
|
||||
await user.click(continueToCancellationButton2);
|
||||
|
||||
expect(
|
||||
screen.getByRole("dialog", {
|
||||
name: "Directing you to your Mozilla account to cancel",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the cancellation survey if the user selects nevermind, take me back", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TestComponentWrapper>
|
||||
<SettingsView
|
||||
l10n={getSpecificL10nSync()}
|
||||
user={{
|
||||
...mockedUser,
|
||||
fxa: {
|
||||
...mockedUser.fxa,
|
||||
subscriptions: ["monitor"],
|
||||
} as Session["user"]["fxa"],
|
||||
}}
|
||||
breachCountByEmailAddress={{
|
||||
[mockedUser.email]: 42,
|
||||
}}
|
||||
emailAddresses={[]}
|
||||
fxaSettingsUrl=""
|
||||
fxaSubscriptionsUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
monthlySubscriptionUrl=""
|
||||
subscriptionBillingAmount={mockedSubscriptionBillingAmount}
|
||||
enabledFeatureFlags={["MonitorAccountDeletion", "CancellationSurvey"]}
|
||||
/>
|
||||
</TestComponentWrapper>,
|
||||
);
|
||||
|
||||
const cancellationButton = screen.getByRole("button", {
|
||||
name: "Cancel your subscription",
|
||||
});
|
||||
|
||||
await user.click(cancellationButton);
|
||||
|
||||
const takeMeBackButton = screen.getByRole("button", {
|
||||
name: "Never mind, take me back",
|
||||
});
|
||||
|
||||
await user.click(takeMeBackButton);
|
||||
|
||||
expect(mockedRecordTelemetry).toHaveBeenCalledWith(
|
||||
"popup",
|
||||
"exit",
|
||||
expect.objectContaining({
|
||||
popup_id: "never_mind_take_me_back",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(takeMeBackButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the cancellation dialog", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TestComponentWrapper>
|
||||
<SettingsView
|
||||
l10n={getSpecificL10nSync()}
|
||||
user={{
|
||||
...mockedUser,
|
||||
fxa: {
|
||||
...mockedUser.fxa,
|
||||
subscriptions: ["monitor"],
|
||||
} as Session["user"]["fxa"],
|
||||
}}
|
||||
breachCountByEmailAddress={{
|
||||
[mockedUser.email]: 42,
|
||||
}}
|
||||
emailAddresses={[]}
|
||||
fxaSettingsUrl=""
|
||||
fxaSubscriptionsUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
monthlySubscriptionUrl=""
|
||||
subscriptionBillingAmount={mockedSubscriptionBillingAmount}
|
||||
enabledFeatureFlags={["MonitorAccountDeletion", "CancellationSurvey"]}
|
||||
/>
|
||||
</TestComponentWrapper>,
|
||||
);
|
||||
|
||||
const cancellationButton = screen.getByRole("button", {
|
||||
name: "Cancel your subscription",
|
||||
});
|
||||
|
||||
await user.click(cancellationButton);
|
||||
|
||||
const cancellationDialogCloseBtn = screen.getByRole("button", {
|
||||
name: "Close modal",
|
||||
});
|
||||
|
||||
await user.click(cancellationDialogCloseBtn);
|
||||
|
||||
expect(mockedRecordTelemetry).toHaveBeenCalledWith(
|
||||
"popup",
|
||||
"exit",
|
||||
expect.objectContaining({
|
||||
popup_id: "settings-cancel-monitor-plus-dialog",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show the account deletion button if the relevant flag is not enabled", () => {
|
||||
render(
|
||||
<TestComponentWrapper>
|
||||
|
@ -785,6 +956,7 @@ describe("to learn about usage", () => {
|
|||
const cancelPlusLink = screen.getByRole("link", {
|
||||
name: "Cancel from your Mozilla account Open link in a new tab",
|
||||
});
|
||||
|
||||
await user.click(cancelPlusLink);
|
||||
|
||||
expect(mockedRecordTelemetry).toHaveBeenCalledWith(
|
||||
|
@ -821,6 +993,7 @@ describe("to learn about usage", () => {
|
|||
const deactivateAccountLink = screen.getByRole("link", {
|
||||
name: "Go to Mozilla account settings Open link in a new tab",
|
||||
});
|
||||
|
||||
await user.click(deactivateAccountLink);
|
||||
|
||||
expect(mockedRecordTelemetry).toHaveBeenCalledWith(
|
||||
|
|
|
@ -20,6 +20,7 @@ import { sanitizeEmailRow } from "../../../../../../functions/server/sanitize";
|
|||
import { SettingsConfirmationDialog } from "./SettingsConfirmationDialog";
|
||||
import { DeleteAccountButton } from "./DeleteAccountButton";
|
||||
import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags";
|
||||
import { CancelFlow } from "./CancelFlow";
|
||||
|
||||
export type Props = {
|
||||
l10n: ExtendedReactLocalization;
|
||||
|
@ -97,21 +98,25 @@ export const SettingsView = (props: Props) => {
|
|||
<div className={styles.cancelSection}>
|
||||
<h3>{l10n.getString("settings-cancel-plus-title")}</h3>
|
||||
<p>{l10n.getString("settings-cancel-plus-details")}</p>
|
||||
<TelemetryLink
|
||||
href={props.fxaSubscriptionsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
eventData={{
|
||||
link_id: "cancel_plus",
|
||||
}}
|
||||
>
|
||||
{l10n.getString("settings-cancel-plus-link-label")}
|
||||
<OpenInNew
|
||||
alt={l10n.getString("open-in-new-tab-alt")}
|
||||
width="13"
|
||||
height="13"
|
||||
/>
|
||||
</TelemetryLink>
|
||||
{props.enabledFeatureFlags.includes("CancellationSurvey") ? (
|
||||
<CancelFlow fxaSubscriptionsUrl={props.fxaSubscriptionsUrl} />
|
||||
) : (
|
||||
<TelemetryLink
|
||||
href={props.fxaSubscriptionsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
eventData={{
|
||||
link_id: "cancel_plus",
|
||||
}}
|
||||
>
|
||||
{l10n.getString("settings-cancel-plus-link-label")}
|
||||
<OpenInNew
|
||||
alt={l10n.getString("open-in-new-tab-alt")}
|
||||
width="13"
|
||||
height="13"
|
||||
/>
|
||||
</TelemetryLink>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 17 KiB |
|
@ -36,7 +36,8 @@ export type FeatureFlagName =
|
|||
| "FxaUidTelemetry"
|
||||
| "RebrandAnnouncement"
|
||||
| "MonitorAccountDeletion"
|
||||
| "RedesignedEmails";
|
||||
| "RedesignedEmails"
|
||||
| "CancellationSurvey";
|
||||
|
||||
export async function getEnabledFeatureFlags(
|
||||
options:
|
||||
|
|
|
@ -84,7 +84,7 @@ function generateCspData() {
|
|||
"font-src 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'self'",
|
||||
"frame-src 'self' https://js.stripe.com",
|
||||
"frame-src 'self' https://js.stripe.com https://mozilla.formstack.com",
|
||||
"object-src 'none'",
|
||||
"block-all-mixed-content",
|
||||
"upgrade-insecure-requests",
|
||||
|
|
Загрузка…
Ссылка в новой задаче