From 36b0b32b26631c41f729d5f5a5f75e0b10d22743 Mon Sep 17 00:00:00 2001 From: Kaitlyn Andres Date: Tue, 5 Dec 2023 12:48:01 -0500 Subject: [PATCH] =?UTF-8?q?MNTOR-2540=20-=20Show=20expanded=20data=20class?= =?UTF-8?q?=20details=20for=20data=20broker=20exposur=E2=80=A6=20(#3814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MNTOR-2540 - Show expanded data class details for data broker exposures for premium users * rearrange address format * add ui tweaks * consolidate breachdataclass and databrokerdataclass * clean up comments * add more detailed comments * change classnames * use index * lint * unit test ignore * add test igonre * use let instead of useState/useEffect * use early guard clauses instead of switch case * fix lint * use exposure instead of type * use flex/gap instead of bottom padding --- .../client/ExposureCard.module.scss | 15 +- src/app/components/client/ExposureCard.tsx | 96 +++++------ .../client/ExposureCardDataClass.tsx | 163 +++++++++++------- 3 files changed, 155 insertions(+), 119 deletions(-) diff --git a/src/app/components/client/ExposureCard.module.scss b/src/app/components/client/ExposureCard.module.scss index 1cfb6c572c6..f3cb33af306 100644 --- a/src/app/components/client/ExposureCard.module.scss +++ b/src/app/components/client/ExposureCard.module.scss @@ -158,13 +158,13 @@ } .exposedInfoTitle { + align-self: start; + @media screen and (min-width: $screen-lg) { - align-self: center; flex: 0 0 90px; // fix width of categories title } @media screen and (min-width: $screen-xl) { - align-self: center; flex: 0 0 150px; } } @@ -198,7 +198,7 @@ @media screen and (min-width: $screen-lg) { display: flex; flex-direction: row; - align-items: center; + align-items: start; gap: $layout-xs; flex-wrap: wrap; justify-content: flex-start; @@ -215,15 +215,18 @@ } } - .emails { + .dataClassListDetailsWrapper { padding-inline-start: var(--exposureDetailsIconWidth); font: $text-body-xs; font-weight: 600; - .emailsList { + .dataClassListDetails { list-style: none; - padding: 0; margin: 0; + padding: $spacing-xs 0 0 $spacing-xs; + gap: $spacing-xs; + display: flex; + flex-direction: column; li { list-style-type: none; diff --git a/src/app/components/client/ExposureCard.tsx b/src/app/components/client/ExposureCard.tsx index 813df27c312..5f125934eb9 100644 --- a/src/app/components/client/ExposureCard.tsx +++ b/src/app/components/client/ExposureCard.tsx @@ -26,7 +26,7 @@ import { SubscriberBreach, } from "../../../utils/subscriberBreaches"; import { FallbackLogo } from "../server/BreachLogo"; -import { BreachDataClass, DataBrokerDataClass } from "./ExposureCardDataClass"; +import { ExposureCardDataClassLayout } from "./ExposureCardDataClass"; import { DataBrokerImage } from "./DataBrokerImage"; export type Exposure = OnerepScanResultRow | SubscriberBreach; @@ -77,67 +77,56 @@ const ScanResultCard = (props: ScanResultCardProps) => { // Scan Result Categories if (scanResult.relatives.length > 0) { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-family-members")} - num={scanResult.relatives.length} + label={l10n.getString("exposure-card-family-members")} + count={scanResult.relatives.length} isPremiumUser={props.isPremiumUser} />, ); } if (scanResult.phones.length > 0) { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-phone-number")} - num={scanResult.phones.length} + label={l10n.getString("exposure-card-phone-number")} + count={scanResult.phones.length} isPremiumUser={props.isPremiumUser} />, ); } if (scanResult.emails.length > 0) { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-email")} - num={scanResult.emails.length} + label={l10n.getString("exposure-card-email")} + count={scanResult.emails.length} isPremiumUser={props.isPremiumUser} />, ); } if (scanResult.addresses.length > 0) { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-address")} - num={scanResult.addresses.length} - isPremiumUser={props.isPremiumUser} - />, - ); - // TODO: Add unit test when changing this code: - /* c8 ignore next 13 */ - } else { - // "Other" item when none of the conditions above are met - exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-other")} - num={0} + label={l10n.getString("exposure-card-address")} + count={scanResult.addresses.length} isPremiumUser={props.isPremiumUser} />, ); } - const COMPANY_NAME_MAX_CHARACTER_COUNT = 20; const isCompanyNameTooLong = scanResult.data_broker.length > COMPANY_NAME_MAX_CHARACTER_COUNT; @@ -284,53 +273,64 @@ const SubscriberBreachCard = (props: SubscriberBreachCardProps) => { subscriberBreach.dataClassesEffected.map((item: DataClassEffected) => { const dataClass = Object.keys(item)[0]; + const emailLength = subscriberBreach.emailsAffected.length; + if (dataClass === "email-addresses") { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-email")} + label={l10n.getString("exposure-card-email")} + count={emailLength} />, ); } else if (dataClass === "passwords") { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-password")} + label={l10n.getString("exposure-card-password")} + count={emailLength} />, ); } else if (dataClass === "phone-numbers") { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-phone-number")} + label={l10n.getString("exposure-card-phone-number")} + count={emailLength} />, ); } else if (dataClass === "ip-addresses") { exposureCategoriesArray.push( - } - exposureCategoryLabel={l10n.getString("exposure-card-ip-address")} + label={l10n.getString("exposure-card-ip-address")} + count={emailLength} />, ); // TODO: Add unit test when changing this code: - /* c8 ignore next 12 */ + /* c8 ignore next 13 */ } // Handle all other breach categories else { exposureCategoriesArray.push( - } // default icon for categories without a unique one - exposureCategoryLabel={l10n.getString(dataClass)} // categories are localized in data-classes.ftl + label={l10n.getString(dataClass)} // categories are localized in data-classes.ftl + count={emailLength} />, ); } diff --git a/src/app/components/client/ExposureCardDataClass.tsx b/src/app/components/client/ExposureCardDataClass.tsx index b277edd5bc2..b50e1ec7d5c 100644 --- a/src/app/components/client/ExposureCardDataClass.tsx +++ b/src/app/components/client/ExposureCardDataClass.tsx @@ -7,94 +7,127 @@ import { ReactElement } from "react"; import { OnerepScanResultRow } from "knex/types/tables"; import styles from "./ExposureCard.module.scss"; -import { SubscriberBreach } from "../../../utils/subscriberBreaches"; import { useL10n } from "../../hooks/l10n"; +import { Exposure, isScanResult } from "./ExposureCard"; +import { HibpBreachDataTypes } from "../../(nextjs_migration)/(authenticated)/user/breaches/breaches"; -type DataBrokerDataClassProps = { - scanResultData: OnerepScanResultRow; - exposureCategoryLabel: string; - num: number; - icon: ReactElement; - isPremiumUser: boolean; -}; +type OnerepScanResultSerializedColumns = Extract< + keyof OnerepScanResultRow, + "emails" | "phones" | "addresses" | "relatives" +>; -export const DataBrokerDataClass = (props: DataBrokerDataClassProps) => { - const emailsList = props.isPremiumUser ? ( -
    - {props.scanResultData.emails.map((email: string, index: number) => ( -
  • {email}
  • - ))} -
- ) : ( - <> - ); - - return ( - - ); -}; - -type BreachDataClassProps = { - subscriberBreachData: SubscriberBreach; - exposureCategoryLabel: string; - icon: ReactElement; +type PremiumDataClassDetailsProps = { + exposure: Exposure; + dataBrokerDataType: OnerepScanResultSerializedColumns; }; -export const BreachDataClass = (props: BreachDataClassProps) => { - const emailLength = props.subscriberBreachData.emailsAffected.length; +// Only for data broker cards +const PremiumDataClassDetails = (props: PremiumDataClassDetailsProps) => { + const { exposure, dataBrokerDataType } = props; - const emailsList = ( -
    - {props.subscriberBreachData.emailsAffected.map( - (email: string, index: number) => ( -
  • {email}
  • - ), - )} -
- ); - - return ( - - ); + // TODO: MNTOR-2617 Add unit test when changing this code: + /* c8 ignore next 3 */ + if (!isScanResult(exposure)) { + return null; + } + if (dataBrokerDataType === "addresses") { + return exposure.addresses.map( + ({ city, state, street, zip }, index: number) => ( +
  • + {street}, {city}, {String(state)}, {zip} +
  • + ), + ); + } + if ( + dataBrokerDataType === "emails" || + dataBrokerDataType === "phones" || + dataBrokerDataType === "relatives" + ) { + return exposure[dataBrokerDataType].map((item: string, index: number) => ( +
  • {item}
  • + )); + // TODO: MNTOR-2617 Add unit test when changing this code: + /* c8 ignore next 3 */ + } else { + return null; + } }; type ExposureCardDataClassLayoutProps = { + exposure: Exposure; icon: ReactElement; label: string; count: number; - emailData: ReactElement; + isPremiumUser?: boolean; + dataBrokerDataType?: OnerepScanResultSerializedColumns; + dataBreachDataType?: HibpBreachDataTypes[keyof HibpBreachDataTypes]; }; -const ExposureCardDataClassLayout = ( +export const ExposureCardDataClassLayout = ( props: ExposureCardDataClassLayoutProps, ) => { const l10n = useL10n(); + const isPremiumUser = props.isPremiumUser; + // Premium users will have fully expanded lists under their respective data class header. + // Breach cards should only have the emails list expanded. + let detailsList; + // Default data class header format: "DataClass: [number]", e.g., "Phone number: 3". + // For premium users, data class headers for data broker cards will update to include just the data class, without the count, e.g., "Phone number". Breach cards remain unchanged. + let dataClassHeader: ReactElement | string = ( + <> + {l10n.getString("exposure-card-label-and-count", { + category_label: props.label, + count: props.count, + })} + + ); + + // Data breach cards only + if (!isScanResult(props.exposure)) { + const emailsList = + // Displaying the list of monitored emails exclusively in a breach card + props.dataBreachDataType === "email-addresses" ? ( + <> + {props.exposure.emailsAffected.map((email: string, index: number) => ( +
  • {email}
  • + ))} + + ) : ( + <> + ); + detailsList = emailsList; + } + + // Data broker cards only + else { + // Update data class header for premium users + if (isPremiumUser) { + dataClassHeader = props.label; + } + + // Render data class details for premium users + const dataClassExpandedDetails = + isPremiumUser && props.dataBrokerDataType ? ( + + ) : ( + <> + ); + detailsList = dataClassExpandedDetails; + } return (
    {props.icon} - - {l10n.getString("exposure-card-label-and-count", { - category_label: props.label, - count: props.count, - })} - + {dataClassHeader} +
    +
    +
      {detailsList}
    - {props.emailData && - props.label === l10n.getString("exposure-card-email") && ( -
    {props.emailData}
    - )}
    ); };