Skip to content

Commit

Permalink
MNTOR-3396 - Organize storybook dashboard states (#5308)
Browse files Browse the repository at this point in the history
* Migrate utils/hibp.js to TypeScript

This removes the `Breach` type in functions/universal/breaches,
which was created when first introducing TypeScript and the flow
of data was still unclear, but by now had overlap with other types
and no clear provenance.

Instead, there are now three breach-related types, that represent
where the data came from:

- HibpGetBreachesResponse: this is an array of breach elements as
                           returned from the HIBP API, unprocessed.
                           Properties are in PascalCase, so are a
                           breach's data classes.
- BreachRow: this is a breach's data as stored in our database,
             along with some data we added to it, such as a favicon
             URL. Properties are snake_case, and data classes are
             lowercased and kebab-cased by the
             formatDataClassesArray function.
- HibpLikeDbBreach: this is a breach's data fetched from the
                    database, but stored in an object meant to look
                    like the ones in HibpGetBreachesResponse. In
                    other words, it contains the same data as
                    BreachRow (including lowercased, kebab-cased
                    data classes), but on PascalCase properties.

The latter is somewhat of a historical artefact, because we used
to try to load breaches from our database, then if our database
didn't contain any breaches yet, fetch them live from the HIBP API
and continue working with that.

We no longer do that: now, even after fetching them from the HIBP
API, we do a new query to get them from the database and process
them into HibpLikeDbBreach, so that we can assume a consisent data
structure everywhere we work with breaches.

* MNTOR-3435 - breaches js to ts

* remove old typedef

* explicitly type breach as any

* fix rebase error

* update breaches path

* organize storybook files

* add mobile and public shell stories

* organize dashboard states

* rollback unwanted changes

* rollback unwanted changes

* rollback unwanted changes

* consolidate brokeroptiona and breachoptions

* consolidate dashboardwrapper props

* top banner spacing

* fix lint error

* rmv checkbox and radio stories

---------

Co-authored-by: Vincent <[email protected]>
  • Loading branch information
codemist and Vinnl authored Nov 29, 2024
1 parent 665fb50 commit bf7a1c3
Show file tree
Hide file tree
Showing 23 changed files with 1,210 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ import {
} from "../../../../../../../telemetry/generated/nimbus/experiments";
import { FeatureFlagName } from "../../../../../../../db/tables/featureFlags";

const brokerOptions = {
export const brokerOptions = {
"no-scan": "No scan started",
empty: "No scan results",
unresolved: "With unresolved scan results",
resolved: "All scan results resolved",
"scan-in-progress": "Scan is in progress",
"manually-resolved": "Manually resolved",
};
const breachOptions = {
export const breachOptions = {
empty: "No data breaches",
unresolved: "With unresolved data breaches",
resolved: "All data breaches resolved",
};
type DashboardWrapperProps = (
export type DashboardWrapperProps = (
| {
countryCode: "us";
brokers: keyof typeof brokerOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import Meta, {
DashboardNonUsNoBreaches,
DashboardNonUsUnresolvedBreaches,
DashboardNonUsResolvedBreaches,
} from "./DashboardNonUSUsers.stories";
import { useTelemetry } from "../../../../../../hooks/useTelemetry";
import { deleteAllCookies } from "../../../../../../functions/client/deleteAllCookies";
import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments";
import {
DashboardUsNoPremiumNoScanNoBreaches,
DashboardUsNoPremiumNoScanUnresolvedBreaches,
DashboardUsNoPremiumNoScanResolvedBreaches,
Expand All @@ -33,6 +38,11 @@ import Meta, {
DashboardUsNoPremiumResolvedScanNoBreaches,
DashboardUsNoPremiumResolvedScanUnresolvedBreaches,
DashboardUsNoPremiumResolvedScanResolvedBreaches,
DashboardUsNoPremiumScanInProgressNoBreaches,
DashboardUsNoPremiumScanInProgressUnresolvedBreaches,
DashboardUsNoPremiumScanInProgressResolvedBreaches,
} from "./DashboardUSUsers.stories";
import {
DashboardUsPremiumEmptyScanNoBreaches,
DashboardUsPremiumEmptyScanUnresolvedBreaches,
DashboardUsPremiumEmptyScanResolvedBreaches,
Expand All @@ -42,18 +52,12 @@ import Meta, {
DashboardUsPremiumResolvedScanNoBreaches,
DashboardUsPremiumResolvedScanUnresolvedBreaches,
DashboardUsPremiumResolvedScanResolvedBreaches,
DashboardUsNoPremiumScanInProgressNoBreaches,
DashboardUsNoPremiumScanInProgressUnresolvedBreaches,
DashboardUsNoPremiumScanInProgressResolvedBreaches,
DashboardUsPremiumScanInProgressNoBreaches,
DashboardUsPremiumScanInProgressUnresolvedBreaches,
DashboardUsPremiumScanInProgressResolvedBreaches,
DashboardInvalidPremiumUserNoScanResolvedBreaches,
DashboardUsPremiumManuallyResolvedScansNoBreaches,
} from "./Dashboard.stories";
import { useTelemetry } from "../../../../../../hooks/useTelemetry";
import { deleteAllCookies } from "../../../../../../functions/client/deleteAllCookies";
import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments";
DashboardUsPremiumScanInProgressNoBreaches,
DashboardUsPremiumScanInProgressResolvedBreaches,
DashboardUsPremiumScanInProgressUnresolvedBreaches,
} from "./DashboardPlusUsers.stories";

jest.mock("next/navigation", () => ({
useRouter: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/* 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 { OnerepScanResultRow, OnerepScanRow } from "knex/types/tables";
import { faker } from "@faker-js/faker";
import { View as DashboardEl } from "./View";
import { Shell } from "../../../../Shell";
import { getL10n } from "../../../../../../functions/l10n/storybookAndJest";
import {
createRandomScanResult,
createRandomBreach,
createUserWithPremiumSubscription,
} from "../../../../../../../apiMocks/mockData";
import { SubscriberBreach } from "../../../../../../../utils/subscriberBreaches";
import { LatestOnerepScanData } from "../../../../../../../db/tables/onerep_scans";
import { CountryCodeProvider } from "../../../../../../../contextProviders/country-code";
import { SessionProvider } from "../../../../../../../contextProviders/session";
import { defaultExperimentData } from "../../../../../../../telemetry/generated/nimbus/experiments";
import {
breachOptions,
brokerOptions,
DashboardWrapperProps,
} from "./Dashboard.stories";

const DashboardWrapper = (props: DashboardWrapperProps) => {
const mockedResolvedBreach: SubscriberBreach = createRandomBreach({
dataClasses: [
"email-addresses",
"ip-addresses",
"phone-numbers",
"passwords",
"pins",
"social-security-numbers",
"partial-credit-card-data",
"security-questions-and-answers",
],
addedDate: new Date("2023-06-18T14:48:00.000Z"),
dataClassesEffected: [
{ "email-addresses": ["[email protected]", "[email protected]"] },
{ "ip-addresses": 1 },
{ "phone-numbers": 1 },
{ passwords: 1 },
],
isResolved: true,
});

const mockedUnresolvedBreach: SubscriberBreach = createRandomBreach({
dataClasses: ["email-addresses", "ip-addresses", "phone-numbers"],
addedDate: new Date("2023-06-18T14:48:00.000Z"),
dataClassesEffected: [
{ "email-addresses": ["[email protected]", "[email protected]"] },
{ "ip-addresses": 1 },
],
isResolved: false,
});

let breaches: SubscriberBreach[] = [];
if (props.breaches === "resolved") {
breaches = [mockedResolvedBreach];
}
if (props.breaches === "unresolved") {
breaches = [mockedResolvedBreach, mockedUnresolvedBreach];
}

const mockedScan: OnerepScanRow = {
created_at: new Date(Date.UTC(1998, 2, 31)),
updated_at: new Date(Date.UTC(1998, 2, 31)),
id: 0,
onerep_profile_id: 0,
onerep_scan_id: 0,
onerep_scan_reason: "initial",
onerep_scan_status: "finished",
};

const mockedScanInProgress: OnerepScanRow = {
...mockedScan,
onerep_scan_status: "in_progress",
};

const mockedInProgressScanResults: OnerepScanResultRow[] = [
createRandomScanResult({ status: "removed", manually_resolved: false }),
createRandomScanResult({
status: "waiting_for_verification",
manually_resolved: false,
}),
createRandomScanResult({
status: "optout_in_progress",
manually_resolved: false,
}),
];

const mockedAllResolvedScanResults: OnerepScanResultRow[] = [
createRandomScanResult({ status: "removed", manually_resolved: false }),
createRandomScanResult({ status: "removed", manually_resolved: false }),
];

const mockedUnresolvedScanResults: OnerepScanResultRow[] = [
...mockedInProgressScanResults,
createRandomScanResult({ status: "new", manually_resolved: false }),
createRandomScanResult({ status: "new", manually_resolved: false }),
createRandomScanResult({ status: "new", manually_resolved: true }),
];

const mockedManuallyResolvedScanResults: OnerepScanResultRow[] = [
createRandomScanResult({ status: "new", manually_resolved: true }),
createRandomScanResult({
status: "waiting_for_verification",
manually_resolved: true,
}),
createRandomScanResult({
status: "optout_in_progress",
manually_resolved: true,
}),
createRandomScanResult({ status: "removed", manually_resolved: true }),
];

const scanData: LatestOnerepScanData = { scan: null, results: [] };
let scanCount = 0;

if (props.countryCode === "us") {
if (props.brokers && props.brokers !== "no-scan") {
const scanInProgress = props.brokers === "scan-in-progress";
scanData.scan = scanInProgress ? mockedScanInProgress : mockedScan;

if (scanInProgress) {
scanCount = 1;
}
if (props.brokers === "resolved") {
scanData.results = mockedAllResolvedScanResults;
}
if (props.brokers === "unresolved") {
scanData.results = mockedUnresolvedScanResults;
}

if (props.brokers === "manually-resolved") {
scanData.results = mockedManuallyResolvedScanResults;
}
}
}

const user = createUserWithPremiumSubscription();
if ((props.countryCode !== "us" || !props.premium) && user.fxa) {
user.fxa.subscriptions = [];
}

const mockedSession = {
expires: new Date().toISOString(),
user: user,
};

const mockedRemovalTimeEstimates = scanData.results
.map((scan) => ({
d: scan.data_broker,
t: faker.number.float({ min: 0, max: 200 }),
}))
.filter(() => Math.random() < 0.1);

return (
<SessionProvider session={mockedSession}>
<CountryCodeProvider countryCode={props.countryCode}>
<Shell
l10n={getL10n()}
session={mockedSession}
nonce=""
countryCode={props.countryCode}
>
<DashboardEl
user={user}
userBreaches={breaches}
userScanData={scanData}
isEligibleForPremium={props.countryCode === "us"}
isEligibleForFreeScan={props.countryCode === "us" && !scanData.scan}
monthlySubscriptionUrl=""
yearlySubscriptionUrl=""
fxaSettingsUrl=""
scanCount={scanCount}
totalNumberOfPerformedScans={props.totalNumberOfPerformedScans}
subscriptionBillingAmount={{
yearly: 13.37,
monthly: 42.42,
}}
isNewUser={true}
elapsedTimeInDaysSinceInitialScan={
props.elapsedTimeInDaysSinceInitialScan
}
enabledFeatureFlags={props.enabledFeatureFlags ?? []}
experimentData={
props.experimentData ?? {
...defaultExperimentData,
"last-scan-date": {
enabled: true,
},
}
}
activeTab={props.activeTab ?? "action-needed"}
hasFirstMonitoringScan={props.hasFirstMonitoringScan ?? false}
signInCount={props.signInCount ?? null}
autoOpenUpsellDialog={props.autoOpenUpsellDialog ?? false}
removalTimeEstimates={mockedRemovalTimeEstimates}
/>
</Shell>
</CountryCodeProvider>
</SessionProvider>
);
};

const meta: Meta<typeof DashboardWrapper> = {
title: "Pages/Logged in/Dashboard/Non US User",
component: DashboardWrapper,
argTypes: {
brokers: {
options: Object.keys(brokerOptions),
description: "Scan results",
control: {
type: "radio",
labels: brokerOptions,
},
},
breaches: {
options: Object.keys(breachOptions),
control: {
type: "radio",
labels: breachOptions,
},
},
elapsedTimeInDaysSinceInitialScan: {
name: "Days since initial scan",
control: {
type: "number",
},
},
hasFirstMonitoringScan: {
name: "Has first monitoring scan",
control: {
type: "boolean",
},
},
signInCount: {
name: "Sign-in count",
control: {
type: "number",
},
},
},
};

export default meta;
type Story = StoryObj<typeof DashboardWrapper>;

export const DashboardNonUsNoBreaches: Story = {
name: "Non-US user, with 0 breaches",
args: {
countryCode: "nl",
breaches: "empty",
},
};

export const DashboardNonUsUnresolvedBreaches: Story = {
name: "Non-US user, with unresolved breaches",
args: {
countryCode: "nl",
breaches: "unresolved",
},
};

export const DashboardNonUsResolvedBreaches: Story = {
name: "Non-US user, with all breaches resolved",
args: {
countryCode: "nl",
breaches: "resolved",
},
};
Loading

0 comments on commit bf7a1c3

Please sign in to comment.