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
|
@ -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 = We’ll 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. We’ll help you resolve data breaches and keep your info private — and we’ll <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 don’t 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 you’ve 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 = There’s a $240 billion industry of <data_brokers>data brokers</data_brokers> selling your private information for profit. It’s 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 don’t 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(
|
||||
"There’s a $240 billion industry of data brokers selling your private information for profit. It’s 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 |