diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/HighRiskBreachLayout.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/HighRiskBreachLayout.tsx index 1870630a3fd..d0036b82b69 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/HighRiskBreachLayout.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/HighRiskBreachLayout.tsx @@ -4,7 +4,9 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { ResolutionContainer } from "../ResolutionContainer"; import { ResolutionContent } from "../ResolutionContent"; import { Button } from "../../../../../../../components/server/Button"; @@ -22,6 +24,8 @@ import { } from "../../../../../../../functions/server/getRelevantGuidedSteps"; import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches"; import { hasPremium } from "../../../../../../../functions/universal/user"; +import { HighRiskDataTypes } from "../../../../../../../functions/universal/breach"; +import { BreachBulkResolutionRequest } from "../../../../../../../(nextjs_migration)/(authenticated)/user/breaches/breaches"; export type HighRiskBreachLayoutProps = { type: HighRiskBreachTypes; @@ -31,6 +35,8 @@ export type HighRiskBreachLayoutProps = { export function HighRiskBreachLayout(props: HighRiskBreachLayoutProps) { const l10n = useL10n(); + const router = useRouter(); + const [isResolving, setIsResolving] = useState(false); const stepMap: Record = { ssn: "HighRiskSsn", @@ -57,8 +63,63 @@ export function HighRiskBreachLayout(props: HighRiskBreachLayoutProps) { // 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"; + const isHighRiskBreachesStep = type !== "none"; const isStepDone = type === "done"; + const hasExposedData = exposedData.length > 0; + + // TODO: Write unit tests MNTOR-2560 + /* c8 ignore start */ + const handlePrimaryButtonPress = async () => { + const highRiskBreachClasses: Record< + HighRiskBreachTypes, + (typeof HighRiskDataTypes)[keyof typeof HighRiskDataTypes] | null + > = { + ssn: HighRiskDataTypes.SSN, + "credit-card": HighRiskDataTypes.CreditCard, + "bank-account": HighRiskDataTypes.BankAccount, + pin: HighRiskDataTypes.PIN, + none: null, + done: null, + }; + + const dataType = highRiskBreachClasses[type]; + // Only attempt to resolve the breaches if the following conditions are true: + // - There is a matching data class type in this step + // - The current step has unresolved exposed data + // - There is no pending breach resolution request + if (!dataType || !hasExposedData || isResolving) { + return; + } + + setIsResolving(true); + try { + const body: BreachBulkResolutionRequest = { dataType }; + const response = await fetch("/api/v1/user/breaches/bulk-resolve", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + const result = await response.json(); + if (!result?.success) { + throw new Error( + `Could not resolve breach data class of type: ${props.type}`, + ); + } + + const isCurrentStepSection = Object.values(stepMap).includes(nextStep.id); + const nextRoute = isCurrentStepSection + ? nextStep.href + : "/redesign/user/dashboard/fix/high-risk-data-breaches/done"; + router.push(nextRoute); + } catch (_error) { + // TODO: MNTOR-2563: Capture client error with @next/sentry + setIsResolving(false); + } + }; + /* c8 ignore stop */ return ( { - // TODO: MNTOR-1700 Add routing logic + fix event here - }} + autoFocus={true} + /* c8 ignore next */ + onPress={() => void handlePrimaryButtonPress()} + disabled={isResolving} > { // 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 + isHighRiskBreachesStep ? l10n.getString("high-risk-breach-mark-as-fixed") : l10n.getString("high-risk-breach-none-continue") } - {hasBreaches && ( + {isHighRiskBreachesStep && ( {l10n.getString("high-risk-breach-skip")} @@ -107,7 +167,7 @@ export function HighRiskBreachLayout(props: HighRiskBreachLayoutProps) { // 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: - estimatedTime={!isStepDone && hasBreaches ? 15 : undefined} + estimatedTime={!isStepDone && isHighRiskBreachesStep ? 15 : undefined} isStepDone={isStepDone} data={props.data} > diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/[type]/HighRiskBreachLayout.test.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/[type]/HighRiskBreachLayout.test.tsx index 61837d7e396..cac121595fe 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/[type]/HighRiskBreachLayout.test.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/high-risk-data-breaches/[type]/HighRiskBreachLayout.test.tsx @@ -16,6 +16,11 @@ import Meta, { HighRiskBreachDoneStory, } from "./HighRiskDataBreach.stories"; +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), + usePathname: jest.fn(), +})); + beforeEach(() => { setupJestCanvasMock(); }); diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/leaked-passwords/LeakedPasswordsLayout.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/leaked-passwords/LeakedPasswordsLayout.tsx index 1f47f0a2a7a..275751b5c3b 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/leaked-passwords/LeakedPasswordsLayout.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/leaked-passwords/LeakedPasswordsLayout.tsx @@ -27,6 +27,7 @@ import { getGuidedExperienceBreaches } from "../../../../../../../functions/univ import { hasPremium } from "../../../../../../../functions/universal/user"; import { useState } from "react"; import { useRouter } from "next/navigation"; +import { LeakedPasswordsDataTypes } from "../../../../../../../functions/universal/breach"; export interface LeakedPasswordsLayoutProps { type: LeakedPasswordsTypes; @@ -37,6 +38,7 @@ export interface LeakedPasswordsLayoutProps { export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { const l10n = useL10n(); const router = useRouter(); + const [isResolving, setIsResolving] = useState(false); const [subscriberBreaches, setSubscriberBreaches] = useState( props.data.subscriberBreaches, ); @@ -48,12 +50,13 @@ export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { "security-questions-done": "LeakedPasswordsSecurityQuestion", none: "LeakedPasswordsSecurityQuestion", }; + const isStepDone = + props.type === "passwords-done" || props.type === "security-questions-done"; const guidedExperienceBreaches = getGuidedExperienceBreaches( subscriberBreaches, props.subscriberEmails, ); - const unresolvedPasswordBreach = findFirstUnresolvedBreach( guidedExperienceBreaches, props.type, @@ -67,15 +70,7 @@ export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { ) ?? ""; const nextStep = getNextGuidedStep(props.data, stepMap[props.type]); - - // Data class string to push to resolutionsChecked array - const resolvedDataClassName = - props.type === "passwords" ? "passwords" : "security-questions-and-answers"; - - const isStepDone = - props.type === "passwords-done" || props.type === "security-questions-done"; - - const unresolvedPasswordBreachContent = getLeakedPasswords({ + const pageData = getLeakedPasswords({ dataType: props.type, breaches: guidedExperienceBreaches, l10n, @@ -83,13 +78,31 @@ export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { nextStep, }); + // The non-null assertion here should be safe since we already did this check + // in `./[type]/page.tsx`: + const { title, illustration, content } = pageData!; + const handleUpdateBreachStatus = async () => { - if (!unresolvedPasswordBreach || !emailAffected) { + const leakedPasswordsBreachClasses: Record< + LeakedPasswordsTypes, + | (typeof LeakedPasswordsDataTypes)[keyof typeof LeakedPasswordsDataTypes] + | null + > = { + passwords: LeakedPasswordsDataTypes.Passwords, + "passwords-done": null, + "security-questions": LeakedPasswordsDataTypes.SecurityQuestions, + "security-questions-done": null, + none: null, + }; + + const dataType = leakedPasswordsBreachClasses[props.type]; + if (!dataType || !unresolvedPasswordBreach || !emailAffected) { return; } + setIsResolving(true); try { - unresolvedPasswordBreach.resolvedDataClasses.push(resolvedDataClassName); + unresolvedPasswordBreach.resolvedDataClasses.push(dataType); // FIXME/BUG: MNTOR-2562 Remove empty [""] string const formattedDataClasses = unresolvedPasswordBreach.resolvedDataClasses.filter(Boolean); @@ -100,11 +113,11 @@ export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { formattedDataClasses, ); - // Manually move to the next step when mark is fixed is selected + // Manually move to the next step when breach has been marked as fixed. const updatedSubscriberBreaches = subscriberBreaches.map( (subscriberBreach) => { if (subscriberBreach.id === unresolvedPasswordBreach.id) { - subscriberBreach.resolvedDataClasses.push(resolvedDataClassName); + subscriberBreach.resolvedDataClasses.push(dataType); } return subscriberBreach; }, @@ -117,63 +130,64 @@ export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) { if (!isComplete) { setSubscriberBreaches(updatedSubscriberBreaches); + setIsResolving(false); + return; } - // If all breaches in the step is fully resolved, take users to the next step - else { - router.push(nextStep.href); - } - } catch (error) { - console.error("Error updating breach status", error); + // If all breaches in the step are fully resolved, + // take users to the celebration view. + const doneSlug: LeakedPasswordsTypes = + props.type === "passwords" + ? "passwords-done" + : "security-questions-done"; + router.push(`/redesign/user/dashboard/fix/leaked-passwords/${doneSlug}`); + } catch (_error) { + // TODO: MNTOR-2563: Capture client error with @next/sentry + setIsResolving(false); } }; /* c8 ignore stop */ return ( - unresolvedPasswordBreachContent && - unresolvedPasswordBreach && ( - + + + + {l10n.getString("leaked-passwords-skip")} + + + ) + } + estimatedTime={!isStepDone ? 4 : undefined} + isStepDone={isStepDone} data={props.data} - nextStep={nextStep} - currentSection="leaked-passwords" - hideProgressIndicator={isStepDone} - showConfetti={isStepDone} > - - - - {l10n.getString("leaked-passwords-skip")} - - - ) - } - estimatedTime={!isStepDone ? 4 : undefined} - isStepDone={isStepDone} - data={props.data} - > - - - - ) + + + ); } diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/SecurityRecommendationsLayout.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/SecurityRecommendationsLayout.tsx index c9e4eb91eb2..15da3c77dc4 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/SecurityRecommendationsLayout.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/SecurityRecommendationsLayout.tsx @@ -4,6 +4,8 @@ "use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; import { SecurityRecommendationTypes, getSecurityRecommendationsByType, @@ -21,6 +23,8 @@ import { } from "../../../../../../../functions/server/getRelevantGuidedSteps"; import { getGuidedExperienceBreaches } from "../../../../../../../functions/universal/guidedExperienceBreaches"; import { hasPremium } from "../../../../../../../functions/universal/user"; +import { SecurityRecommendationDataTypes } from "../../../../../../../functions/universal/breach"; +import { BreachBulkResolutionRequest } from "../../../../../../../(nextjs_migration)/(authenticated)/user/breaches/breaches"; export interface SecurityRecommendationsLayoutProps { type: SecurityRecommendationTypes; @@ -32,6 +36,8 @@ export function SecurityRecommendationsLayout( props: SecurityRecommendationsLayoutProps, ) { const l10n = useL10n(); + const router = useRouter(); + const [isResolving, setIsResolving] = useState(false); const stepMap: Record = { email: "SecurityTipsEmail", @@ -58,12 +64,66 @@ export function SecurityRecommendationsLayout( // The non-null assertion here should be safe since we already did this check // in `./[type]/page.tsx`: const { title, illustration, content, exposedData } = pageData!; + const hasExposedData = exposedData.length > 0; + + // TODO: Write unit tests MNTOR-2560 + /* c8 ignore start */ + const handlePrimaryButtonPress = async () => { + const securityRecommendatioBreachClasses: Record< + SecurityRecommendationTypes, + | (typeof SecurityRecommendationDataTypes)[keyof typeof SecurityRecommendationDataTypes] + | null + > = { + email: SecurityRecommendationDataTypes.Email, + phone: SecurityRecommendationDataTypes.Phone, + ip: SecurityRecommendationDataTypes.IP, + done: null, + }; + + const dataType = securityRecommendatioBreachClasses[props.type]; + // Only attempt to resolve the breaches if the following conditions are true: + // - There is a matching data class type in this step + // - The current step has unresolved exposed data + // - There is no pending breach resolution request + if (!dataType || !hasExposedData || isResolving) { + return; + } + + setIsResolving(true); + try { + const body: BreachBulkResolutionRequest = { dataType }; + const response = await fetch("/api/v1/user/breaches/bulk-resolve", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + const result = await response.json(); + if (!result?.success) { + throw new Error( + `Could not resolve breach data class of type: ${props.type}`, + ); + } + + const isCurrentStepSection = Object.values(stepMap).includes(nextStep.id); + const nextRoute = isCurrentStepSection + ? nextStep.href + : "/redesign/user/dashboard/fix/security-recommendations/done"; + router.push(nextRoute); + } catch (_error) { + // TODO: MNTOR-2563: Capture client error with @next/sentry + setIsResolving(false); + } + }; + /* c8 ignore stop */ return ( void handlePrimaryButtonPress()} + disabled={isResolving} > {l10n.getString("security-recommendation-steps-cta-label")} diff --git a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/[type]/SecurityRecommendations.test.tsx b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/[type]/SecurityRecommendations.test.tsx index ca17bfcdb9b..ac95101f89d 100644 --- a/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/[type]/SecurityRecommendations.test.tsx +++ b/src/app/(proper_react)/redesign/(authenticated)/user/dashboard/fix/security-recommendations/[type]/SecurityRecommendations.test.tsx @@ -14,6 +14,11 @@ import Meta, { DoneStory, } from "./SecurityRecommendations.stories"; +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), + usePathname: jest.fn(), +})); + beforeEach(() => { setupJestCanvasMock(); }); diff --git a/src/app/api/v1/user/breaches/bulk-resolve/route.ts b/src/app/api/v1/user/breaches/bulk-resolve/route.ts index 25ce42a1e62..1519dac221b 100644 --- a/src/app/api/v1/user/breaches/bulk-resolve/route.ts +++ b/src/app/api/v1/user/breaches/bulk-resolve/route.ts @@ -19,7 +19,7 @@ import { export async function PUT(req: NextRequest): Promise { const session = await getServerSession(authOptions); - if (!session?.user?.subscriber || typeof session.user?.email !== "string") { + if (!session?.user?.subscriber || typeof session?.user?.email !== "string") { return new NextResponse( JSON.stringify({ success: false, message: "Unauthenticated" }), { status: 401 }, @@ -28,7 +28,7 @@ export async function PUT(req: NextRequest): Promise { try { const subscriber: Subscriber = await getSubscriberByEmail( - session?.user?.email, + session.user.email, ); const allBreaches = await getBreaches(); const { dataType: dataTypeToResolve }: BreachBulkResolutionRequest = diff --git a/src/app/components/client/FixNavigation.tsx b/src/app/components/client/FixNavigation.tsx index 961269d8c99..a871fd5fd59 100644 --- a/src/app/components/client/FixNavigation.tsx +++ b/src/app/components/client/FixNavigation.tsx @@ -4,6 +4,7 @@ "use client"; +import { ReactNode } from "react"; import Image from "next/image"; import styles from "./FixNavigation.module.scss"; import stepDataBrokerProfilesIcon from "../../(proper_react)/redesign/(authenticated)/user/dashboard/fix/images/step-counter-data-broker-profiles.svg"; @@ -57,8 +58,6 @@ export const Steps = (props: { 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, @@ -69,6 +68,18 @@ export const Steps = (props: { return value.length > 0; }).length; + const StepLabel = ({ + label, + count, + }: { + label: string; + count: number; + }): ReactNode => ( +
+ {label} {count > 0 && `(${count})`} +
+ ); + return (
    {isEligibleForStep(props.data, "Scan") && ( @@ -87,11 +98,10 @@ export const Steps = (props: {
    - -
    - {l10n.getString("fix-flow-nav-data-broker-profiles")} ( - {totalDataBrokerProfiles}) -
    + )}
  • - -
    - {l10n.getString("fix-flow-nav-high-risk-data-breaches")} ( - {totalHighRiskBreaches}) -
    +
  • - -
    - {l10n.getString("fix-flow-nav-leaked-passwords")} ( - {totalPasswordBreaches}) -
    +
  • - -
    - {l10n.getString("fix-flow-nav-security-recommendations")} ( - {totalSecurityRecommendations}) -
    +
  • diff --git a/src/app/functions/universal/breach.ts b/src/app/functions/universal/breach.ts index 0c9b9905ce1..37851316a7d 100644 --- a/src/app/functions/universal/breach.ts +++ b/src/app/functions/universal/breach.ts @@ -26,3 +26,14 @@ export const HighRiskDataTypes = { BankAccount: BreachDataTypes.BankAccount, PIN: BreachDataTypes.PIN, } as const; + +export const LeakedPasswordsDataTypes = { + Passwords: BreachDataTypes.Passwords, + SecurityQuestions: BreachDataTypes.SecurityQuestions, +} as const; + +export const SecurityRecommendationDataTypes = { + Email: BreachDataTypes.Email, + Phone: BreachDataTypes.Phone, + IP: BreachDataTypes.IP, +} as const; diff --git a/src/app/functions/universal/guidedExperienceBreaches.test.ts b/src/app/functions/universal/guidedExperienceBreaches.test.ts index c0f314d0f0b..a854b42087a 100644 --- a/src/app/functions/universal/guidedExperienceBreaches.test.ts +++ b/src/app/functions/universal/guidedExperienceBreaches.test.ts @@ -7,7 +7,7 @@ import { getGuidedExperienceBreaches } from "./guidedExperienceBreaches"; import { SubscriberBreach } from "../../../utils/subscriberBreaches"; import { BreachDataTypes } from "./breach"; -it("getGuidedExperienceBreaches: return guided experience", () => { +it("getGuidedExperienceBreaches: return all guided experience breaches if they have relevant data classes", () => { const subBreach: SubscriberBreach = { addedDate: new Date(), breachDate: new Date(), @@ -35,7 +35,20 @@ it("getGuidedExperienceBreaches: return guided experience", () => { name: "", title: "", emailsAffected: ["test@mozilla.com"], - dataClassesEffected: [], + dataClassesEffected: [ + { [BreachDataTypes.PIN]: 1 }, + { [BreachDataTypes.Passwords]: 1 }, + { [BreachDataTypes.Address]: 1 }, + { [BreachDataTypes.BankAccount]: 1 }, + { [BreachDataTypes.CreditCard]: 1 }, + { [BreachDataTypes.DoB]: 1 }, + { [BreachDataTypes.Email]: 1 }, + { [BreachDataTypes.HistoricalPasswords]: 1 }, + { [BreachDataTypes.IP]: 1 }, + { [BreachDataTypes.Phone]: 1 }, + { [BreachDataTypes.SSN]: 1 }, + { [BreachDataTypes.SecurityQuestions]: 1 }, + ], }; const guidedExp = getGuidedExperienceBreaches( @@ -53,3 +66,53 @@ it("getGuidedExperienceBreaches: return guided experience", () => { expect(guidedExp.securityRecommendations.emailAddress).toHaveLength(1); expect(guidedExp.securityRecommendations.IPAddress).toHaveLength(1); }); + +it("getGuidedExperienceBreaches: exclude guided experience breaches if they do not have the relevant classes", () => { + const subBreach: SubscriberBreach = { + addedDate: new Date(), + breachDate: new Date(), + dataClasses: [ + BreachDataTypes.PIN, + BreachDataTypes.Passwords, + BreachDataTypes.Address, + BreachDataTypes.BankAccount, + BreachDataTypes.CreditCard, + BreachDataTypes.DoB, + BreachDataTypes.Email, + BreachDataTypes.HistoricalPasswords, + BreachDataTypes.IP, + BreachDataTypes.Phone, + BreachDataTypes.SSN, + BreachDataTypes.SecurityQuestions, + ], + resolvedDataClasses: [], + description: "", + domain: "", + id: 1, + isResolved: false, + favIconUrl: "", + modifiedDate: new Date(), + name: "", + title: "", + emailsAffected: ["test@mozilla.com"], + dataClassesEffected: [ + { [BreachDataTypes.PIN]: 1 }, + { [BreachDataTypes.Passwords]: 2 }, + ], + }; + + const guidedExp = getGuidedExperienceBreaches( + [subBreach], + ["test@mozilla.com"], + ); + expect(guidedExp.highRisk.pinBreaches).toHaveLength(1); + expect(guidedExp.emails).toHaveLength(1); + expect(guidedExp.highRisk.ssnBreaches).toHaveLength(0); + expect(guidedExp.highRisk.creditCardBreaches).toHaveLength(0); + expect(guidedExp.highRisk.bankBreaches).toHaveLength(0); + expect(guidedExp.passwordBreaches.passwords).toHaveLength(1); + expect(guidedExp.passwordBreaches.securityQuestions).toHaveLength(0); + expect(guidedExp.securityRecommendations.phoneNumber).toHaveLength(0); + expect(guidedExp.securityRecommendations.emailAddress).toHaveLength(0); + expect(guidedExp.securityRecommendations.IPAddress).toHaveLength(0); +}); diff --git a/src/app/functions/universal/guidedExperienceBreaches.ts b/src/app/functions/universal/guidedExperienceBreaches.ts index 9bb73343dee..f37f9a63743 100644 --- a/src/app/functions/universal/guidedExperienceBreaches.ts +++ b/src/app/functions/universal/guidedExperienceBreaches.ts @@ -6,6 +6,19 @@ import { BreachDataTypes } from "./breach"; import { SubscriberBreach } from "../../../utils/subscriberBreaches"; import { GuidedExperienceBreaches } from "../server/getUserBreaches"; +function isUnresolvedDataBreachClass( + breach: SubscriberBreach, + breachDataClass: (typeof BreachDataTypes)[keyof typeof BreachDataTypes], +): boolean { + const affectedDataClasses = breach.dataClassesEffected.map( + (dataClass) => Object.keys(dataClass)[0], + ); + return ( + affectedDataClasses.includes(breachDataClass) && + !breach.resolvedDataClasses.includes(breachDataClass) + ); +} + export function getGuidedExperienceBreaches( subscriberBreaches: SubscriberBreach[], emails: string[], @@ -28,47 +41,51 @@ export function getGuidedExperienceBreaches( }, emails, }; - subscriberBreaches.forEach((b) => { + + subscriberBreaches.forEach((breach) => { // high risks - if (b.dataClasses.includes(BreachDataTypes.SSN)) { - guidedExperienceBreaches.highRisk.ssnBreaches.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.SSN)) { + guidedExperienceBreaches.highRisk.ssnBreaches.push(breach); } - if (b.dataClasses.includes(BreachDataTypes.CreditCard)) { - guidedExperienceBreaches.highRisk.creditCardBreaches.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.CreditCard)) { + guidedExperienceBreaches.highRisk.creditCardBreaches.push(breach); } - if (b.dataClasses.includes(BreachDataTypes.PIN)) { - guidedExperienceBreaches.highRisk.pinBreaches.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.PIN)) { + guidedExperienceBreaches.highRisk.pinBreaches.push(breach); } - if (b.dataClasses.includes(BreachDataTypes.BankAccount)) { - guidedExperienceBreaches.highRisk.bankBreaches.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.BankAccount)) { + guidedExperienceBreaches.highRisk.bankBreaches.push(breach); } // passwords - // TODO: Add tests when passwords component has been made - MNTOR-1712 /* c8 ignore start */ - if (b.dataClasses.includes(BreachDataTypes.Passwords)) { - guidedExperienceBreaches.passwordBreaches.passwords.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.Passwords)) { + guidedExperienceBreaches.passwordBreaches.passwords.push(breach); } - if (b.dataClasses.includes(BreachDataTypes.SecurityQuestions)) { - guidedExperienceBreaches.passwordBreaches.securityQuestions.push(b); + if ( + isUnresolvedDataBreachClass(breach, BreachDataTypes.SecurityQuestions) + ) { + guidedExperienceBreaches.passwordBreaches.securityQuestions.push(breach); } // security recommendations // TODO: Add tests when security recs work is merged in - if (b.dataClasses.includes(BreachDataTypes.Phone)) { - guidedExperienceBreaches.securityRecommendations.phoneNumber.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.Phone)) { + guidedExperienceBreaches.securityRecommendations.phoneNumber.push(breach); } - if (b.dataClasses.includes(BreachDataTypes.Email)) { - guidedExperienceBreaches.securityRecommendations.emailAddress.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.Email)) { + guidedExperienceBreaches.securityRecommendations.emailAddress.push( + breach, + ); } - if (b.dataClasses.includes(BreachDataTypes.IP)) { - guidedExperienceBreaches.securityRecommendations.IPAddress.push(b); + if (isUnresolvedDataBreachClass(breach, BreachDataTypes.IP)) { + guidedExperienceBreaches.securityRecommendations.IPAddress.push(breach); } /* c8 ignore stop */ });