Add small-screen version of the pricing table

This commit is contained in:
Vincent 2023-12-01 16:15:09 +01:00 коммит произвёл Vincent
Родитель b130d72651
Коммит 78e8f20347
6 изменённых файлов: 971 добавлений и 236 удалений

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

@ -78,3 +78,26 @@ landing-premium-plans-table-cta-free-label = Start free monitoring
landing-premium-plans-table-cta-plus-label = Get data removal
landing-premium-plans-table-reassurance-free-label = Upgrade anytime
landing-premium-plans-table-reassurance-plus-label = Cancel anytime
landing-premium-plans-cards-feature-included = Included:
landing-premium-plans-cards-feature-not-included = Not included:
# Variables:
# $dataBrokerTotalCount (number) - number of scanned data broker sites, e.g. 190
landing-premium-plans-cards-feature-scan-free =
{ $dataBrokerTotalCount ->
[one] <b>One-time</b> scan of { $dataBrokerTotalCount } data broker site that may be selling your personal info
*[other] <b>One-time</b> scan of { $dataBrokerTotalCount } data broker sites that may be selling your personal info
}
# Variables:
# $dataBrokerTotalCount (number) - number of scanned data broker sites, e.g. 190
landing-premium-plans-cards-feature-scan-plus =
{ $dataBrokerTotalCount ->
[one] <b>Monthly</b> scan of { $dataBrokerTotalCount } data broker site that may be selling your personal info
*[other] <b>Monthly</b> scan of { $dataBrokerTotalCount } data broker sites that may be selling your personal info
}
landing-premium-plans-cards-feature-removal-free = <b>Manual removal</b> of personal info from sites that are selling it
landing-premium-plans-cards-feature-removal-plus = <b>Automatic removal</b> of personal info from sites that are selling it
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

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

@ -154,5 +154,12 @@
.lead,
table {
width: $content-lg;
max-width: 100%;
}
.planName,
.lead {
padding-inline: $spacing-lg;
text-align: center;
}
}

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

@ -4,7 +4,15 @@
import { it, expect } from "@jest/globals";
import { composeStory } from "@storybook/react";
import { render, screen } from "@testing-library/react";
import {
getAllByRole,
getByRole,
getByText,
queryByRole,
queryByText,
render,
screen,
} from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { axe } from "jest-axe";
import { signIn } from "next-auth/react";
@ -52,10 +60,14 @@ describe("When Premium is available", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
// We limit our queries to the pricing table, so as not to match similar
// elements that are also present in the pricing _cards_, i.e. the elements
// shown on small screens:
const pricingTable = screen.getByRole("grid");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(queryByRole(pricingTable, "dialog")).not.toBeInTheDocument();
const moreInfoButton = screen.getAllByRole("button", {
const moreInfoButton = getAllByRole(pricingTable, "button", {
name: "More info",
})[0];
await user.click(moreInfoButton);
@ -71,6 +83,10 @@ describe("When Premium is available", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
// We limit our queries to the pricing table, so as not to match similar
// elements that are also present in the pricing _cards_, i.e. the elements
// shown on small screens:
const pricingTable = screen.getByRole("grid");
// Regular expression:
//
@ -82,9 +98,9 @@ describe("When Premium is available", () => {
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(screen.getByText(/Save (.+?)%/)).toBeInTheDocument();
expect(getByText(pricingTable, /Save (.+?)%/)).toBeInTheDocument();
const yearlyToggle = screen.getByRole("radio", { name: "Yearly" });
const yearlyToggle = getByRole(pricingTable, "radio", { name: "Yearly" });
await user.click(yearlyToggle);
await user.keyboard("[ArrowRight][Space]");
@ -98,7 +114,84 @@ describe("When Premium is available", () => {
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(screen.queryByText(/Save (.+?)%/)).not.toBeInTheDocument();
expect(queryByText(pricingTable, /Save (.+?)%/)).not.toBeInTheDocument();
});
it("can switch from the yearly to the monthly plan with the mouse", async () => {
const user = userEvent.setup();
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
// We limit our queries to the pricing table, so as not to match similar
// elements that are also present in the pricing _cards_, i.e. the elements
// shown on small screens:
const pricingTable = screen.getByRole("grid");
// Regular expression:
//
// Save Starts with the characters `Save `,
//
// (.+?) followed by one or more (`+`) arbitrary characters (`.`), until
// the next part of the regular expression matches…
//
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(getByText(pricingTable, /Save (.+?)%/)).toBeInTheDocument();
const monthlyToggle = getByRole(pricingTable, "radio", { name: "Monthly" });
await user.click(monthlyToggle);
// Regular expression:
//
// Save Starts with the characters `Save `,
//
// (.+?) followed by one or more (`+`) arbitrary characters (`.`), until
// the next part of the regular expression matches…
//
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(queryByText(pricingTable, /Save (.+?)%/)).not.toBeInTheDocument();
});
it("switching to the monthly plan in portrait mode is preserved when changing to landscape mode", async () => {
const user = userEvent.setup();
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
const pricingTable = screen.getByRole("grid");
const cards = screen.getAllByRole("group");
const plusCard = cards[0];
// Regular expression:
//
// Save Starts with the characters `Save `,
//
// (.+?) followed by one or more (`+`) arbitrary characters (`.`), until
// the next part of the regular expression matches…
//
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(getByText(plusCard, /Save (.+?)%/)).toBeInTheDocument();
expect(getByText(pricingTable, /Save (.+?)%/)).toBeInTheDocument();
const monthlyToggle = getByRole(plusCard, "radio", { name: "Monthly" });
await user.click(monthlyToggle);
// Regular expression:
//
// Save Starts with the characters `Save `,
//
// (.+?) followed by one or more (`+`) arbitrary characters (`.`), until
// the next part of the regular expression matches…
//
// % …which is a single `%` character.
//
// All that combines to a string like "Save 13.37%".
expect(queryByText(plusCard, /Save (.+?)%/)).not.toBeInTheDocument();
expect(queryByText(pricingTable, /Save (.+?)%/)).not.toBeInTheDocument();
});
it("can move to the subscribe button with the keyboard", async () => {
@ -106,13 +199,17 @@ describe("When Premium is available", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
// We limit our queries to the pricing table, so as not to match similar
// elements that are also present in the pricing _cards_, i.e. the elements
// shown on small screens:
const pricingTable = screen.getByRole("grid");
const plusSubscribeButton = screen.getByRole("link", {
const plusSubscribeButton = getByRole(pricingTable, "link", {
name: "Get data removal",
});
expect(plusSubscribeButton).not.toHaveFocus();
const yearlyToggle = screen.getByRole("radio", { name: "Yearly" });
const yearlyToggle = getByRole(pricingTable, "radio", { name: "Yearly" });
await user.click(yearlyToggle);
await user.keyboard("[ArrowRight][ArrowRight]");
@ -124,13 +221,32 @@ describe("When Premium is available", () => {
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
const pricingTable = screen.getByRole("grid");
expect(signIn).not.toHaveBeenCalled();
const yearlyToggle = screen.getByRole("radio", { name: "Yearly" });
const yearlyToggle = getByRole(pricingTable, "radio", { name: "Yearly" });
await user.click(yearlyToggle);
await user.keyboard("[ArrowLeft][Space]");
expect(signIn).toHaveBeenCalledTimes(1);
});
it("can initiate sign in from the pricing cards", async () => {
const user = userEvent.setup();
const ComposedDashboard = composeStory(LandingUs, Meta);
render(<ComposedDashboard />);
const cards = screen.getAllByRole("group");
const freeCard = cards[1];
expect(signIn).not.toHaveBeenCalled();
const signInButton = getByRole(freeCard, "button", {
name: "Start free monitoring",
});
await user.click(signInButton);
expect(signIn).toHaveBeenCalledTimes(1);
});
});

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

@ -149,9 +149,13 @@
font-weight: 600;
padding: $spacing-sm $spacing-md;
// CSS uppercasing is not reliable for some locales, so limit it to
// English, where it is. See
// CSS uppercasing is not reliable for some locales, see
// https://mozilla-l10n.github.io/documentation/localization/dev_best_practices.html#css-issues
// However, uppercasing the source string directly isn't great for
// accessibility, as it will cause screen readers to spell it out:
// https://www.timdunklee.com/notes/use-css-text-transform-for-uppercase-letters-a11y/
// Thus, we limit the uppercasing for English for now, where uppercasing
// is reliable enough, and fallback to regular-casing for other locales:
[lang="en"] &,
[lang="en-US"] &,
[lang="en-CA"] &,
@ -207,11 +211,172 @@
}
}
.plansCards {
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
gap: $spacing-xl;
padding: $spacing-md;
[role="group"] {
flex: 1 1 $content-sm;
display: flex;
flex-direction: column;
align-items: center;
justify-content: start;
gap: $spacing-lg;
background-color: $color-white;
border-radius: $border-radius-md;
padding: $spacing-2xl $spacing-lg;
hr {
border-style: none;
border-top: 1px solid $color-grey-20;
width: 100%;
}
.head {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-sm;
h3 {
font: $text-title-xs;
font-weight: 600;
b {
color: $color-purple-70;
}
}
}
.priceSection {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-lg;
.cost {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-md;
.price {
font: $text-title-sm;
}
.total {
display: block;
color: $color-grey-40;
em {
font-weight: 700;
font-style: normal;
}
}
}
.cta {
font-weight: 700;
}
.reassurance {
font: $text-body-sm;
font-weight: 400;
}
}
.featuresSection {
display: flex;
flex-direction: column;
gap: $spacing-md;
padding-inline: $spacing-md;
h4 {
font: $text-title-3xs;
}
.featureList {
list-style-type: none;
display: flex;
flex-direction: column;
align-items: start;
justify-content: center;
gap: $spacing-lg;
margin: 0;
padding: 0;
li {
display: flex;
align-items: start;
gap: $spacing-sm;
.inclusionIcon {
margin: $spacing-xs;
flex: 1 0 auto;
}
&.included .inclusionIcon {
color: $color-green-90;
}
&.notIncluded .inclusionIcon {
color: $color-red-60;
}
button {
flex: 1 0 auto;
}
}
}
}
&.plusCard {
position: relative;
border: 4px solid $color-purple-70;
.badge {
position: absolute;
top: 0;
left: 50%;
transform: translateY(-50%) translateX(-50%);
background-color: $color-purple-70;
color: $color-white;
border-radius: $border-radius-md;
font: $text-body-sm;
font-weight: 600;
padding: $spacing-sm $spacing-md;
// CSS uppercasing is not reliable for some locales, see
// https://mozilla-l10n.github.io/documentation/localization/dev_best_practices.html#css-issues
// However, uppercasing the source string directly isn't great for
// accessibility, as it will cause screen readers to spell it out:
// https://www.timdunklee.com/notes/use-css-text-transform-for-uppercase-letters-a11y/
// Thus, we limit the uppercasing for English for now, where uppercasing
// is reliable enough, and fallback to regular-casing for other locales:
[lang="en"] &,
[lang="en-US"] &,
[lang="en-CA"] &,
[lang="en-GB"] & {
text-transform: uppercase;
}
}
}
&.freeCard {
border: 2px solid $color-grey-20;
}
}
}
.popoverTrigger {
background-color: transparent;
border-style: none;
cursor: pointer;
border-radius: $border-radius-md;
padding: 0;
svg {
width: $layout-2xs;
@ -251,3 +416,14 @@
transform: translateX(-50%) rotate(180deg);
}
}
@media (max-width: $screen-lg) {
.plansTable {
display: none;
}
}
@media (min-width: calc($screen-lg + 1px)) {
.plansCards {
display: none;
}
}

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

@ -44,6 +44,7 @@ import styles from "./PlansTable.module.scss";
import { useL10n } from "../../../hooks/l10n";
import {
CheckIcon,
CloseBigIcon,
QuestionMarkCircle,
} from "../../../components/server/Icons";
import { VisuallyHidden } from "../../../components/server/VisuallyHidden";
@ -79,201 +80,53 @@ export const PlansTable = (props: Props) => {
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>("yearly");
return (
<Table aria-labelledby={props["aria-labelledby"]} selectionMode="none">
<TableHeader>
<Column>
{l10n.getString("landing-premium-plans-table-heading-feature")}
</Column>
<Column>
<h3>
{l10n.getString("landing-premium-plans-table-heading-free-title")}
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-free-subtitle",
)}
</p>
</Column>
<Column>
<b className={styles.badge}>
{l10n.getString("landing-premium-plans-table-annotation-plus")}
</b>
<h3>
{l10n.getFragment(
"landing-premium-plans-table-heading-plus-title",
{ elems: { b: <b /> } },
)}
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-plus-subtitle",
)}
</p>
</Column>
</TableHeader>
<TableBody>
<Row>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-scan-label", {
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
})}
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-scan-free")}
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-scan-plus")}
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-removal-label",
)}
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-removal-free")}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-free-callout",
)}
</PopoverContent>
</InfoPopover>
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-removal-plus")}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-plus-callout",
{
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
)}
</PopoverContent>
</InfoPopover>
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-alerts-label")}
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-alerts-free",
<>
<div className={styles.plansCards}>
<div role="group" className={styles.plusCard}>
<div className={styles.head}>
<b className={styles.badge}>
{l10n.getString("landing-premium-plans-table-annotation-plus")}
</b>
<h3>
{l10n.getFragment(
"landing-premium-plans-table-heading-plus-title",
{ elems: { b: <b /> } },
)}
/>
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-alerts-plus",
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-plus-subtitle",
)}
</p>
</div>
<hr />
<div className={styles.priceSection}>
<BillingPeriodToggle
onChange={(newValue) => setBillingPeriod(newValue)}
/>
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-label",
)}
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-free",
)}
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-plus",
)}
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-monitoring-label",
)}
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-monitoring-free",
)}
/>
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-monitoring-plus",
)}
/>
</Cell>
</Row>
<Row>
<Cell>
<VisuallyHidden>
{l10n.getString("landing-premium-plans-table-billing-label")}
</VisuallyHidden>
</Cell>
<Cell>
<div className={styles.priceCell}>
<p className={styles.billingPeriod}>
{l10n.getString("landing-premium-plans-table-billing-free")}
</p>
<p className={styles.cost}>
<b className={styles.price}>
{roundedPriceFormatter.format(0)}
</b>
<span className={styles.total} />
</p>
<Button variant="secondary" onPress={() => void signIn("fxa")}>
{l10n.getString("landing-premium-plans-table-cta-free-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-free-label",
)}
</small>
</div>
</Cell>
<Cell>
<div className={styles.priceCell}>
<div className={styles.billingPeriod}>
<BillingPeriodToggle
onChange={(newValue) => setBillingPeriod(newValue)}
/>
</div>
<p aria-live="polite" className={styles.cost}>
<b className={styles.price}>
{billingPeriod === "yearly"
? l10n.getString(
"landing-premium-plans-table-price-plus-yearly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceAnnualBilling,
),
},
)
: l10n.getString(
"landing-premium-plans-table-price-plus-monthly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceMonthlyBilling,
),
},
)}
</b>
<span className={styles.total}>
{billingPeriod === "yearly" && (
<p aria-live="polite" className={styles.cost}>
<b className={styles.price}>
{billingPeriod === "yearly"
? l10n.getString(
"landing-premium-plans-table-price-plus-yearly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceAnnualBilling,
),
},
)
: l10n.getString(
"landing-premium-plans-table-price-plus-monthly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceMonthlyBilling,
),
},
)}
</b>
<span className={styles.total}>
{billingPeriod === "yearly" && (
<>
<em className={styles.discount}>
{l10n.getString(
"landing-premium-plans-table-price-plus-yearly-discount",
@ -286,39 +139,599 @@ export const PlansTable = (props: Props) => {
},
)}
</em>
)}
<br />
<span className={styles.sum}>
{l10n.getString(
"landing-premium-plans-table-price-plus-yearly-sum",
{
yearlyPrice: priceFormatter.format(
12 *
(billingPeriod === "yearly"
? monthlyPriceAnnualBilling
: monthlyPriceMonthlyBilling),
),
},
)}
</span>
</span>
</p>
<Button
variant="primary"
href={getPremiumSubscriptionUrl({ type: billingPeriod })}
>
{l10n.getString("landing-premium-plans-table-cta-plus-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-plus-label",
&nbsp;&nbsp;
</>
)}
</small>
</div>
</Cell>
</Row>
</TableBody>
</Table>
<span className={styles.sum}>
{l10n.getString(
"landing-premium-plans-table-price-plus-yearly-sum",
{
yearlyPrice: priceFormatter.format(
12 *
(billingPeriod === "yearly"
? monthlyPriceAnnualBilling
: monthlyPriceMonthlyBilling),
),
},
)}
</span>
</span>
</p>
<Button
variant="primary"
href={getPremiumSubscriptionUrl({ type: billingPeriod })}
className={styles.cta}
>
{l10n.getString("landing-premium-plans-table-cta-plus-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-plus-label",
)}
</small>
</div>
<hr />
<div className={styles.featuresSection}>
<h4>
{l10n.getString("landing-premium-plans-table-heading-feature")}
</h4>
<ol className={styles.featureList}>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-scan-free",
{
elems: { b: <b /> },
vars: {
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-scan-plus",
{
elems: { b: <b /> },
vars: {
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-removal-plus",
{
elems: { b: <b /> },
},
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-plus-callout",
{
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
)}
</PopoverContent>
</InfoPopover>
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-removal-free",
{
elems: { b: <b /> },
},
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-free-callout",
)}
</PopoverContent>
</InfoPopover>
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getString("landing-premium-plans-cards-feature-alerts")}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-guidance",
{
elems: { b: <b /> },
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getString(
"landing-premium-plans-cards-feature-monitoring",
)}
</span>
</li>
</ol>
</div>
</div>
<div role="group" className={styles.freeCard}>
<div className={styles.head}>
<h3>
{l10n.getString("landing-premium-plans-table-heading-free-title")}
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-free-subtitle",
)}
</p>
</div>
<hr />
<div className={styles.priceSection}>
<p className={styles.billingPeriod}>
{l10n.getString("landing-premium-plans-table-billing-free")}
</p>
<p className={styles.cost}>
<b className={styles.price}>{roundedPriceFormatter.format(0)}</b>
<span className={styles.total} />
</p>
<Button
variant="secondary"
className={styles.cta}
onPress={() => void signIn("fxa")}
>
{l10n.getString("landing-premium-plans-table-cta-free-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-free-label",
)}
</small>
</div>
<hr />
<div className={styles.featuresSection}>
<h4>
{l10n.getString("landing-premium-plans-table-heading-feature")}
</h4>
<ol className={styles.featureList}>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-scan-free",
{
elems: { b: <b /> },
vars: {
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.notIncluded}`}>
<CloseBigIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-not-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-scan-plus",
{
elems: { b: <b /> },
vars: {
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.notIncluded}`}>
<CloseBigIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-not-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-removal-plus",
{
elems: { b: <b /> },
},
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-plus-callout",
{
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
)}
</PopoverContent>
</InfoPopover>
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-removal-free",
{
elems: { b: <b /> },
},
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-free-callout",
)}
</PopoverContent>
</InfoPopover>
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getString("landing-premium-plans-cards-feature-alerts")}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getFragment(
"landing-premium-plans-cards-feature-guidance",
{
elems: { b: <b /> },
},
)}
</span>
</li>
<li className={`${styles.feature} ${styles.included}`}>
<CheckIcon
className={styles.inclusionIcon}
alt={l10n.getString(
"landing-premium-plans-cards-feature-included",
)}
/>
<span>
{l10n.getString(
"landing-premium-plans-cards-feature-monitoring",
)}
</span>
</li>
</ol>
</div>
</div>
</div>
<Table aria-labelledby={props["aria-labelledby"]} selectionMode="none">
<TableHeader>
<Column>
{l10n.getString("landing-premium-plans-table-heading-feature")}
</Column>
<Column>
<h3>
{l10n.getString("landing-premium-plans-table-heading-free-title")}
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-free-subtitle",
)}
</p>
</Column>
<Column>
<b className={styles.badge}>
{l10n.getString("landing-premium-plans-table-annotation-plus")}
</b>
<h3>
{l10n.getFragment(
"landing-premium-plans-table-heading-plus-title",
{ elems: { b: <b /> } },
)}
</h3>
<p>
{l10n.getString(
"landing-premium-plans-table-heading-plus-subtitle",
)}
</p>
</Column>
</TableHeader>
<TableBody>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-scan-label",
{
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
)}
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-scan-free")}
</Cell>
<Cell>
{l10n.getString("landing-premium-plans-table-feature-scan-plus")}
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-removal-label",
)}
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-removal-free",
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-free-callout",
)}
</PopoverContent>
</InfoPopover>
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-removal-plus",
)}
<InfoPopover>
<PopoverContent>
{l10n.getString(
"landing-premium-plans-table-feature-removal-plus-callout",
{
dataBrokerTotalCount: process.env
.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
},
)}
</PopoverContent>
</InfoPopover>
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-alerts-label",
)}
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-alerts-free",
)}
/>
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-alerts-plus",
)}
/>
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-label",
)}
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-free",
)}
</Cell>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-guidance-plus",
)}
</Cell>
</Row>
<Row>
<Cell>
{l10n.getString(
"landing-premium-plans-table-feature-monitoring-label",
)}
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-monitoring-free",
)}
/>
</Cell>
<Cell>
<CheckIcon
className={styles.checkIcon}
alt={l10n.getString(
"landing-premium-plans-table-feature-monitoring-plus",
)}
/>
</Cell>
</Row>
<Row>
<Cell>
<VisuallyHidden>
{l10n.getString("landing-premium-plans-table-billing-label")}
</VisuallyHidden>
</Cell>
<Cell>
<div className={styles.priceCell}>
<p className={styles.billingPeriod}>
{l10n.getString("landing-premium-plans-table-billing-free")}
</p>
<p className={styles.cost}>
<b className={styles.price}>
{roundedPriceFormatter.format(0)}
</b>
<span className={styles.total} />
</p>
<Button variant="secondary" onPress={() => void signIn("fxa")}>
{l10n.getString("landing-premium-plans-table-cta-free-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-free-label",
)}
</small>
</div>
</Cell>
<Cell>
<div className={styles.priceCell}>
<div className={styles.billingPeriod}>
<BillingPeriodToggle
onChange={(newValue) => setBillingPeriod(newValue)}
/>
</div>
<p aria-live="polite" className={styles.cost}>
<b className={styles.price}>
{billingPeriod === "yearly"
? l10n.getString(
"landing-premium-plans-table-price-plus-yearly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceAnnualBilling,
),
},
)
: l10n.getString(
"landing-premium-plans-table-price-plus-monthly",
{
monthlyPrice: priceFormatter.format(
monthlyPriceMonthlyBilling,
),
},
)}
</b>
<span className={styles.total}>
{billingPeriod === "yearly" && (
<em className={styles.discount}>
{l10n.getString(
"landing-premium-plans-table-price-plus-yearly-discount",
{
discountPercentage:
((monthlyPriceMonthlyBilling -
monthlyPriceAnnualBilling) *
100) /
monthlyPriceMonthlyBilling,
},
)}
</em>
)}
<br />
<span className={styles.sum}>
{l10n.getString(
"landing-premium-plans-table-price-plus-yearly-sum",
{
yearlyPrice: priceFormatter.format(
12 *
(billingPeriod === "yearly"
? monthlyPriceAnnualBilling
: monthlyPriceMonthlyBilling),
),
},
)}
</span>
</span>
</p>
<Button
variant="primary"
href={getPremiumSubscriptionUrl({ type: billingPeriod })}
>
{l10n.getString("landing-premium-plans-table-cta-plus-label")}
</Button>
<small className={styles.reassurance}>
{l10n.getString(
"landing-premium-plans-table-reassurance-plus-label",
)}
</small>
</div>
</Cell>
</Row>
</TableBody>
</Table>
</>
);
};

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

@ -12,7 +12,7 @@ import styles from "./Icons.module.scss";
// These components just render HTML without business logic:
/* c8 ignore start */
// Keywords: cross, X
// Keywords: Arrow
export const ArrowIcon = ({
alt,
...props