merge: main -> resolution-flow-celebration

This commit is contained in:
Florian Zia 2023-11-17 23:58:12 +01:00
Родитель 161eb70108 fbd4502ff7
Коммит e3e88474d6
Не найден ключ, соответствующий данной подписи
20 изменённых файлов: 2562 добавлений и 2566 удалений

4900
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -46,21 +46,21 @@
"npm": "9.6.5"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.450.0",
"@aws-sdk/lib-storage": "^3.450.0",
"@aws-sdk/client-s3": "^3.454.0",
"@aws-sdk/lib-storage": "^3.454.0",
"@fluent/bundle": "^0.18.0",
"@fluent/langneg": "^0.7.0",
"@fluent/react": "^0.15.2",
"@google-cloud/logging-winston": "^6.0.0",
"@google-cloud/pubsub": "^4.0.6",
"@google-cloud/pubsub": "^4.0.7",
"@grpc/grpc-js": "1.9.7",
"@leeoniya/ufuzzy": "^1.0.11",
"@mozilla/glean": "2.0.5",
"@sentry/nextjs": "^7.80.0",
"@sentry/nextjs": "^7.80.1",
"@sentry/node": "^7.58.1",
"@sentry/tracing": "^7.80.0",
"@types/jsdom": "^21.1.4",
"@types/node": "^20.9.0",
"@sentry/tracing": "^7.80.1",
"@types/jsdom": "^21.1.5",
"@types/node": "^20.9.1",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.15",
"canvas-confetti": "^1.9.1",
@ -77,15 +77,15 @@
"patch-package": "^8.0.0",
"pg": "^8.11.3",
"react": "^18.2.0",
"react-aria": "^3.29.1",
"react-aria": "^3.30.0",
"react-cookie": "^6.1.1",
"react-dom": "^18.2.0",
"react-stately": "^3.27.1",
"react-stately": "^3.28.0",
"uuid": "^9.0.1",
"winston": "^3.11.0"
},
"devDependencies": {
"@faker-js/faker": "^8.2.0",
"@faker-js/faker": "^8.3.1",
"@playwright/test": "^1.36.1",
"@storybook/addon-a11y": "^7.5.3",
"@storybook/addon-essentials": "^7.5.3",
@ -96,20 +96,20 @@
"@storybook/react": "^7.5.1",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/adm-zip": "^0.5.3",
"@types/adm-zip": "^0.5.4",
"@types/canvas-confetti": "^1.6.3",
"@types/jest-axe": "^3.5.7",
"@types/jsonwebtoken": "^9.0.4",
"@types/jwk-to-pem": "^2.0.2",
"@types/nodemailer": "^6.4.13",
"@types/nodemailer": "^6.4.14",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"adm-zip": "^0.5.10",
"c8": "^8.0.1",
"eslint": "^8.53.0",
"eslint": "^8.54.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-check-file": "^2.6.2",
"eslint-plugin-header": "^3.1.1",
@ -123,14 +123,14 @@
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"jest-fail-on-console": "^3.1.1",
"lint-staged": "^15.0.2",
"lint-staged": "^15.1.0",
"prettier": "3.0.3",
"react-intersection-observer": "^9.5.2",
"sass": "^1.69.4",
"storybook": "^7.5.3",
"stylelint": "^15.11.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-scss": "^5.2.1",
"stylelint-scss": "^5.3.1",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"

Просмотреть файл

@ -13,6 +13,7 @@ import { WelcomeToPremiumView } from "./WelcomeToPremiumView";
import { getSubscriberEmails } from "../../../../../../../../functions/server/getSubscriberEmails";
import { StepDeterminationData } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { getCountryCode } from "../../../../../../../../functions/server/getCountryCode";
import { activateAndOptoutProfile } from "../../../../../../../../functions/server/onerep";
export default async function WelcomeToPremiumPage() {
const session = await getServerSession(authOptions);
@ -35,6 +36,12 @@ export default async function WelcomeToPremiumPage() {
user: session.user,
};
// If the current user is a subscriber and their OneRep profile is not
// activated: Most likely we were not able or failed to kick-off the
// auto-removal process.
// Lets make sure the users OneRep profile is activated:
await activateAndOptoutProfile(profileId);
return (
<WelcomeToPremiumView data={data} subscriberEmails={subscriberEmails} />
);

Просмотреть файл

@ -20,6 +20,7 @@ import {
import { getOnerepProfileId } from "../../../../../../db/tables/subscribers";
import {
activateAndOptoutProfile,
isEligibleForFreeScan,
isEligibleForPremium,
} from "../../../../../functions/server/onerep";
@ -57,10 +58,11 @@ export default async function DashboardPage() {
const isNewUser =
(parseIso8601Datetime(session.user.subscriber.created_at)?.getTime() ?? 0) >
brokerScanReleaseDate.getTime();
const isPremiumUser = hasPremium(session.user);
if (
!hasRunScan &&
(hasPremium(session.user) ||
(isPremiumUser ||
(isNewUser &&
canSubscribeToPremium({
user: session.user,
@ -72,6 +74,14 @@ export default async function DashboardPage() {
await refreshStoredScanResults(profileId);
// If the current user is a subscriber and their OneRep profile is not
// activated: Most likely we were not able or failed to kick-off the
// auto-removal process.
// Lets make sure the users OneRep profile is activated:
if (isPremiumUser) {
await activateAndOptoutProfile(profileId);
}
const latestScan = await getLatestOnerepScanResults(profileId);
const scanCount = await getScansCountForProfile(profileId);
const subBreaches = await getSubscriberBreaches(session.user);

Просмотреть файл

@ -94,9 +94,6 @@ export const EnterInfo = ({
),
value: firstName,
displayValue: firstName,
errorMessage: l10n.getString(
"onboarding-enter-details-input-error-message-generic",
),
isValid: firstName.trim() !== "",
onChange: setFirstName,
},
@ -109,9 +106,6 @@ export const EnterInfo = ({
),
value: lastName,
displayValue: lastName,
errorMessage: l10n.getString(
"onboarding-enter-details-input-error-message-generic",
),
isValid: lastName.trim() !== "",
onChange: setLastName,
},
@ -124,9 +118,6 @@ export const EnterInfo = ({
),
value: location,
displayValue: location,
errorMessage: l10n.getString(
"onboarding-enter-details-input-error-message-location",
),
isValid: location.trim() !== "",
onChange: setLocation,
},
@ -140,9 +131,6 @@ export const EnterInfo = ({
dateStyle: "medium",
timeZone: "UTC",
}),
errorMessage: l10n.getString(
"onboarding-enter-details-input-error-message-generic",
),
isValid: meetsAgeRequirement(dateOfBirth),
onChange: setDateOfBirth,
},
@ -326,34 +314,28 @@ export const EnterInfo = ({
<form onSubmit={handleOnSubmit}>
<div className={styles.inputContainer}>
{userDetailsData.map(
({
key,
errorMessage,
label,
onChange,
placeholder,
isValid,
type,
value,
}) => {
({ key, label, onChange, placeholder, isValid, type, value }) => {
const validationState =
!isValid && invalidInputs.includes(key) ? "invalid" : "valid";
return key === "location" ? (
<LocationAutocompleteInput
key={key}
errorMessage={errorMessage}
errorMessage={l10n.getString(
"onboarding-enter-details-input-error-message-location",
)}
label={label}
isRequired={true}
onChange={onChange}
onInputChange={onChange}
placeholder={placeholder}
type={type}
validationState={validationState}
value={value}
inputValue={value}
/>
) : (
<InputField
key={key}
errorMessage={errorMessage}
errorMessage={l10n.getString(
"onboarding-enter-details-input-error-message-generic",
)}
label={label}
isRequired={true}
onChange={onChange}

Просмотреть файл

@ -12,9 +12,8 @@ import {
getSubscribersByHashes,
} from "../../../../../../db/tables/subscribers";
import {
activateProfile,
activateAndOptoutProfile,
deactivateProfile,
optoutProfile,
} from "../../../../../functions/server/onerep";
import { captureException } from "@sentry/node";
import { deleteProfileDetails } from "../../../../../../db/tables/onerep_profiles";
@ -120,8 +119,7 @@ export async function PUT(
switch (action) {
case "subscribe": {
// activate and opt out profiles
await activateProfile(onerepProfileId);
await optoutProfile(onerepProfileId);
await activateAndOptoutProfile(onerepProfileId);
logger.info("force_user_subscribe", {
onerepProfileId,
primarySha1,

Просмотреть файл

@ -16,23 +16,27 @@ interface ComboBoxProps extends ComboBoxStateOptions<object> {
}
function ComboBox(props: ComboBoxProps) {
const { errorMessage, label, isRequired, validationState } = props;
const { label, isRequired, validationState } = props;
const inputRef = useRef(null);
const listBoxRef = useRef(null);
const popoverRef = useRef(null);
const state = useComboBoxState({ ...props });
const { inputProps, listBoxProps, labelProps, errorMessageProps } =
useComboBox(
{
...props,
inputRef,
listBoxRef,
popoverRef,
},
state,
);
const {
inputProps,
listBoxProps,
labelProps,
errorMessageProps,
validationErrors,
} = useComboBox(
{
...props,
inputRef,
listBoxRef,
popoverRef,
},
state,
);
const isInvalid = validationState === "invalid";
const showError = errorMessage && isInvalid;
return (
<>
@ -54,9 +58,16 @@ function ComboBox(props: ComboBoxProps) {
!inputProps.value ? styles.noValue : ""
} ${isInvalid ? styles.hasError : ""}`}
/>
{showError && (
{isInvalid && (
<div {...errorMessageProps} className={styles.inputMessage}>
{errorMessage}
{
// We always pass in a string at the time of writing, so we can't
// hit the "else" path with tests:
/* c8 ignore next 3 */
typeof props.errorMessage === "string"
? props.errorMessage
: validationErrors.join(" ")
}
</div>
)}
</div>

Просмотреть файл

@ -9,14 +9,11 @@ import { AriaTextFieldProps, useTextField } from "react-aria";
import styles from "./InputField.module.scss";
export const InputField = (props: AriaTextFieldProps) => {
const { errorMessage, isRequired, label, validationState, value } = props;
const { isRequired, label, validationState, value } = props;
const inputRef = useRef(null);
const { errorMessageProps, inputProps, labelProps } = useTextField(
props,
inputRef,
);
const { errorMessageProps, validationErrors, inputProps, labelProps } =
useTextField(props, inputRef);
const isInvalid = validationState === "invalid";
const showError = errorMessage && isInvalid;
return (
<div className={styles.input}>
@ -35,9 +32,16 @@ export const InputField = (props: AriaTextFieldProps) => {
isInvalid ? styles.hasError : ""
}`}
/>
{showError && (
{isInvalid && (
<div {...errorMessageProps} className={styles.inputMessage}>
{errorMessage}
{
// We always pass in a string at the time of writing, so we can't
// hit the "else" path with tests:
/* c8 ignore next 3 */
typeof props.errorMessage === "string"
? props.errorMessage
: validationErrors.join(" ")
}
</div>
)}
</div>

Просмотреть файл

@ -4,7 +4,7 @@
"use client";
import { Key, ReactNode, RefObject, useRef } from "react";
import { ReactNode, RefObject, useRef } from "react";
import { AriaListBoxOptions, useListBox, useOption } from "react-aria";
import { ListState } from "react-stately";
import { useElementWidth } from "../../hooks/useElementWidth";
@ -12,7 +12,7 @@ import styles from "./ListBox.module.scss";
export interface OptionProps extends AriaListBoxOptions<unknown> {
item: {
key: Key;
key: Parameters<typeof useOption>[0]["key"];
rendered: ReactNode;
};
state: ListState<object>;

Просмотреть файл

@ -4,9 +4,8 @@
"use client";
import { Key, useDeferredValue, useEffect, useState } from "react";
import { AriaTextFieldProps } from "react-aria";
import { Item } from "react-stately";
import { useDeferredValue, useEffect, useState } from "react";
import { ComboBoxStateOptions, Item } from "react-stately";
import { ComboBox } from "./ComboBox";
import {
MatchingLocations,
@ -61,20 +60,26 @@ function getLocationString(location: RelevantLocation) {
return `${name}, ${stateCode}, ${countryCode}`;
}
function getLocationStringByKey(locations: Array<RelevantLocation>, key: Key) {
function getLocationStringByKey(
locations: Array<RelevantLocation>,
key: ComboBoxStateOptions<object>["selectedKey"],
) {
const location = locations.find(({ id }) => id === key);
// TODO: Add unit test when changing this code:
/* c8 ignore next */
return location ? getLocationString(location) : "";
}
export const LocationAutocompleteInput = (props: AriaTextFieldProps) => {
export const LocationAutocompleteInput = (
props: ComboBoxStateOptions<object>,
) => {
const [searchQuery, setSearchQuery] = useState("");
const deferredSearchQuery = useDeferredValue(searchQuery);
const [locationSuggestions, setLocationSuggestions] =
useState<MatchingLocations>([]);
const [selectedKey, setSelectedKey] = useState<Key>("");
const [selectedKey, setSelectedKey] =
useState<ComboBoxStateOptions<object>["selectedKey"]>("");
useEffect(() => {
const abortController = new AbortController();
@ -137,12 +142,14 @@ export const LocationAutocompleteInput = (props: AriaTextFieldProps) => {
}
setSearchQuery(inputValue);
props.onChange?.(inputValue);
props.onInputChange?.(inputValue);
};
// TODO: Add unit test when changing this code:
/* c8 ignore next 3 */
const handleOnSelectionChange = (key: Key) => {
/* c8 ignore next 5 */
const handleOnSelectionChange = (
key: ComboBoxStateOptions<object>["selectedKey"],
) => {
setSelectedKey(key);
};

Просмотреть файл

@ -27,7 +27,7 @@ export type TabListProps = TabsProps & {
export interface TabParams {
item: {
key: Key;
key: Parameters<typeof useTab>[0]["key"];
rendered: ReactNode;
};
state: TabListState<object>;

Просмотреть файл

@ -9,7 +9,10 @@ import {
ISO8601DateString,
} from "../../../utils/parse.js";
import { StateAbbr } from "../../../utils/states.js";
import { getLatestOnerepScanResults } from "../../../db/tables/onerep_scans";
import {
getAllScansForProfile,
getLatestOnerepScanResults,
} from "../../../db/tables/onerep_scans";
import { RemovalStatus } from "../universal/scanResult.js";
import {
FeatureFlagName,
@ -216,6 +219,30 @@ export async function optoutProfile(profileId: number): Promise<void> {
);
}
}
export async function activateAndOptoutProfile(
profileId: number,
): Promise<void> {
try {
const scans = await getAllScansForProfile(profileId);
const hasInitialScan = scans.some(
(scan) => scan.onerep_scan_reason === "initial",
);
if (hasInitialScan) {
return;
}
const { status: profileStatus } = await getProfile(profileId);
if (profileStatus === "inactive") {
await activateProfile(profileId);
}
await optoutProfile(profileId);
} catch (error) {
logger.error("Failed to activate and optout profile:", error);
}
}
export async function createScan(
profileId: number,
): Promise<CreateScanResponse> {

Просмотреть файл

@ -8,7 +8,7 @@
* with the goal of deprecating the column
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../db/connect.js";
import { getAllBreachesFromDb } from "../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../utils/breaches.js";
import { setBreachResolution } from "../db/tables/subscribers.js";

Просмотреть файл

@ -8,7 +8,7 @@
* with the goal of deprecating the column
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
import { BreachDataTypes } from "../../utils/breach-resolution.js";

Просмотреть файл

@ -10,7 +10,7 @@
* `useBreachId: true/false`
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";

Просмотреть файл

@ -7,7 +7,7 @@
* The purpose of the script is to clean up some of the failed records during db migration on 3/28/23
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../db/connect.js";
import { getAllBreachesFromDb } from "../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../utils/breaches.js";
import { setBreachResolution } from "../db/tables/subscribers.js";

Просмотреть файл

@ -8,7 +8,7 @@
* with the goal of deprecating the column
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
import { BreachDataTypes } from "../../utils/breach-resolution.js";

Просмотреть файл

@ -8,7 +8,7 @@
* with the goal of deprecating the column
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
import { getAllEmailsAndBreaches } from "../../utils/breaches.js";
import { BreachDataTypes } from "../../utils/breach-resolution.js";

Просмотреть файл

@ -7,7 +7,7 @@
* The purpose of the script is to benchmark pure read with limit set as 100
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
const knex = createDbConnection();

Просмотреть файл

@ -7,7 +7,7 @@
* The purpose of the script is to benchmark pure read with limit set as 1000
*/
import { createDbConnection } from "../connect";
import { createDbConnection } from "../../db/connect.js";
import { getAllBreachesFromDb } from "../../utils/hibp.js";
const knex = createDbConnection();