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}
+
+
- {props.emailData &&
- props.label === l10n.getString("exposure-card-email") && (
-
{props.emailData}
- )}
);
};