MNTOR-2648 - Walkthrough section LP (#3911)

* MNTOR-2648 - Walkthrough section LP (WIP)

* ui tweak

* add privacy and link show us version string for walkthrough step

* use noormal, non text outlined svgs

* add unit tests

* remove unused strings

* localize img alts

* add localized illustrations

* move all localized images into separate file

* update stories and fix test

* lint

* simplify walkthrough image localized logic

* add tests for de and fr illustrations

* use right em dash

* MNTOR-2647 - Quote LP

* Add telemetry to walkthrough section LP

* fix partially localized fr img

* Walkthrough telemetry LP (#3920)

* rm quote

* use telemetrylink wrapper

* add jest mock usetelemetry

* split onfocus and onchange

* ignore telemetrylink tests

* rm test

* add back quote

* update story titles

* wrap label in visuallyhidden

* change country-code custom attr to lang

* fix unit tests

* remove unneeded elseif condition

* removed unneeded alt text

* group isHero styles in css signupform css

* align images

* remove alt

* update fr img

* remove magic number flex

* add all changes
This commit is contained in:
Kaitlyn Andres 2023-12-29 11:11:19 -05:00 коммит произвёл GitHub
Родитель a4744820a1
Коммит 403665159b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 1156 добавлений и 43 удалений

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

@ -9,3 +9,14 @@ landing-all-hero-emailform-submit-label = Get free scan
# This is a label underneath a big number "14" - it's an image that demos Monitor.
landing-all-hero-image-chart-label = exposures
# Value Proposition
landing-all-value-prop-fix-exposures = Well help you fix your exposures
landing-all-value-prop-fix-exposures-description = Our mission is to put control of your personal data back in your hands. Well help you resolve data breaches and keep your info private — and well <privacy_link>respect your privacy</privacy_link> in the process.
landing-all-value-prop-info-at-risk = What info could be at risk?
landing-all-value-prop-info-at-risk-description = Data leaks are unfortunately part of our digital lives. Your passwords, contact details, financial information, and other personal info can be exposed, putting you at risk of identity theft.
# Quote
landing-all-quote = <data_breaches>Data breaches</data_breaches> happen every 11 minutes, exposing your private information — but dont worry, we can help.

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

@ -101,3 +101,13 @@ landing-premium-plans-cards-feature-removal-plus = <b>Automatic removal</b> of p
landing-premium-plans-cards-feature-alerts = Get alerts when your data has been breached
landing-premium-plans-cards-feature-guidance = <b>Guided help</b> to fix high-risk data breaches
landing-premium-plans-cards-feature-monitoring = Continuous monitoring
# Value proposition
landing-premium-value-prop-fix-exposures-description = We provide steps to follow when youve been affected by a data breach and can even remove your data from more than 190 sites trying to sell it — and we <privacy_link>respect your privacy</privacy_link> in the process.
landing-premium-value-prop-info-at-risk-description = Details like your <exposure_type_list>home address, family members names, financial info</exposure_type_list> and more can be exposed when a website is hacked — or sold on data broker sites to anyone looking for you. Knowing what info is out there is the first step in protecting yourself.
landing-premium-value-prop-progress-card-illustration-alt = Progress card delineating exposures that are fixed, in progress or manually fixed
# Quote
landing-premium-quote = Theres a $240 billion industry of <data_brokers>data brokers</data_brokers> selling your private information for profit. Its time to take back your privacy.

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

@ -76,30 +76,6 @@
font: $text-body-lg;
color: $color-grey-50;
}
form {
display: flex;
flex-wrap: wrap;
gap: $spacing-sm;
input {
flex: 1 0 auto;
border-radius: $border-radius-md;
border: 1px solid $color-grey-20;
padding: $spacing-md;
font: $text-body-xl;
}
button {
flex: 1 0 auto;
font: $text-title-xs;
}
label {
font: $text-body-sm;
font-weight: 500;
}
}
}
.heroImage {
@ -163,3 +139,107 @@
text-align: center;
}
}
.quoteWrapper {
padding: $layout-lg $spacing-md;
display: flex;
justify-content: center;
@media screen and (min-width: $content-md) {
padding: $layout-lg;
}
.quote {
max-width: $content-lg;
text-align: center;
font: $text-title-2xs;
line-height: 1.5;
font-family: var(--font-inter);
font-weight: normal;
b {
em {
font-style: normal;
display: inline;
color: $color-violet-50;
}
}
}
}
.valuePropositionWrapper {
.item {
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: space-between;
padding: $spacing-2xl $spacing-md;
gap: $spacing-lg;
&.grayBg {
background: $color-grey-05;
}
.exposureTypeList {
font-weight: 600;
color: $color-purple-70;
display: inline;
}
@media screen and (min-width: $screen-md) {
padding-inline: $layout-lg;
padding-block: $layout-sm;
flex-direction: row;
gap: $spacing-xl;
&.reverseRow {
flex-direction: row-reverse;
}
@media screen and (min-width: $screen-lg) {
padding-inline: $layout-lg;
gap: $layout-xl;
@media screen and (min-width: $screen-xl) {
padding-inline: $layout-xl;
}
}
}
span {
gap: $spacing-md;
display: flex;
flex-direction: column;
h2 {
font: $text-title-xs;
font-family: var(--font-inter);
font-weight: 600;
}
p {
font: $text-body-lg;
}
}
.illustration {
width: 100%;
display: flex;
justify-content: center;
img {
height: auto;
}
@media screen and (min-width: $screen-md) {
flex: 1;
}
}
@media screen and (min-width: $screen-md) {
span,
.illustration {
flex: 1;
}
}
}
}

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

@ -21,6 +21,7 @@ export const LandingUs: Story = {
name: "US visitors",
args: {
eligibleForPremium: true,
countryCode: "us",
},
};
@ -28,5 +29,22 @@ export const LandingNonUs: Story = {
name: "Non-US visitors",
args: {
eligibleForPremium: false,
countryCode: "nz",
},
};
export const LandingNonUsDe: Story = {
name: "German",
args: {
eligibleForPremium: false,
countryCode: "de",
},
};
export const LandingNonUsFr: Story = {
name: "French",
args: {
eligibleForPremium: false,
countryCode: "fr",
},
};

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

@ -16,9 +16,15 @@ import {
import { userEvent } from "@testing-library/user-event";
import { axe } from "jest-axe";
import { signIn } from "next-auth/react";
import Meta, { LandingNonUs, LandingUs } from "./LandingView.stories";
import Meta, {
LandingNonUs,
LandingNonUsDe,
LandingNonUsFr,
LandingUs,
} from "./LandingView.stories";
jest.mock("next-auth/react");
jest.mock("../../../hooks/useTelemetry");
describe("When Premium is not available", () => {
it("passes the axe accessibility test suite", async () => {
@ -33,19 +39,64 @@ describe("When Premium is not available", () => {
const ComposedDashboard = composeStory(LandingNonUs, Meta);
render(<ComposedDashboard />);
const inputField = screen.getByLabelText(
const inputField = screen.getAllByLabelText(
"Enter your email address to check for data breach exposures.",
);
await user.type(inputField, "mail@example.com");
await user.type(inputField[0], "mail@example.com");
const submitButton = screen.getByRole("button", { name: "Get free scan" });
await user.click(submitButton);
const submitButton = screen.getAllByRole("button", {
name: "Get free scan",
});
await user.click(submitButton[0]);
expect(signIn).toHaveBeenCalledTimes(1);
expect(signIn).toHaveBeenCalledWith("fxa", expect.any(Object), {
email: "mail@example.com",
});
});
it("shows the data breaches quote", () => {
const ComposedDashboard = composeStory(LandingNonUs, Meta);
render(<ComposedDashboard />);
const quote = screen.getByText(
"Data breaches happen every 11 minutes, exposing your private information — but dont worry, we can help.",
);
expect(quote).toBeInTheDocument();
});
it("shows the scanning for exposures illustration in the fix your exposures section", () => {
const ComposedDashboard = composeStory(LandingNonUs, Meta);
render(<ComposedDashboard />);
const scanningForExposuresIllustration = screen.getByTestId(
"scanning-for-exposures-image",
);
expect(scanningForExposuresIllustration).toBeInTheDocument();
});
it("shows the german scanning for exposures illustration", () => {
const ComposedDashboard = composeStory(LandingNonUsDe, Meta);
render(<ComposedDashboard />);
const scanningForExposuresIllustration = screen.getByTestId(
"scanning-for-exposures-image",
);
expect(scanningForExposuresIllustration).toHaveAttribute(
"data-country-code",
"de",
);
});
it("shows the french scanning for exposures illustration", () => {
const ComposedDashboard = composeStory(LandingNonUsFr, Meta);
render(<ComposedDashboard />);
const scanningForExposuresIllustration = screen.getByTestId(
"scanning-for-exposures-image",
);
expect(scanningForExposuresIllustration).toHaveAttribute(
"data-country-code",
"fr",
);
});
});
describe("When Premium is available", () => {
@ -306,4 +357,21 @@ describe("When Premium is available", () => {
expect(signIn).toHaveBeenCalledTimes(1);
});
it("shows the data brokers quote", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
const quote = screen.getByText(
"Theres a $240 billion industry of data brokers selling your private information for profit. Its time to take back your privacy.",
);
expect(quote).toBeInTheDocument();
});
it("shows the progress card illustration in the fix your exposures section", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
const progressCardIllustration = screen.getByTestId("progress-card-image");
expect(progressCardIllustration).toBeInTheDocument();
});
});

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

@ -9,10 +9,18 @@ import { ExtendedReactLocalization } from "../../../hooks/l10n";
import { PlansTable } from "./PlansTable";
import { useId } from "react";
import getPremiumSubscriptionUrl from "../../../functions/server/getPremiumSubscriptionUrl";
import Image from "next/image";
import ProgressCardImage from "./value-prop-images/progress-card.svg";
import {
LeakedPasswordExampleIllustration,
ScanningForExposuresIllustration,
} from "./WalkthroughImages";
import { TelemetryLink } from "./TelemetryLink";
export type Props = {
eligibleForPremium: boolean;
l10n: ExtendedReactLocalization;
countryCode: string;
};
export const View = (props: Props) => {
@ -32,14 +40,135 @@ export const View = (props: Props) => {
)}
</p>
<SignUpForm
isHero
eligibleForPremium={props.eligibleForPremium}
signUpCallbackUrl={`${process.env.SERVER_URL}/redesign/user/dashboard/`}
eventId={{
cta: "clicked_get_scan_header",
field: "entered_email_address_header",
}}
/>
</div>
<div className={styles.heroImage}>
<HeroImage {...props} />
</div>
</header>
<div className={styles.quoteWrapper}>
<div className={styles.quote}>
<b>
{props.eligibleForPremium
? props.l10n.getFragment("landing-premium-quote", {
elems: {
data_brokers: <em />,
},
})
: props.l10n.getFragment("landing-all-quote", {
elems: {
data_breaches: <em />,
},
})}
</b>
</div>
</div>
<div className={styles.valuePropositionWrapper}>
<div className={`${styles.item} ${styles.grayBg}`}>
<span>
<h2>
{props.l10n.getString("landing-all-value-prop-fix-exposures")}
</h2>
<p>
{props.eligibleForPremium
? props.l10n.getFragment(
"landing-premium-value-prop-fix-exposures-description",
{
elems: {
privacy_link: (
<TelemetryLink
eventData={{ button_id: "privacy_information" }}
href="https://www.mozilla.org/en-US/firefox/privacy/"
target="_blank"
/>
),
},
},
)
: props.l10n.getFragment(
"landing-all-value-prop-fix-exposures-description",
{
elems: {
privacy_link: (
<TelemetryLink
eventData={{ button_id: "privacy_information" }}
href="https://www.mozilla.org/en-US/firefox/privacy/"
target="_blank"
/>
),
},
},
)}
</p>
<SignUpForm
eligibleForPremium={props.eligibleForPremium}
signUpCallbackUrl={`${process.env.SERVER_URL}/redesign/user/dashboard/`}
eventId={{
cta: "clicked_get_scan_second",
field: "entered_email_address_second",
}}
/>
</span>
<div className={styles.illustration}>
{props.eligibleForPremium ? (
<Image
src={ProgressCardImage}
alt={props.l10n.getString(
"landing-premium-value-prop-progress-card-illustration-alt",
)}
data-testid="progress-card-image"
/>
) : (
<ScanningForExposuresIllustration {...props} />
)}
</div>
</div>
<div className={`${styles.item} ${styles.reverseRow}`}>
<span>
<h2>
{props.l10n.getString("landing-all-value-prop-info-at-risk")}
</h2>
<p>
{props.eligibleForPremium
? props.l10n.getFragment(
"landing-premium-value-prop-info-at-risk-description",
{
elems: {
exposure_type_list: (
<span className={styles.exposureTypeList} />
),
},
},
)
: props.l10n.getString(
"landing-all-value-prop-info-at-risk-description",
)}
</p>
<SignUpForm
eligibleForPremium={props.eligibleForPremium}
signUpCallbackUrl={`${process.env.SERVER_URL}/redesign/user/dashboard/`}
eventId={{
cta: "clicked_get_scan_third",
field: "entered_email_address_third",
}}
/>
</span>
<div className={styles.illustration}>
<LeakedPasswordExampleIllustration {...props} />
</div>
</div>
</div>
<Plans {...props} />
</main>
);
@ -83,7 +212,7 @@ const Plans = (props: Props) => {
}
return (
<section className={styles.plans}>
<div className={styles.plans}>
<h2 id={headingId} className={styles.planName}>
{props.l10n.getString("landing-premium-plans-heading")}
</h2>
@ -97,6 +226,6 @@ const Plans = (props: Props) => {
yearly: getPremiumSubscriptionUrl({ type: "yearly" }),
}}
/>
</section>
</div>
);
};

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

@ -0,0 +1,38 @@
@import "../../../tokens";
.form {
display: flex;
flex-wrap: wrap;
gap: $spacing-xl;
input {
flex: 1 0 auto;
border-radius: $border-radius-md;
border: 1px solid $color-grey-20;
padding: $spacing-md;
font: $text-body-md;
}
button {
flex: 1 0 auto;
font: $text-body-md;
font-weight: 600;
}
label {
font: $text-body-sm;
font-weight: 500;
}
}
form.isHero {
gap: $spacing-sm;
}
input.isHero {
font: $text-body-xl;
}
button.isHero {
font: $text-title-xs;
}

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

@ -8,20 +8,28 @@ import { FormEventHandler, useId, useState } from "react";
import { signIn } from "next-auth/react";
import { useL10n } from "../../../hooks/l10n";
import { Button } from "../../../components/client/Button";
import styles from "./SignUpForm.module.scss";
import { useTelemetry } from "../../../hooks/useTelemetry";
import { VisuallyHidden } from "../../../components/server/VisuallyHidden";
export type Props = {
eligibleForPremium: boolean;
signUpCallbackUrl: string;
isHero?: boolean;
eventId: {
cta: string;
field: string;
};
};
export const SignUpForm = (props: Props) => {
const emailInputId = useId();
const l10n = useL10n();
const [emailInput, setEmailInput] = useState("");
const record = useTelemetry();
const onSubmit: FormEventHandler = (event) => {
event.preventDefault();
void signIn(
"fxa",
{ callbackUrl: props.signUpCallbackUrl },
@ -30,30 +38,54 @@ export const SignUpForm = (props: Props) => {
// https://mozilla.github.io/ecosystem-platform/relying-parties/reference/query-parameters#email
{ email: emailInput },
);
record("ctaButton", "click", {
button_id: props.eventId.cta,
});
};
const labelContent = (
<label htmlFor={emailInputId}>
{l10n.getString(
props.eligibleForPremium
? "landing-premium-hero-emailform-input-label"
: "landing-all-hero-emailform-input-label",
)}
</label>
);
return (
<form onSubmit={onSubmit}>
<form className={styles.form} onSubmit={onSubmit}>
<input
className={props.isHero ? styles.isHero : ""}
name={emailInputId}
id={emailInputId}
onChange={(e) => setEmailInput(e.target.value)}
onChange={(e) => {
setEmailInput(e.target.value);
}}
onFocus={() => {
record("field", "focus", {
field_id: props.eventId.field,
});
}}
value={emailInput}
type="email"
placeholder={l10n.getString(
"landing-all-hero-emailform-input-placeholder",
)}
/>
<Button type="submit" variant="primary" wide>
<Button
type="submit"
variant="primary"
wide
className={props.isHero ? styles.isHero : ""}
>
{l10n.getString("landing-all-hero-emailform-submit-label")}
</Button>
<label htmlFor={emailInputId}>
{l10n.getString(
props.eligibleForPremium
? "landing-premium-hero-emailform-input-label"
: "landing-all-hero-emailform-input-label",
)}
</label>
{props.isHero ? (
labelContent
) : (
<VisuallyHidden>{labelContent}</VisuallyHidden>
)}
</form>
);
};

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

@ -0,0 +1,34 @@
/* 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 { useTelemetry } from "../../../hooks/useTelemetry";
import { GleanMetricMap } from "../../../../telemetry/generated/_map";
import { HTMLAttributes } from "react";
// Telemetry link is shown in a fluent getFragment (which does not get rendered in tests)
/* c8 ignore start */
export const TelemetryLink = ({
eventData,
...props
}: {
eventData: GleanMetricMap["button"]["click"];
href: string;
target: string;
} & HTMLAttributes<HTMLAnchorElement>) => {
const record = useTelemetry();
return (
<a
{...props}
onClick={(event) => {
record("button", "click", eventData);
props.onClick?.(event);
}}
/>
);
};
/* c8 ignore stop */

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

@ -0,0 +1,73 @@
/* 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 { ExtendedReactLocalization } from "../../../hooks/l10n";
import Image from "next/image";
import ScanningForExposuresImage from "./value-prop-images/scanning-for-exposures.svg";
import LeakedPasswordExampleImage from "./value-prop-images/leaked-password-example.svg";
import ScanningForExposuresImageDe from "./value-prop-images/de/scanning-for-exposures-de.svg";
import LeakedPasswordExampleImageDe from "./value-prop-images/de/leaked-password-example-de.svg";
import ScanningForExposuresImageFr from "./value-prop-images/fr/scanning-for-exposures-fr.svg";
import LeakedPasswordExampleImageFr from "./value-prop-images/fr/leaked-password-example-fr.svg";
const IllustrationWrapper = ({
image,
testId,
countryCode,
}: {
image: string;
l10n: ExtendedReactLocalization;
testId: string;
countryCode: string;
}) => (
<Image
src={image}
alt=""
data-testid={testId}
data-country-code={countryCode}
/>
);
type Props = {
countryCode: string;
l10n: ExtendedReactLocalization;
};
export const ScanningForExposuresIllustration = (props: Props) => {
let imageSrc = ScanningForExposuresImage;
if (props.countryCode === "de") {
imageSrc = ScanningForExposuresImageDe;
} else if (props.countryCode === "fr") {
imageSrc = ScanningForExposuresImageFr;
}
return (
<IllustrationWrapper
{...props}
image={imageSrc}
testId="scanning-for-exposures-image"
countryCode={props.countryCode}
/>
);
};
export const LeakedPasswordExampleIllustration = (props: Props) => {
let imageSrc = LeakedPasswordExampleImage;
if (props.countryCode === "de") {
imageSrc = LeakedPasswordExampleImageDe;
} else if (props.countryCode === "fr") {
imageSrc = LeakedPasswordExampleImageFr;
}
return (
<IllustrationWrapper
{...props}
image={imageSrc}
testId="leaked-password-example"
countryCode={props.countryCode}
/>
);
};

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

@ -14,5 +14,11 @@ export default async function Page() {
const countryCode = getCountryCode(headers());
const eligibleForPremium = isEligibleForPremium(countryCode, enabledFlags);
return <View eligibleForPremium={eligibleForPremium} l10n={getL10n()} />;
return (
<View
eligibleForPremium={eligibleForPremium}
l10n={getL10n()}
countryCode={countryCode}
/>
);
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

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