Make right-arrow skip links work
Unfortunately, this is a fairly significant rework, since the <FixView> needed access to the data required to determine the next step, which often was already available in the relevant page.tsx, so to be able to pass that data into the <FixView>, I had to move it from the layout into every individual page. In addition to the right-arrow skip links, this also sets the correct target for the skip link in the manual broker resolution.
This commit is contained in:
Родитель
1a6e860133
Коммит
3ee2ae07d7
|
@ -80,6 +80,13 @@ const preview: Preview = {
|
|||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/start-free-scan"
|
||||
) {
|
||||
linkTo("Pages/Guided resolution/1a. Free scan")();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers"
|
||||
|
@ -96,6 +103,101 @@ const preview: Preview = {
|
|||
) {
|
||||
linkTo("Pages/Guided resolution/1c. Manually resolve brokers")();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/automatic-remove"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/1d. Automatically resolve brokers"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path === "/redesign/user/dashboard/fix/high-risk-data-breaches/ssn"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/2. High-risk data breaches",
|
||||
"2a. Social Security Number"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/high-risk-data-breaches/credit-card"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/2. High-risk data breaches",
|
||||
"2b. Credit card"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/high-risk-data-breaches/bank-account"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/2. High-risk data breaches",
|
||||
"2c. Bank account"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path === "/redesign/user/dashboard/fix/high-risk-data-breaches/pin"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/2. High-risk data breaches",
|
||||
"2d. PIN"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path === "/redesign/user/dashboard/fix/leaked-passwords/password"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/3. Leaked passwords",
|
||||
"3a. Passwords"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/leaked-passwords/security-question"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/3. Leaked passwords",
|
||||
"3b. Security questions"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/security-recommendations/phone"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/4. Security recommendations",
|
||||
"4a. Phone number"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path ===
|
||||
"/redesign/user/dashboard/fix/security-recommendations/email"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/4. Security recommendations",
|
||||
"4b. Email address"
|
||||
)();
|
||||
}
|
||||
|
||||
if (
|
||||
path === "/redesign/user/dashboard/fix/security-recommendations/ip"
|
||||
) {
|
||||
linkTo(
|
||||
"Pages/Guided resolution/4. Security recommendations",
|
||||
"4c. IP address"
|
||||
)();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -81,7 +81,7 @@ export function createRandomScanResult(
|
|||
}
|
||||
|
||||
export type RandomBreachOptions = Partial<{
|
||||
dataClasses: Array<(typeof BreachDataTypes)[keyof typeof BreachDataTypes]>;
|
||||
dataClasses: SubscriberBreach["dataClasses"];
|
||||
addedDate: Date;
|
||||
isResolved: boolean;
|
||||
dataClassesEffected: DataClassEffected[];
|
||||
|
|
|
@ -7,7 +7,7 @@ import { TabType } from "../View";
|
|||
import { useL10n } from "../../../../../../hooks/l10n";
|
||||
import { DoughnutChart as Chart } from "../../../../../../components/client/Chart";
|
||||
import { DashboardSummary } from "../../../../../../functions/server/dashboard";
|
||||
import { InputData as StepDeterminationData } from "../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { StepDeterminationData } from "../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { DashboardTopBannerContent } from "./DashboardTopBannerContent";
|
||||
|
||||
export type DashboardTopBannerProps = {
|
||||
|
|
|
@ -7,83 +7,32 @@
|
|||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ReactNode } from "react";
|
||||
import { OnerepScanResultRow } from "knex/types/tables";
|
||||
import { FixNavigation } from "../../../../../../components/client/FixNavigation";
|
||||
import styles from "./fix.module.scss";
|
||||
import ImageArrowLeft from "./images/icon-arrow-left.svg";
|
||||
import ImageArrowRight from "./images/icon-arrow-right.svg";
|
||||
import ImageClose from "./images/icon-close.svg";
|
||||
import stepDataBrokerProfilesIcon from "./images/step-counter-data-broker-profiles.svg";
|
||||
import stepHighRiskDataBreachesIcon from "./images/step-counter-high-risk.svg";
|
||||
import stepLeakedPasswordsIcon from "./images/step-counter-leaked-passwords.svg";
|
||||
import stepSecurityRecommendationsIcon from "./images/step-counter-security-recommendations.svg";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { GuidedExperienceBreaches } from "../../../../../../functions/server/getUserBreaches";
|
||||
import { useL10n } from "../../../../../../hooks/l10n";
|
||||
import { stepLinks } from "../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { StepDeterminationData } from "../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
|
||||
export type FixViewProps = {
|
||||
children: ReactNode;
|
||||
breaches: GuidedExperienceBreaches;
|
||||
userScannedResults: OnerepScanResultRow[];
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
nextStepHref: string;
|
||||
currentSection:
|
||||
| "data-broker-profiles"
|
||||
| "high-risk-data-breach"
|
||||
| "leaked-passwords"
|
||||
| "security-recommendations";
|
||||
};
|
||||
|
||||
export const FixView = (props: FixViewProps) => {
|
||||
const pathname = usePathname();
|
||||
const l10n = useL10n();
|
||||
const isResolutionLayout = [
|
||||
"high-risk-data-breach",
|
||||
"leaked-passwords",
|
||||
"security-recommendations",
|
||||
].some((substring) => pathname.includes(substring));
|
||||
const totalHighRiskBreaches = Object.values(props.breaches.highRisk).reduce(
|
||||
(acc, array) => acc + array.length,
|
||||
0
|
||||
);
|
||||
const totalDataBrokerProfiles = props.userScannedResults.length;
|
||||
const totalPasswordBreaches = Object.values(
|
||||
props.breaches.passwordBreaches
|
||||
).reduce((acc, array) => acc + array.length, 0);
|
||||
const totalSecurityRecommendations = Object.values(
|
||||
props.breaches.securityRecommendations
|
||||
).filter((value) => {
|
||||
return value.length > 0;
|
||||
}).length;
|
||||
|
||||
const navigationItemsContent = [
|
||||
{
|
||||
key: "data-broker-profiles",
|
||||
labelStringId: "fix-flow-nav-data-broker-profiles",
|
||||
href: "/redesign/user/dashboard/fix/data-broker-profiles",
|
||||
status: totalDataBrokerProfiles,
|
||||
currentStepId: "dataBrokerProfiles",
|
||||
imageId: stepDataBrokerProfilesIcon,
|
||||
},
|
||||
{
|
||||
key: "high-risk-data-breaches",
|
||||
labelStringId: "fix-flow-nav-high-risk-data-breaches",
|
||||
href: "/redesign/user/dashboard/fix/high-risk-data-breaches",
|
||||
status: totalHighRiskBreaches,
|
||||
currentStepId: "highRiskDataBreaches",
|
||||
imageId: stepHighRiskDataBreachesIcon,
|
||||
},
|
||||
{
|
||||
key: "leaked-passwords",
|
||||
labelStringId: "fix-flow-nav-leaked-passwords",
|
||||
href: "/redesign/user/dashboard/fix/leaked-passwords",
|
||||
status: totalPasswordBreaches,
|
||||
currentStepId: "leakedPasswords",
|
||||
imageId: stepLeakedPasswordsIcon,
|
||||
},
|
||||
{
|
||||
key: "security-recommendations",
|
||||
labelStringId: "fix-flow-nav-security-recommendations",
|
||||
href: "/redesign/user/dashboard/fix/security-recommendations",
|
||||
status: totalSecurityRecommendations,
|
||||
currentStepId: "securityRecommendations",
|
||||
imageId: stepSecurityRecommendationsIcon,
|
||||
},
|
||||
];
|
||||
].includes(props.currentSection);
|
||||
|
||||
const navigationClose = () => {
|
||||
return (
|
||||
|
@ -97,15 +46,6 @@ export const FixView = (props: FixViewProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const currentStepIndex = stepLinks.findIndex((stepLink) =>
|
||||
stepLink.href.startsWith(pathname)
|
||||
);
|
||||
const prevStepHref =
|
||||
currentStepIndex <= 0
|
||||
? "/redesign/user/dashboard"
|
||||
: stepLinks[currentStepIndex - 1].href;
|
||||
const nextStepHref = stepLinks[currentStepIndex + 1].href;
|
||||
|
||||
return (
|
||||
<div className={styles.fixContainer}>
|
||||
<div
|
||||
|
@ -114,22 +54,16 @@ export const FixView = (props: FixViewProps) => {
|
|||
}`}
|
||||
>
|
||||
<FixNavigation
|
||||
navigationItems={navigationItemsContent}
|
||||
pathname={pathname}
|
||||
currentSection={props.currentSection}
|
||||
data={props.data}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
/>
|
||||
{navigationClose()}
|
||||
<section className={styles.fixSection}>
|
||||
<Link
|
||||
className={`${styles.navArrow} ${styles.navArrowBack}`}
|
||||
href={prevStepHref}
|
||||
aria-label={l10n.getString("guided-resolution-flow-back-arrow")}
|
||||
>
|
||||
<Image alt="" src={ImageArrowLeft} />
|
||||
</Link>
|
||||
<div className={styles.viewWrapper}>{props.children}</div>
|
||||
<Link
|
||||
className={`${styles.navArrow} ${styles.navArrowNext}`}
|
||||
href={nextStepHref}
|
||||
href={props.nextStepHref}
|
||||
aria-label={l10n.getString("guided-resolution-flow-next-arrow")}
|
||||
>
|
||||
<Image alt="" src={ImageArrowRight} />
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import { OnerepScanRow } from "knex/types/tables";
|
||||
import { AutomaticRemoveView } from "./AutomaticRemoveView";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createRandomScanResult,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
|
||||
const mockedScan: OnerepScanRow = {
|
||||
created_at: new Date(1998, 2, 31),
|
||||
updated_at: new Date(1998, 2, 31),
|
||||
id: 0,
|
||||
onerep_profile_id: 0,
|
||||
onerep_scan_id: 0,
|
||||
onerep_scan_reason: "initial",
|
||||
onerep_scan_status: "finished",
|
||||
};
|
||||
|
||||
const mockedScanData: LatestOnerepScanData = {
|
||||
scan: mockedScan,
|
||||
results: [...Array(5)].map(() =>
|
||||
createRandomScanResult({ status: "new", manually_resolved: false })
|
||||
),
|
||||
};
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof AutomaticRemoveView> = {
|
||||
title: "Pages/Guided resolution/1d. Automatically resolve brokers",
|
||||
component: AutomaticRemoveView,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AutomaticRemoveView>;
|
||||
|
||||
export const ManualRemoveViewStory: Story = {
|
||||
name: "1d. Automatically resolve brokers",
|
||||
render: () => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<AutomaticRemoveView
|
||||
data={{
|
||||
countryCode: "us",
|
||||
latestScanData: mockedScanData,
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
subscriberEmails={[]}
|
||||
nextStepHref="/redesign/user/dashboard/fix/high-risk-data-breaches/social-security-number"
|
||||
currentSection="data-broker-profiles"
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,166 @@
|
|||
/* 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 React, { ComponentProps, useState } from "react";
|
||||
import styles from "../dataBrokerProfiles.module.scss";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { useL10n } from "../../../../../../../../hooks/l10n";
|
||||
import { FixView } from "../../FixView";
|
||||
|
||||
export type Props = Omit<ComponentProps<typeof FixView>, "children"> & {
|
||||
monthlySubscriptionUrl: string;
|
||||
yearlySubscriptionUrl: string;
|
||||
};
|
||||
|
||||
export function AutomaticRemoveView(props: Props) {
|
||||
const l10n = useL10n();
|
||||
|
||||
const [selectedPlanIsYearly, setSelectedPlanIsYearly] = useState(true);
|
||||
|
||||
const dataBrokerCount = parseInt(
|
||||
process.env.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
|
||||
10
|
||||
);
|
||||
|
||||
const { monthlySubscriptionUrl, yearlySubscriptionUrl, ...fixViewProps } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<FixView {...fixViewProps}>
|
||||
<div>
|
||||
<div className={`${styles.content} ${styles.contentAutomaticRemove}`}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-headline"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-subheadline",
|
||||
{
|
||||
data_broker_count: dataBrokerCount,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.upgradeToggleWrapper}>
|
||||
<div className={styles.upgradeToggle}>
|
||||
<button
|
||||
onClick={() => setSelectedPlanIsYearly(!selectedPlanIsYearly)}
|
||||
className={`${selectedPlanIsYearly ? styles.isActive : ""}`}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-toggle-yearly"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedPlanIsYearly(!selectedPlanIsYearly)}
|
||||
className={`${selectedPlanIsYearly ? "" : styles.isActive}`}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-toggle-monthly"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-save-percent",
|
||||
{ percent: 10 }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.upgradeContentWrapper}>
|
||||
{/* Feature List */}
|
||||
<div className={styles.featuresList}>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-headline"
|
||||
)}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-monthly-scan",
|
||||
{
|
||||
data_broker_count: dataBrokerCount,
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-remove-personal-info"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-guided-experience"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-continuous-monitoring"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-breach-alerts"
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Plan select */}
|
||||
<div className={styles.selectedPlan}>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-headline"
|
||||
)}
|
||||
<small>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-yearly-frequency"
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-monthly-frequency"
|
||||
)}
|
||||
</small>
|
||||
</strong>
|
||||
{/* Price */}
|
||||
<span>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-price",
|
||||
{ price: "X.XX" }
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-price",
|
||||
{ price: "X.XX" }
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={
|
||||
selectedPlanIsYearly
|
||||
? yearlySubscriptionUrl
|
||||
: monthlySubscriptionUrl
|
||||
}
|
||||
onPress={() => (window.location.href = "../../subscribed")} // TODO replace with final UI
|
||||
>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-yearly-button"
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-monthly-button"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FixView>
|
||||
);
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
/* 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 React, { useState } from "react";
|
||||
import styles from "../dataBrokerProfiles.module.scss";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { useL10n } from "../../../../../../../../hooks/l10n";
|
||||
|
||||
interface AutomaticRemoveProps {
|
||||
monthlySubscriptionUrl: string;
|
||||
yearlySubscriptionUrl: string;
|
||||
}
|
||||
|
||||
export default function AutomaticRemove({
|
||||
monthlySubscriptionUrl,
|
||||
yearlySubscriptionUrl,
|
||||
}: AutomaticRemoveProps) {
|
||||
const l10n = useL10n();
|
||||
|
||||
const [selectedPlanIsYearly, setSelectedPlanIsYearly] = useState(true);
|
||||
|
||||
const dataBrokerCount = parseInt(
|
||||
process.env.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
|
||||
10
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${styles.content} ${styles.contentAutomaticRemove}`}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-headline"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-subheadline",
|
||||
{
|
||||
data_broker_count: dataBrokerCount,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.upgradeToggleWrapper}>
|
||||
<div className={styles.upgradeToggle}>
|
||||
<button
|
||||
onClick={() => setSelectedPlanIsYearly(!selectedPlanIsYearly)}
|
||||
className={`${selectedPlanIsYearly ? styles.isActive : ""}`}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-toggle-yearly"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedPlanIsYearly(!selectedPlanIsYearly)}
|
||||
className={`${selectedPlanIsYearly ? "" : styles.isActive}`}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-toggle-monthly"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-save-percent",
|
||||
{ percent: 10 }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.upgradeContentWrapper}>
|
||||
{/* Feature List */}
|
||||
<div className={styles.featuresList}>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-headline"
|
||||
)}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-monthly-scan",
|
||||
{
|
||||
data_broker_count: dataBrokerCount,
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-remove-personal-info"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-guided-experience"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-continuous-monitoring"
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-breach-alerts"
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Plan select */}
|
||||
<div className={styles.selectedPlan}>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-headline"
|
||||
)}
|
||||
<small>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-yearly-frequency"
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-monthly-frequency"
|
||||
)}
|
||||
</small>
|
||||
</strong>
|
||||
{/* Price */}
|
||||
<span>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-price",
|
||||
{ price: "X.XX" }
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-price",
|
||||
{ price: "X.XX" }
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={
|
||||
selectedPlanIsYearly
|
||||
? yearlySubscriptionUrl
|
||||
: monthlySubscriptionUrl
|
||||
}
|
||||
>
|
||||
{selectedPlanIsYearly
|
||||
? l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-yearly-button"
|
||||
)
|
||||
: l10n.getString(
|
||||
"fix-flow-data-broker-profiles-automatic-remove-features-select-plan-monthly-button"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,17 +2,56 @@
|
|||
* 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 React from "react";
|
||||
import { AutomaticRemoveView } from "./AutomaticRemoveView";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { headers } from "next/headers";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import getPremiumSubscriptionUrl from "../../../../../../../../functions/server/getPremiumSubscriptionUrl";
|
||||
import AutomaticRemove from "./View";
|
||||
|
||||
const monthlySubscriptionUrl = getPremiumSubscriptionUrl({ type: "monthly" });
|
||||
const yearlySubscriptionUrl = getPremiumSubscriptionUrl({ type: "yearly" });
|
||||
|
||||
export default function Layout() {
|
||||
export default async function AutomaticRemovePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user?.subscriber?.id) {
|
||||
redirect("/redesign/user/dashboard/");
|
||||
}
|
||||
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
const subBreaches = await getSubscriberBreaches(session.user);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
|
||||
const data: StepDeterminationData = {
|
||||
countryCode: getCountryCode(headers()),
|
||||
latestScanData: scanData,
|
||||
subscriberBreaches: subBreaches,
|
||||
user: session.user,
|
||||
};
|
||||
|
||||
const nextStep = getNextGuidedStep(data, "Scan");
|
||||
|
||||
return (
|
||||
<AutomaticRemove
|
||||
<AutomaticRemoveView
|
||||
data={data}
|
||||
subscriberEmails={subscriberEmails}
|
||||
nextStepHref={nextStep?.href ?? ""}
|
||||
currentSection="data-broker-profiles"
|
||||
monthlySubscriptionUrl={monthlySubscriptionUrl}
|
||||
yearlySubscriptionUrl={yearlySubscriptionUrl}
|
||||
></AutomaticRemove>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,9 +12,7 @@ import {
|
|||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { FixView } from "../../FixView";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { GuidedExperienceBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
|
||||
const mockedScan: OnerepScanRow = {
|
||||
created_at: new Date(1998, 2, 31),
|
||||
|
@ -34,25 +32,6 @@ const mockedScanData: LatestOnerepScanData = {
|
|||
};
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
|
||||
const mockedBreachSummary: GuidedExperienceBreaches = {
|
||||
emails: [],
|
||||
highRisk: {
|
||||
bankBreaches: [],
|
||||
creditCardBreaches: [],
|
||||
pinBreaches: [],
|
||||
ssnBreaches: [],
|
||||
},
|
||||
passwordBreaches: {
|
||||
passwords: [],
|
||||
securityQuestions: [],
|
||||
},
|
||||
securityRecommendations: {
|
||||
emailAddress: [],
|
||||
IPAddress: [],
|
||||
phoneNumber: [],
|
||||
},
|
||||
};
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
|
@ -78,17 +57,13 @@ export const ManualRemoveViewStory: Story = {
|
|||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<FixView
|
||||
breaches={mockedBreachSummary}
|
||||
userScannedResults={mockedScanData.results}
|
||||
>
|
||||
<ManualRemoveView
|
||||
scanData={mockedScanData}
|
||||
breaches={mockedBreaches}
|
||||
countryCode="us"
|
||||
user={mockedSession.user}
|
||||
/>
|
||||
</FixView>
|
||||
<ManualRemoveView
|
||||
scanData={mockedScanData}
|
||||
breaches={mockedBreaches}
|
||||
countryCode="us"
|
||||
user={mockedSession.user}
|
||||
subscriberEmails={[]}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* 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 { it, expect } from "@jest/globals";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
|
||||
jest.mock("../../../../../../../../functions/server/l10n");
|
||||
|
||||
import Meta, { ManualRemoveViewStory } from "./ManualRemove.stories";
|
||||
|
||||
it("passes the axe accessibility test suite", async () => {
|
||||
const ComposedManualRemoveView = composeStory(ManualRemoveViewStory, Meta);
|
||||
const { container } = render(<ComposedManualRemoveView />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("removes the manual resolution button once a profile has been resolved", async () => {
|
||||
const user = userEvent.setup();
|
||||
global.fetch = jest.fn().mockResolvedValueOnce({ ok: true });
|
||||
const ComposedManualRemoveView = composeStory(ManualRemoveViewStory, Meta);
|
||||
render(<ComposedManualRemoveView />);
|
||||
|
||||
const resolveButtonsBeforeResolving = screen.getAllByRole("button", {
|
||||
name: "Mark as fixed",
|
||||
});
|
||||
|
||||
await user.click(resolveButtonsBeforeResolving[0]);
|
||||
|
||||
const resolveButtonsAfterResolving = screen.getAllByRole("button", {
|
||||
name: "Mark as fixed",
|
||||
});
|
||||
expect(resolveButtonsAfterResolving.length).toBeLessThan(
|
||||
resolveButtonsBeforeResolving.length
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the manual resolution button if resolving a profile failed", async () => {
|
||||
const user = userEvent.setup();
|
||||
global.fetch = jest.fn().mockResolvedValueOnce({ ok: false });
|
||||
const ComposedManualRemoveView = composeStory(ManualRemoveViewStory, Meta);
|
||||
render(<ComposedManualRemoveView />);
|
||||
|
||||
const resolveButtonsBeforeResolving = screen.getAllByRole("button", {
|
||||
name: "Mark as fixed",
|
||||
});
|
||||
|
||||
await user.click(resolveButtonsBeforeResolving[0]);
|
||||
|
||||
const resolveButtonsAfterResolving = screen.getAllByRole("button", {
|
||||
name: "Mark as fixed",
|
||||
});
|
||||
expect(resolveButtonsAfterResolving.length).toBe(
|
||||
resolveButtonsBeforeResolving.length
|
||||
);
|
||||
});
|
|
@ -17,13 +17,18 @@ import {
|
|||
} from "../../../../../../../../functions/server/dashboard";
|
||||
import { SubscriberBreach } from "../../../../../../../../../utils/subscriberBreaches";
|
||||
import { RemovalCard } from "./RemovalCard";
|
||||
import { getNextGuidedStep } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { FixView } from "../../FixView";
|
||||
|
||||
export type Props = {
|
||||
scanData: LatestOnerepScanData;
|
||||
breaches: SubscriberBreach[];
|
||||
user: Session["user"];
|
||||
countryCode: string;
|
||||
subscriberEmails: string[];
|
||||
};
|
||||
|
||||
export function ManualRemoveView(props: Props) {
|
||||
|
@ -35,122 +40,130 @@ export function ManualRemoveView(props: Props) {
|
|||
const estimatedTime = countOfDataBrokerProfiles * 10; // 10 minutes per data broker site.
|
||||
const exposureReduction = getExposureReduction(summary);
|
||||
|
||||
const stepAfterSkip = getNextGuidedStep(
|
||||
{
|
||||
countryCode: props.countryCode,
|
||||
latestScanData: props.scanData,
|
||||
subscriberBreaches: props.breaches,
|
||||
user: props.user,
|
||||
},
|
||||
"Scan"
|
||||
);
|
||||
const data: StepDeterminationData = {
|
||||
countryCode: props.countryCode,
|
||||
latestScanData: props.scanData,
|
||||
subscriberBreaches: props.breaches,
|
||||
user: props.user,
|
||||
};
|
||||
|
||||
const stepAfterSkip = getNextGuidedStep(data, "Scan");
|
||||
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-headline"
|
||||
)}
|
||||
</h3>
|
||||
<ol className={styles.removalStepsList}>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-1-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-1-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-2-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-2-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-3-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-3-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-4-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-4-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className={styles.exposureListing}>
|
||||
<h3 className={styles.questionTooltipWrapper}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-review-profiles-headline"
|
||||
)}
|
||||
</h3>
|
||||
<div className={styles.exposureList}>
|
||||
{props.scanData.results.map((scanResult, index) => {
|
||||
return (
|
||||
<RemovalCard
|
||||
key={scanResult.onerep_scan_result_id}
|
||||
scanResult={scanResult}
|
||||
isExpanded={index === 0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<FixView
|
||||
data={data}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
// In practice, there should always be a next step (at least "Done")
|
||||
/* c8 ignore next */
|
||||
nextStepHref={stepAfterSkip?.href ?? ""}
|
||||
currentSection="data-broker-profiles"
|
||||
>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-headline"
|
||||
)}
|
||||
</h3>
|
||||
<ol className={styles.removalStepsList}>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-1-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-1-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-2-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-2-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-3-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-3-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-4-title"
|
||||
)}
|
||||
</strong>
|
||||
<span>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-how-to-remove-step-4-content"
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className={styles.exposureListing}>
|
||||
<h3 className={styles.questionTooltipWrapper}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-review-profiles-headline"
|
||||
)}
|
||||
</h3>
|
||||
<div className={styles.exposureList}>
|
||||
{props.scanData.results.map((scanResult, index) => {
|
||||
return (
|
||||
<RemovalCard
|
||||
key={scanResult.onerep_scan_result_id}
|
||||
scanResult={scanResult}
|
||||
isExpanded={index === 0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/data-broker-profiles/automatic-remove"
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-button-remove-for-me"
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="secondary" href={stepAfterSkip?.href}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-button-skip"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.dataBrokerResolutionStats}>
|
||||
<div>
|
||||
<ClockIcon width="18" height="18" alt="" />
|
||||
{l10n.getString("data-broker-profiles-estimated-time", {
|
||||
estimated_time: estimatedTime,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<AvatarIcon width="18" height="18" alt="" />
|
||||
{l10n.getString("data-broker-profiles-exposure-reduction", {
|
||||
exposure_reduction: exposureReduction,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/data-broker-profiles/automatic-remove"
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-button-remove-for-me"
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="secondary" href={stepAfterSkip?.href}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-manual-remove-button-skip"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.dataBrokerResolutionStats}>
|
||||
<div>
|
||||
<ClockIcon width="18" height="18" alt="" />
|
||||
{l10n.getString("data-broker-profiles-estimated-time", {
|
||||
estimated_time: estimatedTime,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<AvatarIcon width="18" height="18" alt="" />
|
||||
{l10n.getString("data-broker-profiles-exposure-reduction", {
|
||||
exposure_reduction: exposureReduction,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FixView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,8 +11,9 @@ import { getSubscriberBreaches } from "../../../../../../../../functions/server/
|
|||
import { ManualRemoveView } from "./ManualRemoveView";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
|
||||
export default async function ManualRemove() {
|
||||
export default async function ManualRemovePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user?.subscriber?.id) {
|
||||
|
@ -23,12 +24,15 @@ export default async function ManualRemove() {
|
|||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
const subBreaches = await getSubscriberBreaches(session.user);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
|
||||
return (
|
||||
<ManualRemoveView
|
||||
breaches={subBreaches}
|
||||
scanData={scanData}
|
||||
user={session.user}
|
||||
countryCode={getCountryCode(headers())}
|
||||
subscriberEmails={subscriberEmails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import { OnerepScanRow } from "knex/types/tables";
|
||||
import { StartFreeScanView } from "./StartFreeScanView";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createRandomScanResult,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
|
||||
const mockedScan: OnerepScanRow = {
|
||||
created_at: new Date(1998, 2, 31),
|
||||
updated_at: new Date(1998, 2, 31),
|
||||
id: 0,
|
||||
onerep_profile_id: 0,
|
||||
onerep_scan_id: 0,
|
||||
onerep_scan_reason: "initial",
|
||||
onerep_scan_status: "finished",
|
||||
};
|
||||
|
||||
const mockedScanData: LatestOnerepScanData = {
|
||||
scan: mockedScan,
|
||||
results: [...Array(5)].map(() =>
|
||||
createRandomScanResult({ status: "new", manually_resolved: false })
|
||||
),
|
||||
};
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof StartFreeScanView> = {
|
||||
title: "Pages/Guided resolution/1a. Free scan",
|
||||
component: StartFreeScanView,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof StartFreeScanView>;
|
||||
|
||||
export const ManualRemoveViewStory: Story = {
|
||||
name: "1a. Free scan",
|
||||
render: () => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<StartFreeScanView
|
||||
data={{
|
||||
countryCode: "us",
|
||||
latestScanData: mockedScanData,
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
subscriberEmails={[]}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
/* 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 Image from "next/image";
|
||||
import ImageCityScape from "./images/city-scape.svg";
|
||||
import styles from "../dataBrokerProfiles.module.scss";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
import { FixView } from "../../FixView";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
|
||||
export type Props = {
|
||||
data: StepDeterminationData;
|
||||
subscriberEmails: string[];
|
||||
};
|
||||
|
||||
export function StartFreeScanView(props: Props) {
|
||||
const l10n = getL10n();
|
||||
|
||||
return (
|
||||
<FixView
|
||||
data={props.data}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
nextStepHref={getNextGuidedStep(props.data, "Scan")?.href ?? ""}
|
||||
currentSection="data-broker-profiles"
|
||||
>
|
||||
<div className={styles.contentWrapper}>
|
||||
<Image className={styles.cityScape} src={ImageCityScape} alt="" />
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-headline"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-content-p1",
|
||||
{
|
||||
data_broker_count: parseInt(
|
||||
process.env.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
|
||||
10
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-content-p2"
|
||||
)}
|
||||
<a href="#">
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-link-learn-more"
|
||||
)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers"
|
||||
}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-button-start-scan"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={"/redesign/user/dashboard/fix/high-risk-data-breaches"}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-button-skip"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FixView>
|
||||
);
|
||||
}
|
|
@ -3,21 +3,18 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { headers } from "next/headers";
|
||||
import Image from "next/image";
|
||||
import ImageCityScape from "./images/city-scape.svg";
|
||||
import styles from "../dataBrokerProfiles.module.scss";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
|
||||
export default async function StartFreeScan() {
|
||||
const l10n = getL10n();
|
||||
import { StepDeterminationData } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { StartFreeScanView } from "./StartFreeScanView";
|
||||
|
||||
export default async function StartFreeScanPage() {
|
||||
const countryCode = getCountryCode(headers());
|
||||
if (countryCode !== "us") {
|
||||
redirect("/redesign/user/dashboard");
|
||||
|
@ -31,67 +28,24 @@ export default async function StartFreeScan() {
|
|||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const onerepProfileId = result[0]["onerep_profile_id"];
|
||||
|
||||
if (typeof onerepProfileId === "number") {
|
||||
const scanData = await getLatestOnerepScanResults(onerepProfileId);
|
||||
if (scanData.scan !== null) {
|
||||
// If the user already has done a scan, let them view their results:
|
||||
return redirect(
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers"
|
||||
);
|
||||
}
|
||||
const latestScanData =
|
||||
typeof onerepProfileId === "number"
|
||||
? await getLatestOnerepScanResults(onerepProfileId)
|
||||
: undefined;
|
||||
if (latestScanData?.scan) {
|
||||
// If the user already has done a scan, let them view their results:
|
||||
return redirect(
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers"
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.contentWrapper}>
|
||||
<Image className={styles.cityScape} src={ImageCityScape} alt="" />
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-headline"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-content-p1",
|
||||
{
|
||||
data_broker_count: parseInt(
|
||||
process.env.NEXT_PUBLIC_ONEREP_DATA_BROKER_COUNT as string,
|
||||
10
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-content-p2"
|
||||
)}
|
||||
<a href="#">
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-link-learn-more"
|
||||
)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers"
|
||||
}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-button-start-scan"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={"/redesign/user/dashboard/fix/high-risk-data-breaches"}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-start-free-scan-button-skip"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const data: StepDeterminationData = {
|
||||
countryCode: countryCode,
|
||||
user: session.user,
|
||||
latestScanData: latestScanData ?? null,
|
||||
subscriberBreaches: await getSubscriberBreaches(session.user),
|
||||
};
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
|
||||
return <StartFreeScanView data={data} subscriberEmails={subscriberEmails} />;
|
||||
}
|
||||
|
|
|
@ -5,63 +5,78 @@
|
|||
import styles from "../dataBrokerProfiles.module.scss";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
import { DataBrokerProfiles } from "../../../../../../../../components/client/DataBrokerProfiles";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { AboutBrokersIcon } from "./AboutBrokersIcon";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { FixView } from "../../FixView";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
|
||||
export type Props = {
|
||||
scanData: LatestOnerepScanData;
|
||||
data: StepDeterminationData;
|
||||
subscriberEmails: string[];
|
||||
};
|
||||
|
||||
export const ViewDataBrokersView = (props: Props) => {
|
||||
const l10n = getL10n();
|
||||
|
||||
const countOfDataBrokerProfiles = props.scanData.results.length;
|
||||
const countOfDataBrokerProfiles =
|
||||
props.data.latestScanData?.results.length ?? 0;
|
||||
|
||||
const stepAfterSkip = getNextGuidedStep(props.data, "Scan");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-headline",
|
||||
{ data_broker_sites_results_num: countOfDataBrokerProfiles }
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-content"
|
||||
)}
|
||||
</p>
|
||||
<FixView
|
||||
data={props.data}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
nextStepHref={stepAfterSkip?.href ?? ""}
|
||||
currentSection="data-broker-profiles"
|
||||
>
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-headline",
|
||||
{ data_broker_sites_results_num: countOfDataBrokerProfiles }
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-content"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<h4 className={styles.questionTooltipWrapper}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-view-info-on-sites"
|
||||
)}
|
||||
<AboutBrokersIcon />
|
||||
</h4>
|
||||
<DataBrokerProfiles data={props.data.latestScanData?.results ?? []} />
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/data-broker-profiles/automatic-remove"
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-button-remove-for-me"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/manual-remove"
|
||||
}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-button-remove-manually"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<h4 className={styles.questionTooltipWrapper}>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-view-info-on-sites"
|
||||
)}
|
||||
<AboutBrokersIcon />
|
||||
</h4>
|
||||
<DataBrokerProfiles data={props.scanData.results} />
|
||||
</div>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/data-broker-profiles/automatic-remove"
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-button-remove-for-me"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={
|
||||
"/redesign/user/dashboard/fix/data-broker-profiles/manual-remove"
|
||||
}
|
||||
>
|
||||
{l10n.getString(
|
||||
"fix-flow-data-broker-profiles-view-data-broker-profiles-button-remove-manually"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FixView>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,29 +11,8 @@ import {
|
|||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { FixView } from "../../FixView";
|
||||
import { GuidedExperienceBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
|
||||
const mockedBreachesEmpty: GuidedExperienceBreaches = {
|
||||
emails: [],
|
||||
highRisk: {
|
||||
bankBreaches: [],
|
||||
creditCardBreaches: [],
|
||||
pinBreaches: [],
|
||||
ssnBreaches: [],
|
||||
},
|
||||
passwordBreaches: {
|
||||
passwords: [],
|
||||
securityQuestions: [],
|
||||
},
|
||||
securityRecommendations: {
|
||||
emailAddress: [],
|
||||
IPAddress: [],
|
||||
phoneNumber: [],
|
||||
},
|
||||
};
|
||||
|
||||
const brokerOptions = {
|
||||
"no-scan": "No scan started",
|
||||
empty: "No scan results",
|
||||
|
@ -121,12 +100,15 @@ const ViewWrapper = (props: ViewWrapperProps) => {
|
|||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<FixView
|
||||
breaches={mockedBreachesEmpty}
|
||||
userScannedResults={scanData.results}
|
||||
>
|
||||
<ViewDataBrokersView scanData={scanData} />
|
||||
</FixView>
|
||||
<ViewDataBrokersView
|
||||
data={{
|
||||
latestScanData: scanData,
|
||||
countryCode: "us",
|
||||
subscriberBreaches: [],
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
subscriberEmails={[]}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,10 +4,15 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { ViewDataBrokersView } from "./View";
|
||||
import { StepDeterminationData } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
|
||||
export default async function ViewDataBrokers() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
@ -19,6 +24,15 @@ export default async function ViewDataBrokers() {
|
|||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const latestScan = await getLatestOnerepScanResults(profileId);
|
||||
const data: StepDeterminationData = {
|
||||
countryCode: getCountryCode(headers()),
|
||||
user: session.user,
|
||||
latestScanData: latestScan ?? null,
|
||||
subscriberBreaches: await getSubscriberBreaches(session.user),
|
||||
};
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
|
||||
return <ViewDataBrokersView scanData={latestScan} />;
|
||||
return (
|
||||
<ViewDataBrokersView data={data} subscriberEmails={subscriberEmails} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import { OnerepScanRow } from "knex/types/tables";
|
||||
import { WelcomeToPremiumView } from "./WelcomeToPremiumView";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createRandomScanResult,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { LatestOnerepScanData } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
|
||||
const mockedScan: OnerepScanRow = {
|
||||
created_at: new Date(1998, 2, 31),
|
||||
updated_at: new Date(1998, 2, 31),
|
||||
id: 0,
|
||||
onerep_profile_id: 0,
|
||||
onerep_scan_id: 0,
|
||||
onerep_scan_reason: "initial",
|
||||
onerep_scan_status: "finished",
|
||||
};
|
||||
|
||||
const mockedScanData: LatestOnerepScanData = {
|
||||
scan: mockedScan,
|
||||
results: [...Array(5)].map(() =>
|
||||
createRandomScanResult({ status: "new", manually_resolved: false })
|
||||
),
|
||||
};
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WelcomeToPremiumView> = {
|
||||
title: "Pages/Guided resolution/1e. Welcome to Premium",
|
||||
component: WelcomeToPremiumView,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WelcomeToPremiumView>;
|
||||
|
||||
export const ManualRemoveViewStory: Story = {
|
||||
name: "1e. Welcome to Premium",
|
||||
render: () => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<WelcomeToPremiumView
|
||||
data={{
|
||||
countryCode: "us",
|
||||
latestScanData: mockedScanData,
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
subscriberEmails={[]}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/* 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 styles from "./welcomeToPremium.module.scss";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
import { PercentageChart } from "../../../../../../../../components/client/PercentageChart";
|
||||
import {
|
||||
getDashboardSummary,
|
||||
getExposureReduction,
|
||||
} from "../../../../../../../../functions/server/dashboard";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { FixView } from "../../FixView";
|
||||
|
||||
export type Props = {
|
||||
data: StepDeterminationData;
|
||||
subscriberEmails: string[];
|
||||
};
|
||||
|
||||
export function WelcomeToPremiumView(props: Props) {
|
||||
const l10n = getL10n();
|
||||
|
||||
const countOfDataBrokerProfiles =
|
||||
props.data.latestScanData?.results.length ?? 0;
|
||||
const summary = getDashboardSummary(
|
||||
props.data.latestScanData?.results ?? [],
|
||||
props.data.subscriberBreaches
|
||||
);
|
||||
const exposureReduction = getExposureReduction(summary);
|
||||
|
||||
return (
|
||||
<FixView
|
||||
data={props.data}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
nextStepHref={getNextGuidedStep(props.data, "Scan")?.href ?? ""}
|
||||
currentSection="data-broker-profiles"
|
||||
>
|
||||
<div className={styles.contentWrapper}>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-title-part-one"
|
||||
)}
|
||||
<br />
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-title-part-two"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-one",
|
||||
{
|
||||
profile_total_num: countOfDataBrokerProfiles,
|
||||
exposure_reduction_percentage: exposureReduction,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-two"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-three"
|
||||
)}
|
||||
</p>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/high-risk-data-breaches"
|
||||
disabled
|
||||
wide
|
||||
>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-cta-label"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.chart}>
|
||||
<PercentageChart exposureReduction={exposureReduction} />
|
||||
</div>
|
||||
</div>
|
||||
</FixView>
|
||||
);
|
||||
}
|
|
@ -2,24 +2,20 @@
|
|||
* 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 styles from "./welcomeToPremium.module.scss";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { headers } from "next/headers";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { PercentageChart } from "../../../../../../../../components/client/PercentageChart";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import {
|
||||
getDashboardSummary,
|
||||
getExposureReduction,
|
||||
} from "../../../../../../../../functions/server/dashboard";
|
||||
import { Button } from "../../../../../../../../components/server/Button";
|
||||
import { WelcomeToPremiumView } from "./WelcomeToPremiumView";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { StepDeterminationData } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { hasPremium } from "../../../../../../../../functions/universal/user";
|
||||
|
||||
export default async function WelcomeToPremium() {
|
||||
const l10n = getL10n();
|
||||
export default async function WelcomeToPremiumPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user?.subscriber?.id) {
|
||||
|
@ -32,61 +28,19 @@ export default async function WelcomeToPremium() {
|
|||
}
|
||||
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] ?? -1;
|
||||
const scanResultItems =
|
||||
(await getLatestOnerepScanResults(profileId))?.results ?? [];
|
||||
const countOfDataBrokerProfiles = scanResultItems.length;
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
const subBreaches = await getSubscriberBreaches(session.user);
|
||||
const summary = getDashboardSummary(scanResultItems, subBreaches);
|
||||
const exposureReduction = getExposureReduction(summary);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
|
||||
const data: StepDeterminationData = {
|
||||
countryCode: getCountryCode(headers()),
|
||||
latestScanData: scanData,
|
||||
subscriberBreaches: subBreaches,
|
||||
user: session.user,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.contentWrapper}>
|
||||
<div className={styles.content}>
|
||||
<h3>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-title-part-one"
|
||||
)}
|
||||
<br />
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-title-part-two"
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-one",
|
||||
{
|
||||
profile_total_num: countOfDataBrokerProfiles,
|
||||
exposure_reduction_percentage: exposureReduction,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-two"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-description-part-three"
|
||||
)}
|
||||
</p>
|
||||
<div className={styles.buttonsWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
href="/redesign/user/dashboard/fix/high-risk-data-breaches"
|
||||
disabled
|
||||
wide
|
||||
>
|
||||
{l10n.getString(
|
||||
"welcome-to-premium-data-broker-profiles-cta-label"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.chart}>
|
||||
<PercentageChart exposureReduction={exposureReduction} />
|
||||
</div>
|
||||
</div>
|
||||
<WelcomeToPremiumView data={data} subscriberEmails={subscriberEmails} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,8 @@ export const FraudAlertModal = () => {
|
|||
<div className={styles.confirmButtonWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
// TODO: Test dialog closing
|
||||
/* c8 ignore next */
|
||||
onPress={() => overlayTriggerState.close()}
|
||||
autoFocus={true}
|
||||
>
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
|
||||
import { createRandomBreach } from "../../../../../../../../apiMocks/mockData";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
|
||||
|
||||
import creditCardIllustration from "../images/high-risk-data-breach-credit-card.svg";
|
||||
import socialSecurityNumberIllustration from "../images/high-risk-data-breach-ssn.svg";
|
||||
import bankAccountIllustration from "../images/high-risk-data-breach-bank-account.svg";
|
||||
import pinIllustration from "../images/high-risk-data-breach-pin.svg";
|
||||
import noBreachesIllustration from "../images/high-risk-breaches-none.svg";
|
||||
|
||||
const meta: Meta<typeof HighRiskBreachLayout> = {
|
||||
title: "ResolutionLayout",
|
||||
component: HighRiskBreachLayout,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof HighRiskBreachLayout>;
|
||||
|
||||
const scannedResultsArraySample: SubscriberBreach[] = Array.from(
|
||||
{ length: 5 },
|
||||
() => createRandomBreach({ isHighRiskOnly: true })
|
||||
);
|
||||
|
||||
const summaryString = "It appeared in 2 data breaches:";
|
||||
const recommendations = {
|
||||
title: "Here’s what to do",
|
||||
steps: (
|
||||
<ol>
|
||||
<li>Recommendation one</li>
|
||||
<li>Recommendation two</li>
|
||||
<li>Recommendation three</li>
|
||||
</ol>
|
||||
),
|
||||
};
|
||||
const content = {
|
||||
summary: summaryString,
|
||||
description: <p>Security recommendation description text.</p>,
|
||||
recommendations,
|
||||
};
|
||||
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
scannedResultsArraySample,
|
||||
["test@mozilla.com"]
|
||||
);
|
||||
|
||||
export const CreditCard: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "credit-card",
|
||||
title: "Credit card",
|
||||
illustration: creditCardIllustration,
|
||||
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const BankAccount: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "bank-account",
|
||||
title: "Bank account",
|
||||
illustration: bankAccountIllustration,
|
||||
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SSN: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "ssn",
|
||||
title: "SNN",
|
||||
illustration: socialSecurityNumberIllustration,
|
||||
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PIN: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "pin",
|
||||
title: "PIN",
|
||||
illustration: pinIllustration,
|
||||
exposedData: guidedExperienceBreaches.highRisk.ssnBreaches,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const None: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "none",
|
||||
title: "No breaches",
|
||||
illustration: noBreachesIllustration,
|
||||
exposedData: [],
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
/* 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 { render } from "@testing-library/react";
|
||||
import { it, expect } from "@jest/globals";
|
||||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
import Meta, {
|
||||
CreditCard,
|
||||
BankAccount,
|
||||
SSN,
|
||||
PIN,
|
||||
None,
|
||||
} from "./HighRiskBreachLayout.stories";
|
||||
|
||||
it("high risk data breach component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(CreditCard, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("bank account data breach component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(BankAccount, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("SSN data breach component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(SSN, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("PIN data breach component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(PIN, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("Zero state breach component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(None, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
|
@ -9,55 +9,110 @@ import { ResolutionContainer } from "../ResolutionContainer";
|
|||
import { ResolutionContent } from "../ResolutionContent";
|
||||
import { Button } from "../../../../../../../components/server/Button";
|
||||
import { useL10n } from "../../../../../../../hooks/l10n";
|
||||
import { HighRiskBreach } from "./highRiskBreachData";
|
||||
import { getLocale } from "../../../../../../../functions/universal/getLocale";
|
||||
import { FixView } from "../FixView";
|
||||
import {
|
||||
HighRiskBreachTypes,
|
||||
getHighRiskBreachesByType,
|
||||
} from "./highRiskBreachData";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
StepLink,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
|
||||
export type HighRiskBreachLayoutProps = {
|
||||
pageData: HighRiskBreach;
|
||||
type: HighRiskBreachTypes;
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
};
|
||||
|
||||
export function HighRiskBreachLayout({ pageData }: HighRiskBreachLayoutProps) {
|
||||
export function HighRiskBreachLayout(props: HighRiskBreachLayoutProps) {
|
||||
const l10n = useL10n();
|
||||
const { title, illustration, content, exposedData, type } = pageData;
|
||||
|
||||
const stepMap: Record<HighRiskBreachTypes, StepLink["id"]> = {
|
||||
ssn: "HighRiskSsn",
|
||||
"credit-card": "HighRiskCreditCard",
|
||||
"bank-account": "HighRiskBankAccount",
|
||||
pin: "HighRiskPin",
|
||||
none: "HighRiskPin",
|
||||
};
|
||||
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
props.data.subscriberBreaches,
|
||||
props.subscriberEmails
|
||||
);
|
||||
|
||||
const pageData = getHighRiskBreachesByType({
|
||||
dataType: props.type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
// The non-null assertion here should be safe since we already did this check
|
||||
// in `./[type]/page.tsx`:
|
||||
const { title, illustration, content, exposedData, type } = pageData!;
|
||||
const hasBreaches = type !== "none";
|
||||
|
||||
return (
|
||||
<ResolutionContainer
|
||||
type="securityRecommendations"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic + fix event here
|
||||
}}
|
||||
>
|
||||
{hasBreaches
|
||||
? l10n.getString("high-risk-breach-mark-as-fixed")
|
||||
: l10n.getString("high-risk-breach-none-continue")}
|
||||
</Button>
|
||||
{hasBreaches && (
|
||||
<Link
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
href="/"
|
||||
>
|
||||
{l10n.getString("high-risk-breach-skip")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<FixView
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
data={props.data}
|
||||
nextStepHref={
|
||||
// We *should* always match a next step:
|
||||
/* c8 ignore next */
|
||||
getNextGuidedStep(props.data, stepMap[props.type])?.href ?? ""
|
||||
}
|
||||
estimatedTime={hasBreaches ? 15 : undefined}
|
||||
currentSection="high-risk-data-breach"
|
||||
>
|
||||
<ResolutionContent
|
||||
content={content}
|
||||
exposedData={exposedData}
|
||||
locale={getLocale(l10n)}
|
||||
/>
|
||||
</ResolutionContainer>
|
||||
<ResolutionContainer
|
||||
type="securityRecommendations"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic + fix event here
|
||||
}}
|
||||
>
|
||||
{
|
||||
// Theoretically, this page should never be shown if the user
|
||||
// has no breaches, unless the user directly visits its URL, so
|
||||
// no tests represents it either:
|
||||
/* c8 ignore next 3 */
|
||||
hasBreaches
|
||||
? l10n.getString("high-risk-breach-mark-as-fixed")
|
||||
: l10n.getString("high-risk-breach-none-continue")
|
||||
}
|
||||
</Button>
|
||||
{hasBreaches && (
|
||||
<Link
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
href="/"
|
||||
>
|
||||
{l10n.getString("high-risk-breach-skip")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
// Theoretically, this page should never be shown if the user has no
|
||||
// breaches, unless the user directly visits its URL, so no tests
|
||||
// represents it either:
|
||||
/* c8 ignore next */
|
||||
estimatedTime={hasBreaches ? 15 : undefined}
|
||||
>
|
||||
<ResolutionContent
|
||||
content={content}
|
||||
exposedData={exposedData}
|
||||
locale={getLocale(l10n)}
|
||||
/>
|
||||
</ResolutionContainer>
|
||||
</FixView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/* 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 { it, expect } from "@jest/globals";
|
||||
import { render } from "@testing-library/react";
|
||||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
|
||||
import Meta, {
|
||||
BankAccountStory,
|
||||
CreditCardStory,
|
||||
PinStory,
|
||||
SsnStory,
|
||||
} from "./HighRiskDataBreach.stories";
|
||||
|
||||
it("passes the axe accessibility test suite for credit card breaches", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(
|
||||
CreditCardStory,
|
||||
Meta
|
||||
);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("passes the axe accessibility test suite for bank account breaches", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(
|
||||
BankAccountStory,
|
||||
Meta
|
||||
);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("passes the axe accessibility test suite for SSN breaches", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(SsnStory, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("passes the axe accessibility test suite for PIN breaches", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(PinStory, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
|
||||
import { HighRiskBreachTypes } from "../highRiskBreachData";
|
||||
import { BreachDataTypes } from "../../../../../../../../functions/universal/breach";
|
||||
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
// Ensure all high-risk data breaches are present in at least one breach:
|
||||
mockedBreaches.push(
|
||||
createRandomBreach({
|
||||
dataClasses: [
|
||||
BreachDataTypes.SSN,
|
||||
BreachDataTypes.CreditCard,
|
||||
BreachDataTypes.BankAccount,
|
||||
BreachDataTypes.PIN,
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const HighRiskBreachWrapper = (props: { type: HighRiskBreachTypes }) => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<HighRiskBreachLayout
|
||||
subscriberEmails={[]}
|
||||
type={props.type}
|
||||
data={{
|
||||
countryCode: "nl",
|
||||
latestScanData: { results: [], scan: null },
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof HighRiskBreachWrapper> = {
|
||||
title: "Pages/Guided resolution/2. High-risk data breaches",
|
||||
component: HighRiskBreachWrapper,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HighRiskBreachWrapper>;
|
||||
|
||||
export const SsnStory: Story = {
|
||||
name: "2a. Social Security Number",
|
||||
args: {
|
||||
type: "ssn",
|
||||
},
|
||||
};
|
||||
|
||||
export const CreditCardStory: Story = {
|
||||
name: "2b. Credit card",
|
||||
args: {
|
||||
type: "credit-card",
|
||||
},
|
||||
};
|
||||
|
||||
export const BankAccountStory: Story = {
|
||||
name: "2c. Bank account",
|
||||
args: {
|
||||
type: "bank-account",
|
||||
},
|
||||
};
|
||||
|
||||
export const PinStory: Story = {
|
||||
name: "2d. PIN",
|
||||
args: {
|
||||
type: "pin",
|
||||
},
|
||||
};
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { HighRiskBreachLayout } from "../HighRiskBreachLayout";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
|
@ -13,6 +14,10 @@ import {
|
|||
HighRiskBreachTypes,
|
||||
getHighRiskBreachesByType,
|
||||
} from "../highRiskBreachData";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
|
||||
interface SecurityRecommendationsProps {
|
||||
params: {
|
||||
|
@ -27,6 +32,7 @@ export default async function SecurityRecommendations({
|
|||
if (!session?.user?.subscriber?.id) {
|
||||
return redirect("/");
|
||||
}
|
||||
const l10n = getL10n();
|
||||
const breaches = await getSubscriberBreaches(session.user);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
|
@ -38,11 +44,27 @@ export default async function SecurityRecommendations({
|
|||
const pageData = getHighRiskBreachesByType({
|
||||
dataType: type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
if (!pageData) {
|
||||
redirect("/redesign/user/dashboard");
|
||||
}
|
||||
|
||||
return <HighRiskBreachLayout pageData={pageData} />;
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
|
||||
return (
|
||||
<HighRiskBreachLayout
|
||||
subscriberEmails={subscriberEmails}
|
||||
type={type}
|
||||
data={{
|
||||
countryCode: getCountryCode(headers()),
|
||||
subscriberBreaches: breaches,
|
||||
user: session.user,
|
||||
latestScanData: scanData,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { getL10n } from "../../../../../../../functions/server/l10n";
|
||||
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
|
||||
import creditCardIllustration from "../images/high-risk-data-breach-credit-card.svg";
|
||||
import socialSecurityNumberIllustration from "../images/high-risk-data-breach-ssn.svg";
|
||||
|
@ -13,6 +12,7 @@ import noBreachesIllustration from "../images/high-risk-breaches-none.svg";
|
|||
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
|
||||
import { FraudAlertModal } from "./FraudAlertModal";
|
||||
import { getLocale } from "../../../../../../../functions/universal/getLocale";
|
||||
import { ExtendedReactLocalization } from "../../../../../../../hooks/l10n";
|
||||
|
||||
export type HighRiskBreachContent = {
|
||||
summary: string;
|
||||
|
@ -42,12 +42,12 @@ export type HighRiskBreach = {
|
|||
function getHighRiskBreachesByType({
|
||||
dataType,
|
||||
breaches,
|
||||
l10n,
|
||||
}: {
|
||||
dataType: HighRiskBreachTypes;
|
||||
breaches: GuidedExperienceBreaches;
|
||||
l10n: ExtendedReactLocalization;
|
||||
}) {
|
||||
const l10n = getL10n();
|
||||
|
||||
// TODO: Expose email list & count here https://mozilla-hub.atlassian.net/browse/MNTOR-2112
|
||||
const emailsFormatter = new Intl.ListFormat(getLocale(l10n), {
|
||||
style: "long",
|
||||
|
|
|
@ -4,18 +4,24 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { HighRiskBreachLayout } from "./HighRiskBreachLayout";
|
||||
import { authOptions } from "../../../../../../../api/utils/auth";
|
||||
import { getSubscriberEmails } from "../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
import { getSubscriberBreaches } from "../../../../../../../functions/server/getUserBreaches";
|
||||
import { getHighRiskBreachesByType } from "./highRiskBreachData";
|
||||
import { getL10n } from "../../../../../../../functions/server/l10n";
|
||||
import { getOnerepProfileId } from "../../../../../../../../db/tables/subscribers";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../db/tables/onerep_scans";
|
||||
import { getCountryCode } from "../../../../../../../functions/server/getCountryCode";
|
||||
|
||||
export default async function HighRiskDataBreaches() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.subscriber?.id) {
|
||||
return redirect("/");
|
||||
}
|
||||
const l10n = getL10n();
|
||||
const breaches = await getSubscriberBreaches(session.user);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
|
@ -26,16 +32,30 @@ export default async function HighRiskDataBreaches() {
|
|||
const pageData = getHighRiskBreachesByType({
|
||||
dataType: "none",
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
if (!pageData) {
|
||||
redirect("/redesign/user/dashboard/fix/high-risk-data-breaches");
|
||||
}
|
||||
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: MNTOR-1700 Add routing logic here, currently default to no high risk breach data */}
|
||||
<HighRiskBreachLayout pageData={pageData} />
|
||||
<HighRiskBreachLayout
|
||||
subscriberEmails={subscriberEmails}
|
||||
type="none"
|
||||
data={{
|
||||
countryCode: getCountryCode(headers()),
|
||||
subscriberBreaches: breaches,
|
||||
user: session.user,
|
||||
latestScanData: scanData,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,13 +4,8 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../../../api/utils/auth";
|
||||
import { getSubscriberBreaches } from "../../../../../../functions/server/getUserBreaches";
|
||||
import { getSubscriberEmails } from "../../../../../../functions/server/getSubscriberEmails";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ReactNode } from "react";
|
||||
import { FixView } from "./FixView";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../db/tables/onerep_scans";
|
||||
import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers";
|
||||
import { canSubscribeToPremium } from "../../../../../../functions/universal/user";
|
||||
import { getCountryCode } from "../../../../../../functions/server/getCountryCode";
|
||||
|
@ -21,12 +16,6 @@ export default async function Layout({ children }: { children: ReactNode }) {
|
|||
if (!session?.user?.subscriber?.id) {
|
||||
return redirect("/");
|
||||
}
|
||||
const breaches = await getSubscriberBreaches(session.user);
|
||||
const subscriberEmails = await getSubscriberEmails(session.user);
|
||||
const guidedExperience = getGuidedExperienceBreaches(
|
||||
breaches,
|
||||
subscriberEmails
|
||||
);
|
||||
|
||||
const headersList = headers();
|
||||
const countryCode = getCountryCode(headersList);
|
||||
|
@ -39,12 +28,5 @@ export default async function Layout({ children }: { children: ReactNode }) {
|
|||
return redirect("/redesign/user/welcome/");
|
||||
}
|
||||
|
||||
const scanResult = await getLatestOnerepScanResults(profileId);
|
||||
const scanResultItems = scanResult.results;
|
||||
|
||||
return (
|
||||
<FixView breaches={guidedExperience} userScannedResults={scanResultItems}>
|
||||
{children}
|
||||
</FixView>
|
||||
);
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import passwordIllustration from "../images/leaked-passwords.svg";
|
||||
import securityQuestionsIllustration from "../images/security-questions.svg";
|
||||
import { LeakedPasswordsLayout } from "./LeakedPasswordsLayout";
|
||||
|
||||
const meta: Meta<typeof LeakedPasswordsLayout> = {
|
||||
title: "ResolutionLayout",
|
||||
component: LeakedPasswordsLayout,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof LeakedPasswordsLayout>;
|
||||
|
||||
const summaryString = "It appeared in 2 data breaches:";
|
||||
const recommendations = {
|
||||
title: "Here’s what to do",
|
||||
steps: (
|
||||
<ol>
|
||||
<li>Recommendation one</li>
|
||||
<li>Recommendation two</li>
|
||||
<li>Recommendation three</li>
|
||||
</ol>
|
||||
),
|
||||
};
|
||||
const content = {
|
||||
summary: summaryString,
|
||||
description: (
|
||||
<p>
|
||||
Leaked passwords / Security questions leaked recommendation description
|
||||
text.
|
||||
</p>
|
||||
),
|
||||
recommendations,
|
||||
};
|
||||
|
||||
export const Passwords: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "password",
|
||||
title: "Passwords",
|
||||
illustration: passwordIllustration,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SecurityQuestions: Story = {
|
||||
args: {
|
||||
pageData: {
|
||||
type: "security-question",
|
||||
title: "Security questions",
|
||||
illustration: securityQuestionsIllustration,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -8,51 +8,90 @@ import { ResolutionContainer } from "../ResolutionContainer";
|
|||
import { ResolutionContent } from "../ResolutionContent";
|
||||
import { Button } from "../../../../../../../components/server/Button";
|
||||
import { useL10n } from "../../../../../../../hooks/l10n";
|
||||
import { LeakedPassword } from "./leakedPasswordsData";
|
||||
import {
|
||||
LeakedPasswordsTypes,
|
||||
getLeakedPasswords,
|
||||
} from "./leakedPasswordsData";
|
||||
import Link from "next/link";
|
||||
import { getLocale } from "../../../../../../../functions/universal/getLocale";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
StepLink,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { FixView } from "../FixView";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
|
||||
export interface LeakedPasswordsLayoutProps {
|
||||
label: string;
|
||||
pageData: LeakedPassword;
|
||||
type: LeakedPasswordsTypes;
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
}
|
||||
|
||||
export function LeakedPasswordsLayout({
|
||||
pageData,
|
||||
}: LeakedPasswordsLayoutProps) {
|
||||
export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) {
|
||||
const l10n = useL10n();
|
||||
const { title, illustration, content } = pageData;
|
||||
|
||||
const stepMap: Record<LeakedPasswordsTypes, StepLink["id"]> = {
|
||||
password: "LeakedPasswordsPassword",
|
||||
"security-question": "LeakedPasswordsSecurityQuestion",
|
||||
};
|
||||
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
props.data.subscriberBreaches,
|
||||
props.subscriberEmails
|
||||
);
|
||||
|
||||
const pageData = getLeakedPasswords({
|
||||
dataType: props.type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
// The non-null assertion here should be safe since we already did this check
|
||||
// in `./[type]/page.tsx`:
|
||||
const { title, illustration, content } = pageData!;
|
||||
|
||||
return (
|
||||
<ResolutionContainer
|
||||
type="leakedPasswords"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic
|
||||
}}
|
||||
autoFocus={true}
|
||||
>
|
||||
{l10n.getString("leaked-passwords-mark-as-fixed")}
|
||||
</Button>
|
||||
<Link
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
href="/"
|
||||
>
|
||||
{l10n.getString("leaked-passwords-skip")}
|
||||
</Link>
|
||||
</>
|
||||
<FixView
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
data={props.data}
|
||||
nextStepHref={
|
||||
// In practice, there should always be a next step (at least "Done")
|
||||
/* c8 ignore next */
|
||||
getNextGuidedStep(props.data, stepMap[props.type])?.href ?? ""
|
||||
}
|
||||
estimatedTime={4}
|
||||
currentSection="leaked-passwords"
|
||||
>
|
||||
<ResolutionContent content={content} locale={getLocale(l10n)} />
|
||||
</ResolutionContainer>
|
||||
<ResolutionContainer
|
||||
type="leakedPasswords"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic
|
||||
}}
|
||||
autoFocus={true}
|
||||
>
|
||||
{l10n.getString("leaked-passwords-mark-as-fixed")}
|
||||
</Button>
|
||||
<Link
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
href="/"
|
||||
>
|
||||
{l10n.getString("leaked-passwords-skip")}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
estimatedTime={4}
|
||||
>
|
||||
<ResolutionContent content={content} locale={getLocale(l10n)} />
|
||||
</ResolutionContainer>
|
||||
</FixView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { LeakedPasswordsLayout } from "../LeakedPasswordsLayout";
|
||||
import { LeakedPasswordsTypes } from "../leakedPasswordsData";
|
||||
import { BreachDataTypes } from "../../../../../../../../functions/universal/breach";
|
||||
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
// Ensure all leaked passwords data breaches are present in at least one breach:
|
||||
mockedBreaches.push(
|
||||
createRandomBreach({
|
||||
dataClasses: [BreachDataTypes.Passwords, BreachDataTypes.SecurityQuestions],
|
||||
})
|
||||
);
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const LeakedPasswordsWrapper = (props: { type: LeakedPasswordsTypes }) => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<LeakedPasswordsLayout
|
||||
subscriberEmails={[]}
|
||||
type={props.type}
|
||||
data={{
|
||||
countryCode: "nl",
|
||||
latestScanData: { results: [], scan: null },
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof LeakedPasswordsWrapper> = {
|
||||
title: "Pages/Guided resolution/3. Leaked passwords",
|
||||
component: LeakedPasswordsWrapper,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof LeakedPasswordsWrapper>;
|
||||
|
||||
export const PasswordsStory: Story = {
|
||||
name: "3a. Passwords",
|
||||
args: {
|
||||
type: "password",
|
||||
},
|
||||
};
|
||||
|
||||
export const SecurityQuestionsStory: Story = {
|
||||
name: "3b. Security questions",
|
||||
args: {
|
||||
type: "security-question",
|
||||
},
|
||||
};
|
|
@ -7,19 +7,22 @@ import { it, expect } from "@jest/globals";
|
|||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
import Meta, {
|
||||
Passwords,
|
||||
SecurityQuestions,
|
||||
} from "./LeakedPasswordsLayout.stories";
|
||||
PasswordsStory,
|
||||
SecurityQuestionsStory,
|
||||
} from "./LeakedPasswords.stories";
|
||||
|
||||
it("leaked passwords component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(Passwords, Meta);
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(
|
||||
PasswordsStory,
|
||||
Meta
|
||||
);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("security questions component passes the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(
|
||||
SecurityQuestions,
|
||||
SecurityQuestionsStory,
|
||||
Meta
|
||||
);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
|
@ -4,17 +4,24 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
import { LeakedPasswordsLayout } from "../LeakedPasswordsLayout";
|
||||
import { getLeakedPasswords } from "../leakedPasswordsData";
|
||||
import {
|
||||
LeakedPasswordsTypes,
|
||||
getLeakedPasswords,
|
||||
} from "../leakedPasswordsData";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
|
||||
interface LeakedPasswordsProps {
|
||||
params: {
|
||||
type: string;
|
||||
type: LeakedPasswordsTypes;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,16 +44,27 @@ export default async function LeakedPasswords({
|
|||
const pageData = getLeakedPasswords({
|
||||
dataType: type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
if (!pageData) {
|
||||
redirect("/redesign/user/dashboard");
|
||||
}
|
||||
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
|
||||
return (
|
||||
<LeakedPasswordsLayout
|
||||
label={l10n.getString("security-recommendation-steps-label")}
|
||||
pageData={pageData}
|
||||
subscriberEmails={subscriberEmails}
|
||||
type={type}
|
||||
data={{
|
||||
countryCode: getCountryCode(headers()),
|
||||
subscriberBreaches: breaches,
|
||||
user: session.user,
|
||||
latestScanData: scanData,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import passwordIllustration from "../images/leaked-passwords.svg";
|
|||
import securityQuestionsIllustration from "../images/security-questions.svg";
|
||||
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
|
||||
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
|
||||
import { getL10n } from "../../../../../../../functions/server/l10n";
|
||||
import { ExtendedReactLocalization } from "../../../../../../../hooks/l10n";
|
||||
|
||||
export type LeakedPasswordsContent = {
|
||||
summary: string;
|
||||
|
@ -31,12 +31,12 @@ export type LeakedPassword = {
|
|||
function getLeakedPasswords({
|
||||
dataType,
|
||||
breaches,
|
||||
l10n,
|
||||
}: {
|
||||
dataType: string;
|
||||
breaches: GuidedExperienceBreaches;
|
||||
l10n: ExtendedReactLocalization;
|
||||
}) {
|
||||
const l10n = getL10n();
|
||||
|
||||
const findFirstUnresolvedBreach = (breachClassType: LeakedPasswordsTypes) => {
|
||||
const leakedPasswordType =
|
||||
breachClassType === "password" ? "passwords" : "securityQuestions";
|
||||
|
@ -48,9 +48,14 @@ function getLeakedPasswords({
|
|||
const unresolvedPasswordBreach = findFirstUnresolvedBreach("password");
|
||||
const unresolvedSecurityQuestionsBreach =
|
||||
findFirstUnresolvedBreach("security-question");
|
||||
// This env var is always defined in test, so the other branch can't be covered:
|
||||
/* c8 ignore next */
|
||||
const blockList = (process.env.HIBP_BREACH_DOMAIN_BLOCKLIST ?? "").split(",");
|
||||
|
||||
const getBreachInfo = (breach?: SubscriberBreach) => ({
|
||||
// Old code without tests for the case where `breach` is `undefined`
|
||||
// (is that even possible?)
|
||||
/* c8 ignore next 6 */
|
||||
name: breach ? breach.name : "",
|
||||
breachDate: breach ? breach.breachDate : "",
|
||||
breachSite:
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { SecurityRecommendation } from "./securityRecommendationsData";
|
||||
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
|
||||
import { createRandomBreach } from "../../../../../../../../apiMocks/mockData";
|
||||
import { SecurityRecommendationsLayout } from "./SecurityRecommendationsLayout";
|
||||
import phoneIllustration from "../images/security-recommendations-phone.svg";
|
||||
|
||||
const meta: Meta<typeof SecurityRecommendationsLayout> = {
|
||||
title: "SecurityRecommendationsLayout",
|
||||
component: SecurityRecommendationsLayout,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof SecurityRecommendationsLayout>;
|
||||
|
||||
const scannedResultsArraySample: SubscriberBreach[] = Array.from(
|
||||
{ length: 5 },
|
||||
() => createRandomBreach({ isHighRiskOnly: true })
|
||||
);
|
||||
|
||||
const pageDummyData: SecurityRecommendation = {
|
||||
type: "phone",
|
||||
title: "Dummy title",
|
||||
illustration: phoneIllustration,
|
||||
exposedData: scannedResultsArraySample,
|
||||
content: {
|
||||
summary: "It appeared in 2 data breaches:",
|
||||
description: <p>Security recommendatino description text.</p>,
|
||||
recommendations: {
|
||||
title: "Here",
|
||||
steps: (
|
||||
<ol>
|
||||
<li>Recommendation one</li>
|
||||
<li>Recommendation two</li>
|
||||
<li>Recommendation three</li>
|
||||
</ol>
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Layout: Story = {
|
||||
args: {
|
||||
label: "Security recommendations",
|
||||
pageData: pageDummyData,
|
||||
},
|
||||
};
|
||||
|
||||
export const Phone: Story = {
|
||||
args: {
|
||||
label: "Security recommendations",
|
||||
pageData: pageDummyData,
|
||||
},
|
||||
};
|
||||
|
||||
export const Email: Story = {
|
||||
args: {
|
||||
label: "Security recommendations",
|
||||
pageData: { ...pageDummyData, type: "email" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Ip: Story = {
|
||||
args: {
|
||||
label: "Security recommendations",
|
||||
pageData: { ...pageDummyData, type: "ip" },
|
||||
},
|
||||
};
|
|
@ -4,51 +4,92 @@
|
|||
|
||||
"use client";
|
||||
|
||||
import type { SecurityRecommendation } from "./securityRecommendationsData";
|
||||
import {
|
||||
SecurityRecommendationTypes,
|
||||
getSecurityRecommendationsByType,
|
||||
} from "./securityRecommendationsData";
|
||||
import { ResolutionContainer } from "../ResolutionContainer";
|
||||
import { ResolutionContent } from "../ResolutionContent";
|
||||
import { Button } from "../../../../../../../components/server/Button";
|
||||
import { useL10n } from "../../../../../../../hooks/l10n";
|
||||
import { getLocale } from "../../../../../../../functions/universal/getLocale";
|
||||
import { FixView } from "../FixView";
|
||||
import {
|
||||
StepDeterminationData,
|
||||
StepLink,
|
||||
getNextGuidedStep,
|
||||
} from "../../../../../../../functions/server/getRelevantGuidedSteps";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
|
||||
export interface SecurityRecommendationsLayoutProps {
|
||||
label: string;
|
||||
pageData: SecurityRecommendation;
|
||||
type: SecurityRecommendationTypes;
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
}
|
||||
|
||||
export function SecurityRecommendationsLayout({
|
||||
label,
|
||||
pageData,
|
||||
}: SecurityRecommendationsLayoutProps) {
|
||||
export function SecurityRecommendationsLayout(
|
||||
props: SecurityRecommendationsLayoutProps
|
||||
) {
|
||||
const l10n = useL10n();
|
||||
const { title, illustration, content, exposedData } = pageData;
|
||||
|
||||
const stepMap: Record<SecurityRecommendationTypes, StepLink["id"]> = {
|
||||
email: "SecurityTipsEmail",
|
||||
ip: "SecurityTipsIp",
|
||||
phone: "SecurityTipsPhone",
|
||||
};
|
||||
|
||||
const guidedExperienceBreaches = getGuidedExperienceBreaches(
|
||||
props.data.subscriberBreaches,
|
||||
props.subscriberEmails
|
||||
);
|
||||
|
||||
const pageData = getSecurityRecommendationsByType({
|
||||
dataType: props.type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
// The non-null assertion here should be safe since we already did this check
|
||||
// in `./[type]/page.tsx`:
|
||||
const { title, illustration, content, exposedData } = pageData!;
|
||||
|
||||
return (
|
||||
<ResolutionContainer
|
||||
label={label}
|
||||
type="securityRecommendations"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic
|
||||
}}
|
||||
autoFocus={true}
|
||||
>
|
||||
{l10n.getString("security-recommendation-steps-cta-label")}
|
||||
</Button>
|
||||
<FixView
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
data={props.data}
|
||||
nextStepHref={
|
||||
// In practice, there should always be a next step (at least "Done")
|
||||
/* c8 ignore next */
|
||||
getNextGuidedStep(props.data, stepMap[props.type])?.href ?? ""
|
||||
}
|
||||
currentSection="security-recommendations"
|
||||
>
|
||||
<ResolutionContent
|
||||
content={content}
|
||||
exposedData={exposedData}
|
||||
locale={getLocale(l10n)}
|
||||
/>
|
||||
</ResolutionContainer>
|
||||
<ResolutionContainer
|
||||
label={l10n.getString("security-recommendation-steps-label")}
|
||||
type="securityRecommendations"
|
||||
title={title}
|
||||
illustration={illustration}
|
||||
cta={
|
||||
<Button
|
||||
variant="primary"
|
||||
small
|
||||
// TODO: Add test once MNTOR-1700 logic is added
|
||||
/* c8 ignore next 3 */
|
||||
onPress={() => {
|
||||
// TODO: MNTOR-1700 Add routing logic
|
||||
}}
|
||||
autoFocus={true}
|
||||
>
|
||||
{l10n.getString("security-recommendation-steps-cta-label")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ResolutionContent
|
||||
content={content}
|
||||
exposedData={exposedData}
|
||||
locale={getLocale(l10n)}
|
||||
/>
|
||||
</ResolutionContainer>
|
||||
</FixView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/* 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 type { Meta, StoryObj } from "@storybook/react";
|
||||
import {
|
||||
createRandomBreach,
|
||||
createUserWithPremiumSubscription,
|
||||
} from "../../../../../../../../../apiMocks/mockData";
|
||||
import { Shell } from "../../../../../../Shell";
|
||||
import { getEnL10nSync } from "../../../../../../../../functions/server/mockL10n";
|
||||
import { SecurityRecommendationsLayout } from "../SecurityRecommendationsLayout";
|
||||
import { SecurityRecommendationTypes } from "../securityRecommendationsData";
|
||||
import { BreachDataTypes } from "../../../../../../../../functions/universal/breach";
|
||||
|
||||
const mockedBreaches = [...Array(5)].map(() => createRandomBreach());
|
||||
// Ensure all security recommendation data breaches are present in at least one breach:
|
||||
mockedBreaches.push(
|
||||
createRandomBreach({
|
||||
dataClasses: [
|
||||
BreachDataTypes.Phone,
|
||||
BreachDataTypes.Email,
|
||||
BreachDataTypes.IP,
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const user = createUserWithPremiumSubscription();
|
||||
|
||||
const mockedSession = {
|
||||
expires: new Date().toISOString(),
|
||||
user: user,
|
||||
};
|
||||
|
||||
const SecurityRecommendationsWrapper = (props: {
|
||||
type: SecurityRecommendationTypes;
|
||||
}) => {
|
||||
return (
|
||||
<Shell
|
||||
l10n={getEnL10nSync()}
|
||||
session={mockedSession}
|
||||
nonce=""
|
||||
monthlySubscriptionUrl=""
|
||||
yearlySubscriptionUrl=""
|
||||
>
|
||||
<SecurityRecommendationsLayout
|
||||
subscriberEmails={[]}
|
||||
type={props.type}
|
||||
data={{
|
||||
countryCode: "nl",
|
||||
latestScanData: { results: [], scan: null },
|
||||
subscriberBreaches: mockedBreaches,
|
||||
user: mockedSession.user,
|
||||
}}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof SecurityRecommendationsWrapper> = {
|
||||
title: "Pages/Guided resolution/4. Security recommendations",
|
||||
component: SecurityRecommendationsWrapper,
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SecurityRecommendationsWrapper>;
|
||||
|
||||
export const PhoneStory: Story = {
|
||||
name: "4a. Phone number",
|
||||
args: {
|
||||
type: "phone",
|
||||
},
|
||||
};
|
||||
|
||||
export const EmailStory: Story = {
|
||||
name: "4b. Email address",
|
||||
args: {
|
||||
type: "email",
|
||||
},
|
||||
};
|
||||
|
||||
export const IpStory: Story = {
|
||||
name: "4c. IP address",
|
||||
args: {
|
||||
type: "ip",
|
||||
},
|
||||
};
|
|
@ -6,22 +6,26 @@ import { render } from "@testing-library/react";
|
|||
import { it, expect } from "@jest/globals";
|
||||
import { composeStory } from "@storybook/react";
|
||||
import { axe } from "jest-axe";
|
||||
import Meta, { Phone, Email, Ip } from "./SecurityRecommendations.stories";
|
||||
import Meta, {
|
||||
EmailStory,
|
||||
IpStory,
|
||||
PhoneStory,
|
||||
} from "./SecurityRecommendations.stories";
|
||||
|
||||
it("Phone security recommendations the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(Phone, Meta);
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(PhoneStory, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("Email security recommendations the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(Email, Meta);
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(EmailStory, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it("IP security recommendations the axe accessibility test suite", async () => {
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(Ip, Meta);
|
||||
const ComposedHighRiskDataBreachComponent = composeStory(IpStory, Meta);
|
||||
const { container } = render(<ComposedHighRiskDataBreachComponent />);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
});
|
|
@ -4,17 +4,24 @@
|
|||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { SecurityRecommendationsLayout } from "../SecurityRecommendationsLayout";
|
||||
import { getSecurityRecommendationsByType } from "../securityRecommendationsData";
|
||||
import {
|
||||
SecurityRecommendationTypes,
|
||||
getSecurityRecommendationsByType,
|
||||
} from "../securityRecommendationsData";
|
||||
import { authOptions } from "../../../../../../../../api/utils/auth";
|
||||
import { getSubscriberBreaches } from "../../../../../../../../functions/server/getUserBreaches";
|
||||
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
|
||||
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
|
||||
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
|
||||
import { getOnerepProfileId } from "../../../../../../../../../db/tables/subscribers";
|
||||
import { getLatestOnerepScanResults } from "../../../../../../../../../db/tables/onerep_scans";
|
||||
import { getL10n } from "../../../../../../../../functions/server/l10n";
|
||||
|
||||
interface SecurityRecommendationsProps {
|
||||
params: {
|
||||
type: string;
|
||||
type: SecurityRecommendationTypes;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,16 +44,27 @@ export default async function SecurityRecommendations({
|
|||
const pageData = getSecurityRecommendationsByType({
|
||||
dataType: type,
|
||||
breaches: guidedExperienceBreaches,
|
||||
l10n: l10n,
|
||||
});
|
||||
|
||||
if (!pageData) {
|
||||
redirect("/redesign/user/dashboard");
|
||||
}
|
||||
|
||||
const result = await getOnerepProfileId(session.user.subscriber.id);
|
||||
const profileId = result[0]["onerep_profile_id"] as number;
|
||||
const scanData = await getLatestOnerepScanResults(profileId);
|
||||
|
||||
return (
|
||||
<SecurityRecommendationsLayout
|
||||
label={l10n.getString("security-recommendation-steps-label")}
|
||||
pageData={pageData}
|
||||
subscriberEmails={subscriberEmails}
|
||||
type={type}
|
||||
data={{
|
||||
countryCode: getCountryCode(headers()),
|
||||
subscriberBreaches: breaches,
|
||||
user: session.user,
|
||||
latestScanData: scanData,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import type { ReactNode } from "react";
|
|||
import emailIllustration from "../images/security-recommendations-email.svg";
|
||||
import phoneIllustration from "../images/security-recommendations-phone.svg";
|
||||
import ipIllustration from "../images/security-recommendations-ip.svg";
|
||||
import { getL10n } from "../../../../../../../functions/server/l10n";
|
||||
import { GuidedExperienceBreaches } from "../../../../../../../functions/server/getUserBreaches";
|
||||
import { SubscriberBreach } from "../../../../../../../../utils/subscriberBreaches";
|
||||
import { ExtendedReactLocalization } from "../../../../../../../hooks/l10n";
|
||||
|
||||
export type SecurityRecommendationContent = {
|
||||
summary: string;
|
||||
|
@ -33,12 +33,12 @@ export type SecurityRecommendation = {
|
|||
function getSecurityRecommendationsByType({
|
||||
dataType,
|
||||
breaches,
|
||||
l10n,
|
||||
}: {
|
||||
dataType: string;
|
||||
breaches: GuidedExperienceBreaches;
|
||||
l10n: ExtendedReactLocalization;
|
||||
}) {
|
||||
const l10n = getL10n();
|
||||
|
||||
const securityRecommendationsData: SecurityRecommendation[] = [
|
||||
{
|
||||
type: "phone",
|
||||
|
|
|
@ -6,114 +6,181 @@
|
|||
|
||||
import Image from "next/image";
|
||||
import styles from "./FixNavigation.module.scss";
|
||||
import { useState } from "react";
|
||||
import stepDataBrokerProfilesIcon from "../../(proper_react)/redesign/(authenticated)/user/dashboard/fix/images/step-counter-data-broker-profiles.svg";
|
||||
import stepHighRiskDataBreachesIcon from "../../(proper_react)/redesign/(authenticated)/user/dashboard/fix/images/step-counter-high-risk.svg";
|
||||
import stepLeakedPasswordsIcon from "../../(proper_react)/redesign/(authenticated)/user/dashboard/fix/images/step-counter-leaked-passwords.svg";
|
||||
import stepSecurityRecommendationsIcon from "../../(proper_react)/redesign/(authenticated)/user/dashboard/fix/images/step-counter-security-recommendations.svg";
|
||||
import { useL10n } from "../../hooks/l10n";
|
||||
|
||||
type StepId =
|
||||
| "dataBrokerProfiles"
|
||||
| "highRiskDataBreaches"
|
||||
| "leakedPasswords"
|
||||
| "securityRecommendations";
|
||||
import { StepDeterminationData } from "../../functions/server/getRelevantGuidedSteps";
|
||||
import { getGuidedExperienceBreaches } from "../../functions/universal/guidedExperienceBreaches";
|
||||
|
||||
export type Props = {
|
||||
navigationItems: Array<NavigationItem>;
|
||||
pathname: string;
|
||||
currentSection:
|
||||
| "data-broker-profiles"
|
||||
| "high-risk-data-breach"
|
||||
| "leaked-passwords"
|
||||
| "security-recommendations";
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
};
|
||||
|
||||
export const FixNavigation = (props: Props) => {
|
||||
// TODO: Add logic to abstract away from hard-coded dataBrokerProfiles section
|
||||
const [showDataBrokerProfiles] = useState(true);
|
||||
const [currentStep, setCurrentStep] = useState<StepId>("dataBrokerProfiles");
|
||||
|
||||
if (!showDataBrokerProfiles) {
|
||||
setCurrentStep("highRiskDataBreaches");
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={styles.stepsWrapper}>
|
||||
<Steps
|
||||
navigationItems={props.navigationItems}
|
||||
showDataBrokerProfiles={showDataBrokerProfiles}
|
||||
currentStep={currentStep}
|
||||
pathname={props.pathname}
|
||||
currentSection={props.currentSection}
|
||||
subscriberEmails={props.subscriberEmails}
|
||||
data={props.data}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export interface NavigationItem {
|
||||
key: string;
|
||||
labelStringId: string;
|
||||
href: string;
|
||||
status: string | number;
|
||||
currentStepId: string;
|
||||
imageId: string;
|
||||
}
|
||||
|
||||
export const Steps = (props: {
|
||||
showDataBrokerProfiles: boolean;
|
||||
currentStep: StepId;
|
||||
navigationItems: Array<NavigationItem>;
|
||||
pathname: string;
|
||||
currentSection: Props["currentSection"];
|
||||
subscriberEmails: string[];
|
||||
data: StepDeterminationData;
|
||||
}) => {
|
||||
const l10n = useL10n();
|
||||
|
||||
function calculateActiveProgressBarPosition(pathname: string) {
|
||||
if (pathname === "/redesign/user/dashboard/fix/high-risk-data-breaches") {
|
||||
return styles.beginHighRiskDataBreaches;
|
||||
} else if (
|
||||
pathname.startsWith(
|
||||
"/redesign/user/dashboard/fix/high-risk-data-breaches"
|
||||
)
|
||||
) {
|
||||
return styles.duringHighRiskDataBreaches;
|
||||
} else if (pathname === "/redesign/user/dashboard/fix/leaked-passwords") {
|
||||
return styles.beginLeakedPasswords;
|
||||
} else if (
|
||||
pathname.startsWith("/redesign/user/dashboard/fix/leaked-passwords")
|
||||
) {
|
||||
return styles.duringLeakedPasswords;
|
||||
} else if (
|
||||
pathname === "/redesign/user/dashboard/fix/security-recommendations"
|
||||
) {
|
||||
return styles.beginSecurityRecommendations;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
const breachesByClassification = getGuidedExperienceBreaches(
|
||||
props.data.subscriberBreaches,
|
||||
props.subscriberEmails
|
||||
);
|
||||
const totalHighRiskBreaches = Object.values(
|
||||
breachesByClassification.highRisk
|
||||
).reduce((acc, array) => acc + array.length, 0);
|
||||
const totalDataBrokerProfiles =
|
||||
// No tests simulate the absence of scan data yet:
|
||||
/* c8 ignore next */
|
||||
props.data.latestScanData?.results.length ?? 0;
|
||||
const totalPasswordBreaches = Object.values(
|
||||
breachesByClassification.passwordBreaches
|
||||
).reduce((acc, array) => acc + array.length, 0);
|
||||
const totalSecurityRecommendations = Object.values(
|
||||
breachesByClassification.securityRecommendations
|
||||
).filter((value) => {
|
||||
return value.length > 0;
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<ul className={styles.steps}>
|
||||
{props.navigationItems.map(
|
||||
({ key, labelStringId, href, imageId, status }) => (
|
||||
<li
|
||||
key={key}
|
||||
aria-current={props.pathname.includes(href) ? "step" : undefined}
|
||||
className={`${styles.navigationItem} ${
|
||||
props.pathname.includes(href) ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.stepIcon}>
|
||||
<Image src={imageId} alt="" width={22} height={22} />
|
||||
{/* // TODO: Add logic to mark icon as checked when step is complete */}
|
||||
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
|
||||
</div>
|
||||
<li
|
||||
aria-current={
|
||||
props.currentSection === "data-broker-profiles" ? "step" : undefined
|
||||
}
|
||||
className={`${styles.navigationItem} ${
|
||||
props.currentSection === "data-broker-profiles" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.stepIcon}>
|
||||
<Image
|
||||
src={stepDataBrokerProfilesIcon}
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
{/* // TODO: Add logic to mark icon as checked when step is complete */}
|
||||
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
|
||||
</div>
|
||||
|
||||
<div className={styles.stepLabel}>
|
||||
{l10n.getString(labelStringId)} ({status})
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<div className={styles.stepLabel}>
|
||||
{l10n.getString("fix-flow-nav-data-broker-profiles")} (
|
||||
{totalDataBrokerProfiles})
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-current={
|
||||
props.currentSection === "high-risk-data-breach" ? "step" : undefined
|
||||
}
|
||||
className={`${styles.navigationItem} ${
|
||||
props.currentSection === "high-risk-data-breach" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.stepIcon}>
|
||||
<Image
|
||||
src={stepHighRiskDataBreachesIcon}
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
{/* // TODO: Add logic to mark icon as checked when step is complete */}
|
||||
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
|
||||
</div>
|
||||
|
||||
<div className={styles.stepLabel}>
|
||||
{l10n.getString("fix-flow-nav-high-risk-data-breaches")} (
|
||||
{totalHighRiskBreaches})
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-current={
|
||||
props.currentSection === "leaked-passwords" ? "step" : undefined
|
||||
}
|
||||
className={`${styles.navigationItem} ${
|
||||
props.currentSection === "leaked-passwords" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.stepIcon}>
|
||||
<Image src={stepLeakedPasswordsIcon} alt="" width={22} height={22} />
|
||||
{/* // TODO: Add logic to mark icon as checked when step is complete */}
|
||||
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
|
||||
</div>
|
||||
|
||||
<div className={styles.stepLabel}>
|
||||
{l10n.getString("fix-flow-nav-leaked-passwords")} (
|
||||
{totalPasswordBreaches})
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-current={
|
||||
props.currentSection === "security-recommendations"
|
||||
? "step"
|
||||
: undefined
|
||||
}
|
||||
className={`${styles.navigationItem} ${
|
||||
props.currentSection === "security-recommendations"
|
||||
? styles.active
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className={styles.stepIcon}>
|
||||
<Image
|
||||
src={stepSecurityRecommendationsIcon}
|
||||
alt=""
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
{/* // TODO: Add logic to mark icon as checked when step is complete */}
|
||||
{/* <CheckIcon className={styles.checkIcon} alt="" width={22} height={22} /> */}
|
||||
</div>
|
||||
|
||||
<div className={styles.stepLabel}>
|
||||
{l10n.getString("fix-flow-nav-security-recommendations")} (
|
||||
{totalSecurityRecommendations})
|
||||
</div>
|
||||
</li>
|
||||
<li className={styles.progressBarLineContainer}>
|
||||
<div className={styles.progressBarLineWrapper}>
|
||||
<div
|
||||
className={`${
|
||||
styles.activeProgressBarLine
|
||||
} ${calculateActiveProgressBarPosition(props.pathname)}`}
|
||||
} ${calculateActiveProgressBarPosition(props.currentSection)}`}
|
||||
></div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
function calculateActiveProgressBarPosition(section: Props["currentSection"]) {
|
||||
if (section === "high-risk-data-breach") {
|
||||
return styles.beginHighRiskDataBreaches;
|
||||
} else if (section === "leaked-passwords") {
|
||||
return styles.beginLeakedPasswords;
|
||||
} else if (section === "security-recommendations") {
|
||||
return styles.beginSecurityRecommendations;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/* 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 { getEnL10nBundlesInNodeContext, getEnL10nSync } from "../mockL10n";
|
||||
|
||||
export const getL10nBundles = getEnL10nBundlesInNodeContext;
|
||||
|
||||
export const getL10n = getEnL10nSync;
|
|
@ -7,7 +7,7 @@ import { LatestOnerepScanData } from "../../../db/tables/onerep_scans";
|
|||
import { SubscriberBreach } from "../../../utils/subscriberBreaches";
|
||||
import { BreachDataTypes, HighRiskDataTypes } from "../universal/breach";
|
||||
|
||||
export type InputData = {
|
||||
export type StepDeterminationData = {
|
||||
user: Session["user"];
|
||||
countryCode: string;
|
||||
latestScanData: LatestOnerepScanData | null;
|
||||
|
@ -34,7 +34,7 @@ export const stepLinks = [
|
|||
id: "HighRiskBankAccount",
|
||||
},
|
||||
{
|
||||
href: "/redesign/user/dashboard/fix/high-risk-data-breaches/pin-number",
|
||||
href: "/redesign/user/dashboard/fix/high-risk-data-breaches/pin",
|
||||
id: "HighRiskPin",
|
||||
},
|
||||
{
|
||||
|
@ -69,11 +69,6 @@ export type StepLinkWithStatus = (typeof stepLinks)[number] & {
|
|||
completed: boolean;
|
||||
};
|
||||
|
||||
export type OutputData = {
|
||||
current: StepLink | null;
|
||||
skipTarget: StepLink | null;
|
||||
};
|
||||
|
||||
export function isGuidedResolutionInProgress(stepId: StepLink["id"]) {
|
||||
const inProgressStepIds = stepLinks
|
||||
.filter((step) => step.id !== "Scan" && step.id !== "Done")
|
||||
|
@ -82,7 +77,7 @@ export function isGuidedResolutionInProgress(stepId: StepLink["id"]) {
|
|||
}
|
||||
|
||||
export function getNextGuidedStep(
|
||||
data: InputData,
|
||||
data: StepDeterminationData,
|
||||
afterStep?: StepLink["id"]
|
||||
): StepLink | null {
|
||||
// Resisting the urge to add a state machine... ^.^
|
||||
|
@ -93,15 +88,19 @@ export function getNextGuidedStep(
|
|||
return stepLink.eligible && !stepLink.completed;
|
||||
});
|
||||
|
||||
// In practice, there should always be a next step (at least "Done")
|
||||
/* c8 ignore next */
|
||||
return nextStep ?? null;
|
||||
}
|
||||
|
||||
export function getGuidedStepStatuses(data: InputData): StepLinkWithStatus[] {
|
||||
export function getGuidedStepStatuses(
|
||||
data: StepDeterminationData
|
||||
): StepLinkWithStatus[] {
|
||||
return stepLinks.map((stepLink) => getStepWithStatus(data, stepLink));
|
||||
}
|
||||
|
||||
function getStepWithStatus(
|
||||
data: InputData,
|
||||
data: StepDeterminationData,
|
||||
stepLink: StepLink
|
||||
): StepLinkWithStatus {
|
||||
return {
|
||||
|
@ -111,7 +110,10 @@ function getStepWithStatus(
|
|||
};
|
||||
}
|
||||
|
||||
function isEligibleFor(data: InputData, stepId: StepLink["id"]): boolean {
|
||||
function isEligibleFor(
|
||||
data: StepDeterminationData,
|
||||
stepId: StepLink["id"]
|
||||
): boolean {
|
||||
if (stepId === "Scan") {
|
||||
return data.countryCode === "us";
|
||||
}
|
||||
|
@ -157,7 +159,10 @@ function isEligibleFor(data: InputData, stepId: StepLink["id"]): boolean {
|
|||
return false as never;
|
||||
}
|
||||
|
||||
function hasCompleted(data: InputData, stepId: StepLink["id"]): boolean {
|
||||
function hasCompleted(
|
||||
data: StepDeterminationData,
|
||||
stepId: StepLink["id"]
|
||||
): boolean {
|
||||
if (stepId === "Scan") {
|
||||
const hasRunScan =
|
||||
typeof data.latestScanData?.scan === "object" &&
|
||||
|
|
Загрузка…
Ссылка в новой задаче