From aa572e6be1a4bf0aac9e9530115a3e0ec25c4fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 29 Jul 2024 14:12:06 +0200 Subject: [PATCH 1/9] Use new warning page with new showing logic --- .../src/flows/recovery/recoveryWizard.ts | 51 +++++-- .../src/repositories/identityMetadata.test.ts | 141 +++++++++++++++++- .../src/repositories/identityMetadata.ts | 95 ++++++++---- src/frontend/src/utils/utils.ts | 7 + 4 files changed, 254 insertions(+), 40 deletions(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index e4fad90808..d7a163510e 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -4,9 +4,10 @@ import { renderPage } from "$src/utils/lit-html"; import { TemplateResult } from "lit-html"; import { AuthenticatedConnection } from "$src/utils/iiConnection"; -import { setupRecovery } from "./setupRecovery"; import { infoScreenTemplate } from "$src/components/infoScreen"; +import { isNullish } from "@dfinity/utils"; +import { addDevice } from "../addDevice/manage/addDevice"; import copyJson from "./recoveryWizard.json"; /* Phrase creation kick-off screen */ @@ -127,6 +128,23 @@ const addDeviceWarningTemplate = ({ // TODO: Create the `addDeviceWarning` page and use it in `recoveryWizard` function. export const addDeviceWarningPage = renderPage(addDeviceWarningTemplate); +// Prompt the user to create a recovery phrase +export const addDeviceWarning = ({ + status, +}: { + status: DeviceStatus; +}): Promise<{ action: "remind-later" | "do-not-remind" | "add-device" }> => { + return new Promise((resolve) => + addDeviceWarningPage({ + i18n: new I18n(), + ok: () => resolve({ action: "add-device" }), + remindLater: () => resolve({ action: "remind-later" }), + doNotRemindAgain: () => resolve({ action: "do-not-remind" }), + status, + }) + ); +}; + // TODO: Add e2e test https://dfinity.atlassian.net/browse/GIX-2600 export const recoveryWizard = async ( userNumber: bigint, @@ -134,9 +152,9 @@ export const recoveryWizard = async ( ): Promise => { // Here, if the user doesn't have any recovery device, we prompt them to add // one. - const [recoveries, identityMetadata] = await withLoader(() => + const [credentials, identityMetadata] = await withLoader(() => Promise.all([ - connection.lookupRecovery(userNumber), + connection.lookupCredentials(userNumber), connection.getIdentityMetadata(), ]) ); @@ -147,16 +165,31 @@ export const recoveryWizard = async ( const hasNotSeenRecoveryPageLastWeek = (identityMetadata?.recoveryPageShownTimestampMillis ?? 0) < oneWeekAgoTimestamp; - if (recoveries.length === 0 && hasNotSeenRecoveryPageLastWeek) { + const showWarningPageEnabled = isNullish( + identityMetadata?.doNotShowRecoveryPageRequestTimestampMillis + ); + const hasLessThanOneDevice = + credentials.credentials.length + credentials.recovery_credentials.length <= + 1; + if ( + hasLessThanOneDevice && + hasNotSeenRecoveryPageLastWeek && + showWarningPageEnabled + ) { // `await` here doesn't add any waiting time beacause we already got the metadata earlier. await connection.updateIdentityMetadata({ recoveryPageShownTimestampMillis: nowInMillis, }); - const doAdd = await addPhrase({ intent: "securityReminder" }); - if (doAdd !== "cancel") { - doAdd satisfies "ok"; - - await setupRecovery({ userNumber, connection }); + const userChoice = await addDeviceWarning({ status: "one-passkey" }); + if (userChoice.action === "add-device") { + await addDevice({ userNumber, connection }); + } + if (userChoice.action === "do-not-remind") { + // `await` here doesn't add any waiting time beacause we already got the metadata earlier. + await connection.updateIdentityMetadata({ + doNotShowRecoveryPageRequestTimestampMillis: nowInMillis, + }); } + // Do nothing if `"remind-later"`. } }; diff --git a/src/frontend/src/repositories/identityMetadata.test.ts b/src/frontend/src/repositories/identityMetadata.test.ts index 18940fbe23..1402bd2d7e 100644 --- a/src/frontend/src/repositories/identityMetadata.test.ts +++ b/src/frontend/src/repositories/identityMetadata.test.ts @@ -1,19 +1,26 @@ import { MetadataMapV2 } from "$generated/internet_identity_types"; import { + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, IdentityMetadata, IdentityMetadataRepository, RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, } from "./identityMetadata"; const recoveryPageShownTimestampMillis = 1234567890; +const doNotShowRecoveryPageRequestTimestampMillis = 3456789012; const mockRawMetadata: MetadataMapV2 = [ [ RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, { String: String(recoveryPageShownTimestampMillis) }, ], + [ + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, + { String: String(doNotShowRecoveryPageRequestTimestampMillis) }, + ], ]; const mockIdentityMetadata: IdentityMetadata = { recoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis, }; const getterMockSuccess = vi.fn().mockResolvedValue(mockRawMetadata); @@ -66,12 +73,81 @@ test("IdentityMetadataRepository returns undefined without raising an error if f expect(console.warn).toHaveBeenCalledTimes(1); }); +test("IdentityMetadataRepository changes partial data in memory", async () => { + const instance = IdentityMetadataRepository.init({ + getter: getterMockSuccess, + setter: setterMockSuccess, + }); + + const newRecoveryPageShownTimestampMillis = 9876543210; + await instance.updateMetadata({ + recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + doNotShowRecoveryPageRequestTimestampMillis, + }); +}); + test("IdentityMetadataRepository changes data in memory", async () => { const instance = IdentityMetadataRepository.init({ getter: getterMockSuccess, setter: setterMockSuccess, }); + const newRecoveryPageShownTimestampMillis = 9876543210; + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; + await instance.updateMetadata({ + recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); +}); + +test("IdentityMetadataRepository sets data from partial data in memory", async () => { + const partialMetadata: MetadataMapV2 = [ + [ + RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, + { String: String(recoveryPageShownTimestampMillis) }, + ], + ]; + const instance = IdentityMetadataRepository.init({ + getter: vi.fn().mockResolvedValue(partialMetadata), + setter: setterMockSuccess, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: recoveryPageShownTimestampMillis, + }); + + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; + await instance.updateMetadata({ + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: recoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); +}); + +test("IdentityMetadataRepository sets partial data in memory", async () => { + const noMetadata: MetadataMapV2 = []; + const instance = IdentityMetadataRepository.init({ + getter: vi.fn().mockResolvedValue(noMetadata), + setter: setterMockSuccess, + }); + const newRecoveryPageShownTimestampMillis = 9876543210; await instance.updateMetadata({ recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, @@ -90,12 +166,17 @@ test("IdentityMetadataRepository sets data in memory", async () => { }); const newRecoveryPageShownTimestampMillis = 9876543210; + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; await instance.updateMetadata({ recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, }); expect(await instance.getMetadata()).toEqual({ recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, }); }); @@ -106,8 +187,11 @@ test("IdentityMetadataRepository commits updated metadata to canister", async () }); const newRecoveryPageShownTimestampMillis = 9876543210; + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; await instance.updateMetadata({ recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, }); expect(setterMockSuccess).not.toHaveBeenCalled(); @@ -119,6 +203,10 @@ test("IdentityMetadataRepository commits updated metadata to canister", async () RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, { String: String(newRecoveryPageShownTimestampMillis) }, ], + [ + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, + { String: String(newDoNotShowRecoveryPageRequestTimestampMillis) }, + ], ]); }); @@ -141,8 +229,11 @@ test("IdentityMetadataRepository doesn't raise an error if committing fails", as }); const newRecoveryPageShownTimestampMillis = 9876543210; + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; const newMetadata = { recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, }; await instance.updateMetadata(newMetadata); @@ -156,6 +247,10 @@ test("IdentityMetadataRepository doesn't raise an error if committing fails", as RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, { String: String(newRecoveryPageShownTimestampMillis) }, ], + [ + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, + { String: String(newDoNotShowRecoveryPageRequestTimestampMillis) }, + ], ]); // But the value in memory is not lost. @@ -190,10 +285,54 @@ test("IdentityMetadataRepository commits additional metadata to canister after u expect(setterMockSuccess).toHaveBeenCalledTimes(1); expect(setterMockSuccess).toHaveBeenCalledWith([ - anotherMetadataEntry, [ RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, { String: String(newRecoveryPageShownTimestampMillis) }, ], + anotherMetadataEntry, + ]); +}); + +test("IdentityMetadataRepository commits from initial partial data after adding more partial data", async () => { + const partialMetadata: MetadataMapV2 = [ + [ + RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, + { String: String(recoveryPageShownTimestampMillis) }, + ], + ]; + const instance = IdentityMetadataRepository.init({ + getter: vi.fn().mockResolvedValue(partialMetadata), + setter: setterMockSuccess, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: recoveryPageShownTimestampMillis, + }); + + const newDoNotShowRecoveryPageRequestTimestampMillis = 1234567890; + await instance.updateMetadata({ + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); + + expect(await instance.getMetadata()).toEqual({ + recoveryPageShownTimestampMillis: recoveryPageShownTimestampMillis, + doNotShowRecoveryPageRequestTimestampMillis: + newDoNotShowRecoveryPageRequestTimestampMillis, + }); + + expect(setterMockSuccess).not.toHaveBeenCalled(); + await instance.commitMetadata(); + + expect(setterMockSuccess).toHaveBeenCalledTimes(1); + expect(setterMockSuccess).toHaveBeenCalledWith([ + [ + RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, + { String: String(recoveryPageShownTimestampMillis) }, + ], + [ + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, + { String: String(newDoNotShowRecoveryPageRequestTimestampMillis) }, + ], ]); }); diff --git a/src/frontend/src/repositories/identityMetadata.ts b/src/frontend/src/repositories/identityMetadata.ts index 9bc40199e3..754824c5d7 100644 --- a/src/frontend/src/repositories/identityMetadata.ts +++ b/src/frontend/src/repositories/identityMetadata.ts @@ -1,30 +1,73 @@ import { MetadataMapV2 } from "$generated/internet_identity_types"; +import { isValidKey } from "$src/utils/utils"; export type IdentityMetadata = { recoveryPageShownTimestampMillis?: number; + doNotShowRecoveryPageRequestTimestampMillis?: number; }; export const RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS = "recoveryPageShownTimestampMillis"; +export const DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS = + "doNotShowRecoveryPageRequestTimestampMillis"; +const NUMBER_KEYS: Array = [ + RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, + DO_NOT_SHOW_RECOVERY_PAGE_REQUEST_TIMESTAMP_MILLIS, +]; const convertMetadata = (rawMetadata: MetadataMapV2): IdentityMetadata => { - const recoveryPageEntry = rawMetadata.find( - ([key]) => key === RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS - ); - if (recoveryPageEntry === undefined) { - return {}; - } - const stringValue = recoveryPageEntry[1]; - if ("String" in stringValue) { - const recoveryPageShownTimestampMillis = Number(stringValue.String); - if (isNaN(recoveryPageShownTimestampMillis)) { - return {}; + return rawMetadata.reduce((acc, [key, value]) => { + if (isValidKey(key, NUMBER_KEYS)) { + if (!("String" in value)) { + return acc; + } + const stringValue = value.String; + const numberValue = Number(stringValue); + if (!isNaN(numberValue)) { + // We need to cast the key because TS is not smart enough to + acc[key] = numberValue; + } } - return { - recoveryPageShownTimestampMillis, - }; - } - return {}; + return acc; + }, {} as IdentityMetadata); +}; + +const updateMetadataMapV2 = ({ + metadataMap, + partialIdentityMetadata, +}: { + metadataMap: MetadataMapV2; + partialIdentityMetadata: Partial; +}): MetadataMapV2 => { + // Convert the partialIdentityMetadata into the format of MetadataMapV2 + const identityMetadataEntries: MetadataMapV2 = Object.entries( + partialIdentityMetadata + ).map(([key, value]) => { + if (typeof value === "number") { + return [key, { String: value.toString() }] as [ + string, + { String: string } + ]; + } + return [key, { String: value as string }] as [string, { String: string }]; + }); + + // Update or add entries in metadataMap + const updatedMetadataMap: MetadataMapV2 = metadataMap.map(([key, value]) => { + const updatedEntry = identityMetadataEntries.find( + ([identityKey]) => identityKey === key + ); + return updatedEntry ? updatedEntry : [key, value]; + }); + + // Add new entries that were not in the original metadataMap + identityMetadataEntries.forEach(([identityKey, identityValue]) => { + if (!metadataMap.some(([key]) => key === identityKey)) { + updatedMetadataMap.push([identityKey, identityValue]); + } + }); + + return updatedMetadataMap; }; type MetadataGetter = () => Promise; @@ -116,8 +159,8 @@ export class IdentityMetadataRepository { let currentWait = 0; const MAX_WAIT_MILLIS = 10_000; const ONE_WAIT_MILLIS = 1_000; - while (this.rawMetadata === "loading" || currentWait < MAX_WAIT_MILLIS) { - await new Promise((resolve) => setTimeout(resolve, 100)); + while (this.rawMetadata === "loading" && currentWait < MAX_WAIT_MILLIS) { + await new Promise((resolve) => setTimeout(resolve, ONE_WAIT_MILLIS)); currentWait += ONE_WAIT_MILLIS; } }; @@ -155,19 +198,11 @@ export class IdentityMetadataRepository { ): Promise => { await this.waitUntilMetadataIsLoaded(); if (this.metadataIsLoaded(this.rawMetadata)) { - let updatedMetadata: MetadataMapV2 = [...this.rawMetadata]; this.updatedMetadata = true; - updatedMetadata = updatedMetadata - .filter(([key]) => key !== RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS) - .concat([ - [ - RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS, - { - String: String(partialMetadata.recoveryPageShownTimestampMillis), - }, - ], - ]); - this.rawMetadata = updatedMetadata; + this.rawMetadata = updateMetadataMapV2({ + metadataMap: this.rawMetadata, + partialIdentityMetadata: partialMetadata, + }); } // Do nothing if the metadata is not loaded. }; diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 7351193d0a..95d6e21f00 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -353,3 +353,10 @@ export type OmitParams any, A extends string> = ( // Zip two arrays together export const zip = (a: A[], b: B[]): [A, B][] => Array.from(Array(Math.min(b.length, a.length)), (_, i) => [a[i], b[i]]); + +export const isValidKey = ( + key: string | number | symbol, + keys: Array +): key is keyof T => { + return keys.includes(key as keyof T); +}; From fd7ad838fabe0c44c602c583617b173b24a77553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 29 Jul 2024 15:49:11 +0200 Subject: [PATCH 2/9] Use a helper to be tested --- .../src/flows/recovery/recoveryWizard.test.ts | 147 ++++++++++++++++++ .../src/flows/recovery/recoveryWizard.ts | 64 ++++++-- 2 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 src/frontend/src/flows/recovery/recoveryWizard.test.ts diff --git a/src/frontend/src/flows/recovery/recoveryWizard.test.ts b/src/frontend/src/flows/recovery/recoveryWizard.test.ts new file mode 100644 index 0000000000..afec287dc3 --- /dev/null +++ b/src/frontend/src/flows/recovery/recoveryWizard.test.ts @@ -0,0 +1,147 @@ +import { + AnchorCredentials, + CredentialId, + PublicKey, + WebAuthnCredential, +} from "$generated/internet_identity_types"; +import { shouldShowRecoveryWarning } from "./recoveryWizard"; + +const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; +const nowInMillis = 1722259851155; +const moreThanAWeekAgo = nowInMillis - ONE_WEEK_MILLIS - 1; + +const noCredentials: AnchorCredentials = { + credentials: [], + recovery_credentials: [], + recovery_phrases: [], +}; + +const device: WebAuthnCredential = { + pubkey: [] as PublicKey, + credential_id: [] as CredentialId, +}; + +const oneDeviceOnly: AnchorCredentials = { + credentials: [device], + recovery_credentials: [], + recovery_phrases: [], +}; + +const oneRecoveryDeviceOnly: AnchorCredentials = { + credentials: [], + recovery_credentials: [device], + recovery_phrases: [], +}; + +const oneDeviceAndPhrase: AnchorCredentials = { + credentials: [device], + recovery_credentials: [], + recovery_phrases: [[] as PublicKey], +}; + +const twoDevices: AnchorCredentials = { + credentials: [device, { ...device }], + recovery_credentials: [], + recovery_phrases: [[] as PublicKey], +}; + +const oneNormalOneRecovery: AnchorCredentials = { + credentials: [device], + recovery_credentials: [device], + recovery_phrases: [[] as PublicKey], +}; + +test("shouldShowRecoveryWarning returns true for user with pin and has seen recovery longer than a week ago", () => { + expect( + shouldShowRecoveryWarning({ + credentials: noCredentials, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(true); +}); + +test("shouldShowRecoveryWarning returns true for user with one passkey and has seen recovery longer than a week ago", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneDeviceOnly, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(true); +}); + +test("shouldShowRecoveryWarning returns true for user with one passkey and empty identity metadata", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneDeviceOnly, + identityMetadata: {}, + nowInMillis, + }) + ).toBe(true); +}); + +test("shouldShowRecoveryWarning returns true for user with one recovery device and has seen recovery longer than a week ago", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneRecoveryDeviceOnly, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(true); +}); + +test("shouldShowRecoveryWarning doesn't count phrase as one device", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneDeviceAndPhrase, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(true); +}); + +test("shouldShowRecoveryWarning returns false for user with pin that has disabled the warning", () => { + expect( + shouldShowRecoveryWarning({ + credentials: noCredentials, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + doNotShowRecoveryPageRequestTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(false); +}); + +test("shouldShowRecoveryWarning returns false for user with two devices", () => { + expect( + shouldShowRecoveryWarning({ + credentials: twoDevices, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(false); +}); + +test("shouldShowRecoveryWarning returns false for user with one normal device and a recovery device", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneNormalOneRecovery, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(false); +}); diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index d7a163510e..570f4e9543 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -5,7 +5,9 @@ import { TemplateResult } from "lit-html"; import { AuthenticatedConnection } from "$src/utils/iiConnection"; +import { AnchorCredentials } from "$generated/internet_identity_types"; import { infoScreenTemplate } from "$src/components/infoScreen"; +import { IdentityMetadata } from "$src/repositories/identityMetadata"; import { isNullish } from "@dfinity/utils"; import { addDevice } from "../addDevice/manage/addDevice"; import copyJson from "./recoveryWizard.json"; @@ -145,6 +147,52 @@ export const addDeviceWarning = ({ ); }; +/** + * Helper to encapsulate the logic of when to show the recovery warning page. + * + * Three conditions must be met for the warning page to be shown: + * * Not having seen the recovery page in the last week + * (on registration, the user is not shown the page, but set it as seen to not bother during the onboarding) + * * The user has less than one device. + * (a phrase is not considered a device, only normal devices or recovery devices) + * (the pin is not considered a device) + * * The user has not disabled the warning. + * (users can choose to not see the warning again by clicking "do not remind" button) + * + * @param params {Object} + * @param params.credentials {AnchorCredentials} + * @param params.identityMetadata {IdentityMetadata | undefined} + * @param params.nowInMillis {number} + * @returns {boolean} + */ +// Exported for testing +export const shouldShowRecoveryWarning = ({ + credentials, + identityMetadata, + nowInMillis, +}: { + credentials: AnchorCredentials; + identityMetadata: IdentityMetadata | undefined; + nowInMillis: number; +}): boolean => { + const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; + const oneWeekAgoTimestamp = nowInMillis - ONE_WEEK_MILLIS; + const hasNotSeenRecoveryPageLastWeek = + (identityMetadata?.recoveryPageShownTimestampMillis ?? 0) < + oneWeekAgoTimestamp; + const showWarningPageEnabled = isNullish( + identityMetadata?.doNotShowRecoveryPageRequestTimestampMillis + ); + const hasLessThanOneDevice = + credentials.credentials.length + credentials.recovery_credentials.length <= + 1; + return ( + hasLessThanOneDevice && + hasNotSeenRecoveryPageLastWeek && + showWarningPageEnabled + ); +}; + // TODO: Add e2e test https://dfinity.atlassian.net/browse/GIX-2600 export const recoveryWizard = async ( userNumber: bigint, @@ -158,23 +206,9 @@ export const recoveryWizard = async ( connection.getIdentityMetadata(), ]) ); - - const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; const nowInMillis = Date.now(); - const oneWeekAgoTimestamp = nowInMillis - ONE_WEEK_MILLIS; - const hasNotSeenRecoveryPageLastWeek = - (identityMetadata?.recoveryPageShownTimestampMillis ?? 0) < - oneWeekAgoTimestamp; - const showWarningPageEnabled = isNullish( - identityMetadata?.doNotShowRecoveryPageRequestTimestampMillis - ); - const hasLessThanOneDevice = - credentials.credentials.length + credentials.recovery_credentials.length <= - 1; if ( - hasLessThanOneDevice && - hasNotSeenRecoveryPageLastWeek && - showWarningPageEnabled + shouldShowRecoveryWarning({ credentials, identityMetadata, nowInMillis }) ) { // `await` here doesn't add any waiting time beacause we already got the metadata earlier. await connection.updateIdentityMetadata({ From 2fc3d0a884baddba79be968e1c41a4f43ceaffc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 29 Jul 2024 16:09:42 +0200 Subject: [PATCH 3/9] Add missing tests --- .../src/flows/recovery/recoveryWizard.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.test.ts b/src/frontend/src/flows/recovery/recoveryWizard.test.ts index afec287dc3..ffadc67ba8 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.test.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.test.ts @@ -9,6 +9,7 @@ import { shouldShowRecoveryWarning } from "./recoveryWizard"; const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; const nowInMillis = 1722259851155; const moreThanAWeekAgo = nowInMillis - ONE_WEEK_MILLIS - 1; +const lessThanAWeekAgo = nowInMillis - 1; const noCredentials: AnchorCredentials = { credentials: [], @@ -45,6 +46,12 @@ const twoDevices: AnchorCredentials = { recovery_phrases: [[] as PublicKey], }; +const threeDevices: AnchorCredentials = { + credentials: [device, { ...device }, { ...device }], + recovery_credentials: [], + recovery_phrases: [[] as PublicKey], +}; + const oneNormalOneRecovery: AnchorCredentials = { credentials: [device], recovery_credentials: [device], @@ -122,6 +129,19 @@ test("shouldShowRecoveryWarning returns false for user with pin that has disable ).toBe(false); }); +test("shouldShowRecoveryWarning returns false for user with one device that has disabled the warning", () => { + expect( + shouldShowRecoveryWarning({ + credentials: oneDeviceOnly, + identityMetadata: { + recoveryPageShownTimestampMillis: moreThanAWeekAgo, + doNotShowRecoveryPageRequestTimestampMillis: moreThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(false); +}); + test("shouldShowRecoveryWarning returns false for user with two devices", () => { expect( shouldShowRecoveryWarning({ @@ -145,3 +165,25 @@ test("shouldShowRecoveryWarning returns false for user with one normal device an }) ).toBe(false); }); + +test("shouldShowRecoveryWarning returns false for user with more than two devices and empty identity metadata", () => { + expect( + shouldShowRecoveryWarning({ + credentials: threeDevices, + identityMetadata: {}, + nowInMillis, + }) + ).toBe(false); +}); + +test("shouldShowRecoveryWarning returns false for user with pin and has seen recovery less than a week ago", () => { + expect( + shouldShowRecoveryWarning({ + credentials: noCredentials, + identityMetadata: { + recoveryPageShownTimestampMillis: lessThanAWeekAgo, + }, + nowInMillis, + }) + ).toBe(false); +}); From 95f36ccf9b99dbbbb5621511ae5deb8530a7587a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 30 Jul 2024 10:42:39 +0200 Subject: [PATCH 4/9] Improve test description --- src/frontend/src/flows/recovery/recoveryWizard.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.test.ts b/src/frontend/src/flows/recovery/recoveryWizard.test.ts index ffadc67ba8..7fe7cea379 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.test.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.test.ts @@ -104,7 +104,7 @@ test("shouldShowRecoveryWarning returns true for user with one recovery device a ).toBe(true); }); -test("shouldShowRecoveryWarning doesn't count phrase as one device", () => { +test("shouldShowRecoveryWarning returns true for user with one device and a recovery phrase", () => { expect( shouldShowRecoveryWarning({ credentials: oneDeviceAndPhrase, From 7466cdcb3d22adf462a8fcc081121295bcfaebf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 30 Jul 2024 10:46:46 +0200 Subject: [PATCH 5/9] Improve comment --- src/frontend/src/flows/recovery/recoveryWizard.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index 570f4e9543..b5520b5f96 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -154,8 +154,7 @@ export const addDeviceWarning = ({ * * Not having seen the recovery page in the last week * (on registration, the user is not shown the page, but set it as seen to not bother during the onboarding) * * The user has less than one device. - * (a phrase is not considered a device, only normal devices or recovery devices) - * (the pin is not considered a device) + * (a phrase and pin are not considered a device, only normal devices or recovery devices) * * The user has not disabled the warning. * (users can choose to not see the warning again by clicking "do not remind" button) * From 3e3371db0ed3b71bc1c9c440b8fff88e51b70cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 30 Jul 2024 11:43:23 +0200 Subject: [PATCH 6/9] Refactor helper --- .../src/flows/recovery/recoveryWizard.test.ts | 83 +++++++++++-------- .../src/flows/recovery/recoveryWizard.ts | 65 ++++++++++----- .../pages/addDeviceWarningOnePasskey.astro | 2 +- 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.test.ts b/src/frontend/src/flows/recovery/recoveryWizard.test.ts index 7fe7cea379..2f686bbff4 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.test.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.test.ts @@ -4,13 +4,17 @@ import { PublicKey, WebAuthnCredential, } from "$generated/internet_identity_types"; -import { shouldShowRecoveryWarning } from "./recoveryWizard"; +import { PinIdentityMaterial } from "../pin/idb"; +import { getDevicesStatus } from "./recoveryWizard"; const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; const nowInMillis = 1722259851155; const moreThanAWeekAgo = nowInMillis - ONE_WEEK_MILLIS - 1; const lessThanAWeekAgo = nowInMillis - 1; +const pinIdentityMaterial: PinIdentityMaterial = + {} as unknown as PinIdentityMaterial; + const noCredentials: AnchorCredentials = { credentials: [], recovery_credentials: [], @@ -58,132 +62,143 @@ const oneNormalOneRecovery: AnchorCredentials = { recovery_phrases: [[] as PublicKey], }; -test("shouldShowRecoveryWarning returns true for user with pin and has seen recovery longer than a week ago", () => { +test("getDevicesStatus returns 'pin-only' for user with pin and has seen recovery longer than a week ago", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: noCredentials, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial, nowInMillis, }) - ).toBe(true); + ).toBe("pin-only"); }); -test("shouldShowRecoveryWarning returns true for user with one passkey and has seen recovery longer than a week ago", () => { +test("getDevicesStatus returns 'one-device' for user with one passkey and has seen recovery longer than a week ago", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneDeviceOnly, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(true); + ).toBe("one-device"); }); -test("shouldShowRecoveryWarning returns true for user with one passkey and empty identity metadata", () => { +test("getDevicesStatus returns true for user with one passkey and empty identity metadata", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneDeviceOnly, identityMetadata: {}, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(true); + ).toBe("one-device"); }); -test("shouldShowRecoveryWarning returns true for user with one recovery device and has seen recovery longer than a week ago", () => { +test("getDevicesStatus returns 'one-device' for user with one recovery device and has seen recovery longer than a week ago", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneRecoveryDeviceOnly, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(true); + ).toBe("one-device"); }); -test("shouldShowRecoveryWarning returns true for user with one device and a recovery phrase", () => { +test("getDevicesStatus returns 'one-device' for user with one device and a recovery phrase", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneDeviceAndPhrase, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(true); + ).toBe("one-device"); }); -test("shouldShowRecoveryWarning returns false for user with pin that has disabled the warning", () => { +test("getDevicesStatus returns 'no-warning' for user with pin that has disabled the warning", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: noCredentials, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, doNotShowRecoveryPageRequestTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); -test("shouldShowRecoveryWarning returns false for user with one device that has disabled the warning", () => { +test("getDevicesStatus returns 'no-warning' for user with one device that has disabled the warning", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneDeviceOnly, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, doNotShowRecoveryPageRequestTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); -test("shouldShowRecoveryWarning returns false for user with two devices", () => { +test("getDevicesStatus returns 'no-warning' for user with two devices", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: twoDevices, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); -test("shouldShowRecoveryWarning returns false for user with one normal device and a recovery device", () => { +test("getDevicesStatus returns 'no-warning' for user with one normal device and a recovery device", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: oneNormalOneRecovery, identityMetadata: { recoveryPageShownTimestampMillis: moreThanAWeekAgo, }, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); -test("shouldShowRecoveryWarning returns false for user with more than two devices and empty identity metadata", () => { +test("getDevicesStatus returns 'no-warning' for user with more than two devices and empty identity metadata", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: threeDevices, identityMetadata: {}, + pinIdentityMaterial: undefined, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); -test("shouldShowRecoveryWarning returns false for user with pin and has seen recovery less than a week ago", () => { +test("getDevicesStatus returns 'no-warning' for user with pin and has seen recovery less than a week ago", () => { expect( - shouldShowRecoveryWarning({ + getDevicesStatus({ credentials: noCredentials, identityMetadata: { recoveryPageShownTimestampMillis: lessThanAWeekAgo, }, + pinIdentityMaterial, nowInMillis, }) - ).toBe(false); + ).toBe("no-warning"); }); diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index b5520b5f96..80ab6d07c5 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -10,6 +10,10 @@ import { infoScreenTemplate } from "$src/components/infoScreen"; import { IdentityMetadata } from "$src/repositories/identityMetadata"; import { isNullish } from "@dfinity/utils"; import { addDevice } from "../addDevice/manage/addDevice"; +import { + PinIdentityMaterial, + idbRetrievePinIdentityMaterial, +} from "../pin/idb"; import copyJson from "./recoveryWizard.json"; /* Phrase creation kick-off screen */ @@ -83,7 +87,7 @@ export const addPhrase = ({ ); }; -type DeviceStatus = "pin-only" | "one-passkey"; +type DeviceStatus = "pin-only" | "one-device"; const addDeviceWarningTemplate = ({ ok, @@ -105,7 +109,7 @@ const addDeviceWarningTemplate = ({ copy.paragraph_add_device_pin_only, copy.add_device_title_pin_only, ], - "one-passkey": [ + "one-device": [ copy.paragraph_add_device_one_passkey, copy.add_device_title_one_passkey, ], @@ -162,18 +166,20 @@ export const addDeviceWarning = ({ * @param params.credentials {AnchorCredentials} * @param params.identityMetadata {IdentityMetadata | undefined} * @param params.nowInMillis {number} - * @returns {boolean} + * @returns {DeviceStatus | "no-warning"} */ // Exported for testing -export const shouldShowRecoveryWarning = ({ +export const getDevicesStatus = ({ credentials, identityMetadata, + pinIdentityMaterial, nowInMillis, }: { credentials: AnchorCredentials; identityMetadata: IdentityMetadata | undefined; + pinIdentityMaterial: PinIdentityMaterial | undefined; nowInMillis: number; -}): boolean => { +}): DeviceStatus | "no-warning" => { const ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000; const oneWeekAgoTimestamp = nowInMillis - ONE_WEEK_MILLIS; const hasNotSeenRecoveryPageLastWeek = @@ -182,14 +188,21 @@ export const shouldShowRecoveryWarning = ({ const showWarningPageEnabled = isNullish( identityMetadata?.doNotShowRecoveryPageRequestTimestampMillis ); - const hasLessThanOneDevice = - credentials.credentials.length + credentials.recovery_credentials.length <= - 1; - return ( - hasLessThanOneDevice && + const totalDevicesCount = + credentials.credentials.length + credentials.recovery_credentials.length; + if ( + totalDevicesCount <= 1 && hasNotSeenRecoveryPageLastWeek && showWarningPageEnabled - ); + ) { + if (totalDevicesCount === 0 && !pinIdentityMaterial) { + // This should never happen because it means that the user has no devices and no pin. + // But we still handle it to avoid a crash assuming there was an error retrieving the pin material. + return "pin-only"; + } + return totalDevicesCount === 0 ? "pin-only" : "one-device"; + } + return "no-warning"; }; // TODO: Add e2e test https://dfinity.atlassian.net/browse/GIX-2600 @@ -199,21 +212,33 @@ export const recoveryWizard = async ( ): Promise => { // Here, if the user doesn't have any recovery device, we prompt them to add // one. - const [credentials, identityMetadata] = await withLoader(() => - Promise.all([ - connection.lookupCredentials(userNumber), - connection.getIdentityMetadata(), - ]) + const [credentials, identityMetadata, pinIdentityMaterial] = await withLoader( + () => + Promise.all([ + connection.lookupCredentials(userNumber), + connection.getIdentityMetadata(), + idbRetrievePinIdentityMaterial({ + userNumber, + }), + ]) ); const nowInMillis = Date.now(); - if ( - shouldShowRecoveryWarning({ credentials, identityMetadata, nowInMillis }) - ) { + + const devivesStatus = getDevicesStatus({ + credentials, + identityMetadata, + pinIdentityMaterial, + nowInMillis, + }); + + if (devivesStatus !== "no-warning") { // `await` here doesn't add any waiting time beacause we already got the metadata earlier. await connection.updateIdentityMetadata({ recoveryPageShownTimestampMillis: nowInMillis, }); - const userChoice = await addDeviceWarning({ status: "one-passkey" }); + const userChoice = await addDeviceWarning({ + status: devivesStatus, + }); if (userChoice.action === "add-device") { await addDevice({ userNumber, connection }); } diff --git a/src/showcase/src/pages/addDeviceWarningOnePasskey.astro b/src/showcase/src/pages/addDeviceWarningOnePasskey.astro index 6079f1d049..0936d519de 100644 --- a/src/showcase/src/pages/addDeviceWarningOnePasskey.astro +++ b/src/showcase/src/pages/addDeviceWarningOnePasskey.astro @@ -13,7 +13,7 @@ import Screen from "../layouts/Screen.astro"; remindLater: () => toast.info("Remind later"), doNotRemindAgain: () => toast.info("Do not remind again"), i18n, - status: "one-passkey", + status: "one-device", }); From 8b67700b98b69313c52e190787d4ee39df8c7aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 30 Jul 2024 11:50:50 +0200 Subject: [PATCH 7/9] Improve comment --- src/frontend/src/flows/recovery/recoveryWizard.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index 80ab6d07c5..19c9f009ef 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -152,7 +152,7 @@ export const addDeviceWarning = ({ }; /** - * Helper to encapsulate the logic of when to show the recovery warning page. + * Helper to encapsulate the logic of when and which recovery warning page to show. * * Three conditions must be met for the warning page to be shown: * * Not having seen the recovery page in the last week @@ -162,9 +162,14 @@ export const addDeviceWarning = ({ * * The user has not disabled the warning. * (users can choose to not see the warning again by clicking "do not remind" button) * + * If the page is shown, there are two options: + * * User has only the pin authentication method. + * * User has only one device. + * * @param params {Object} * @param params.credentials {AnchorCredentials} * @param params.identityMetadata {IdentityMetadata | undefined} + * @param params.pinIdentityMaterial {PinIdentityMaterial | undefined} * @param params.nowInMillis {number} * @returns {DeviceStatus | "no-warning"} */ From 4cc7bad9b1eb4ed37c4992de25cfc8910925887f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Wed, 31 Jul 2024 11:10:40 +0200 Subject: [PATCH 8/9] Improve comments --- src/frontend/src/flows/recovery/recoveryWizard.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index 19c9f009ef..2f3adf35a6 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -157,12 +157,12 @@ export const addDeviceWarning = ({ * Three conditions must be met for the warning page to be shown: * * Not having seen the recovery page in the last week * (on registration, the user is not shown the page, but set it as seen to not bother during the onboarding) - * * The user has less than one device. + * * The user has at most one device. * (a phrase and pin are not considered a device, only normal devices or recovery devices) * * The user has not disabled the warning. * (users can choose to not see the warning again by clicking "do not remind" button) * - * If the page is shown, there are two options: + * When the warning page is shown, two different messages could be displayed: * * User has only the pin authentication method. * * User has only one device. * From 83997a0d02972e34ec880f210dc14cefc477df6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Wed, 31 Jul 2024 13:20:20 +0200 Subject: [PATCH 9/9] Fix type --- src/frontend/src/flows/recovery/recoveryWizard.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/flows/recovery/recoveryWizard.ts b/src/frontend/src/flows/recovery/recoveryWizard.ts index 2f3adf35a6..3af05989be 100644 --- a/src/frontend/src/flows/recovery/recoveryWizard.ts +++ b/src/frontend/src/flows/recovery/recoveryWizard.ts @@ -229,20 +229,20 @@ export const recoveryWizard = async ( ); const nowInMillis = Date.now(); - const devivesStatus = getDevicesStatus({ + const devicesStatus = getDevicesStatus({ credentials, identityMetadata, pinIdentityMaterial, nowInMillis, }); - if (devivesStatus !== "no-warning") { + if (devicesStatus !== "no-warning") { // `await` here doesn't add any waiting time beacause we already got the metadata earlier. await connection.updateIdentityMetadata({ recoveryPageShownTimestampMillis: nowInMillis, }); const userChoice = await addDeviceWarning({ - status: devivesStatus, + status: devicesStatus, }); if (userChoice.action === "add-device") { await addDevice({ userNumber, connection });