Add small-screen version of the pricing table
This commit is contained in:
Родитель
b130d72651
Коммит
78e8f20347
|
@ -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",
|
||||
•
|
||||
</>
|
||||
)}
|
||||
</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
|
||||
|
|
Загрузка…
Ссылка в новой задаче