diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index daac3bfed8..2b3603549d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,6 +16,11 @@ on: options: - staging.element.io - app.element.io + skip-checks: + description: Skip CI on the tagged commit + required: true + default: false + type: boolean concurrency: ${{ inputs.site || 'staging.element.io' }} permissions: {} jobs: @@ -75,6 +80,7 @@ jobs: - name: Wait for other steps to succeed uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork + if: inputs.skip-checks != true with: ref: ${{ github.sha }} running-workflow-name: "Deploy to Cloudflare Pages" diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe9f061ed..d5e000f494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18) +================================================================================================== +This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room. + +## 🐛 Bug Fixes + +* Upgrade matrix-sdk-crypto-wasm to 1.11.0 (https://github.com/matrix-org/matrix-js-sdk/pull/4593) +* Fix url preview display ([#28766](https://github.com/element-hq/element-web/pull/28766)). + + +Changes in [1.11.88](https://github.com/element-hq/element-web/releases/tag/v1.11.88) (2024-12-17) +================================================================================================== +## ✨ Features + +* Allow trusted Element Call widget to send and receive media encryption key to-device messages ([#28316](https://github.com/element-hq/element-web/pull/28316)). Contributed by @hughns. +* increase ringing timeout from 10 seconds to 90 seconds ([#28630](https://github.com/element-hq/element-web/pull/28630)). Contributed by @fkwp. +* Add `Close` tooltip to dialog ([#28617](https://github.com/element-hq/element-web/pull/28617)). Contributed by @florianduros. +* New UX for Share dialog ([#28598](https://github.com/element-hq/element-web/pull/28598)). Contributed by @florianduros. +* Improve performance of RoomContext in RoomHeader ([#28574](https://github.com/element-hq/element-web/pull/28574)). Contributed by @t3chguy. +* Remove `Features.RustCrypto` flag ([#28582](https://github.com/element-hq/element-web/pull/28582)). Contributed by @florianduros. +* Add Modernizr warning when running in non-secure context ([#28581](https://github.com/element-hq/element-web/pull/28581)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Fix jumpy timeline when the pinned message banner is displayed ([#28654](https://github.com/element-hq/element-web/pull/28654)). Contributed by @florianduros. +* Fix font \& spaces in settings subsection ([#28631](https://github.com/element-hq/element-web/pull/28631)). Contributed by @florianduros. +* Remove manual device verification which is not supported by the new cryptography stack ([#28588](https://github.com/element-hq/element-web/pull/28588)). Contributed by @florianduros. +* Fix code block highlighting not working reliably with many code blocks ([#28613](https://github.com/element-hq/element-web/pull/28613)). Contributed by @t3chguy. +* Remove remaining reply fallbacks code ([#28610](https://github.com/element-hq/element-web/pull/28610)). Contributed by @t3chguy. +* Provide a way to activate GIFs via the keyboard for a11y ([#28611](https://github.com/element-hq/element-web/pull/28611)). Contributed by @t3chguy. +* Fix format bar position ([#28591](https://github.com/element-hq/element-web/pull/28591)). Contributed by @florianduros. +* Fix room taking long time to load ([#28579](https://github.com/element-hq/element-web/pull/28579)). Contributed by @florianduros. +* Show the correct shield status in tooltip for more conditions ([#28476](https://github.com/element-hq/element-web/pull/28476)). Contributed by @uhoreg. + + Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03) ================================================================================================== ## ✨ Features diff --git a/package.json b/package.json index 9cd0163945..999d1237f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.87", + "version": "1.11.89", "description": "A feature-rich client for Matrix.org", "author": "New Vector Ltd.", "repository": { @@ -282,7 +282,7 @@ "terser-webpack-plugin": "^5.3.9", "ts-node": "^10.9.1", "ts-prune": "^0.10.3", - "typescript": "5.6.3", + "typescript": "5.7.2", "util": "^0.12.5", "web-streams-polyfill": "^4.0.0", "webpack": "^5.89.0", diff --git a/playwright/e2e/crypto/backups.spec.ts b/playwright/e2e/crypto/backups.spec.ts index da162474fa..d174cc89e5 100644 --- a/playwright/e2e/crypto/backups.spec.ts +++ b/playwright/e2e/crypto/backups.spec.ts @@ -9,6 +9,8 @@ Please see LICENSE files in the repository root for full details. import { type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; +import { test as masTest, registerAccountMas } from "../oidc"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText( @@ -18,6 +20,32 @@ async function expectBackupVersionToBe(page: Page, version: string) { await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version); } +masTest.describe("Encryption state after registration", () => { + masTest.skip(isDendrite, "does not yet support MAS"); + + masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + + await app.settings.openUserSettings("Security & Privacy"); + expect(page.getByText("This session is backing up your keys.")).toBeVisible(); + }); + + masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => { + await page.goto("/#/login"); + await page.getByRole("button", { name: "Continue" }).click(); + await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!"); + + await page.getByRole("button", { name: "Add room" }).click(); + await page.getByRole("menuitem", { name: "New room" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("test room"); + await page.getByRole("button", { name: "Create room" }).click(); + + await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible(); + }); +}); + test.describe("Backups", () => { test.use({ displayName: "Hanako", diff --git a/playwright/e2e/crypto/dehydration.spec.ts b/playwright/e2e/crypto/dehydration.spec.ts index 590ab774b5..39629c8262 100644 --- a/playwright/e2e/crypto/dehydration.spec.ts +++ b/playwright/e2e/crypto/dehydration.spec.ts @@ -8,11 +8,11 @@ Please see LICENSE files in the repository root for full details. import { Locator, type Page } from "@playwright/test"; -import { test as base, expect } from "../../element-web-test"; +import { test as base, expect, Fixtures } from "../../element-web-test"; import { viewRoomSummaryByName } from "../right-panel/utils"; import { isDendrite } from "../../plugins/homeserver/dendrite"; -const test = base.extend({ +const test = base.extend({ // eslint-disable-next-line no-empty-pattern startHomeserverOpts: async ({}, use) => { await use("dehydration"); @@ -50,8 +50,6 @@ test.describe("Dehydration", () => { }); test("Create dehydrated device", async ({ page, user, app }, workerInfo) => { - test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto."); - // Create a backup (which will create SSSS, and dehydrated device) const securityTab = await app.settings.openUserSettings("Security & Privacy"); diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 0beb8e3650..da9fe1fd1a 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -133,8 +133,7 @@ test.describe("Cryptography", function () { "Encrypted by a device not verified by its owner.", ); - /* In legacy crypto: should show a grey padlock for a message from a deleted device. - * In rust crypto: should show a red padlock for a message from an unverified device. + /* Should show a red padlock for a message from an unverified device. * Rust crypto remembers the verification state of the sending device, so it will know that the device was * unverified, even if it gets deleted. */ // bob deletes his second device @@ -168,9 +167,7 @@ test.describe("Cryptography", function () { await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - workerInfo.project.name === "Legacy Crypto" - ? "Encrypted by an unknown or deleted device." - : "Encrypted by a device not verified by its owner.", + "Encrypted by a device not verified by its owner.", ); }); diff --git a/playwright/e2e/crypto/migration.spec.ts b/playwright/e2e/crypto/migration.spec.ts index 048b39f06a..a9530a288b 100644 --- a/playwright/e2e/crypto/migration.spec.ts +++ b/playwright/e2e/crypto/migration.spec.ts @@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details. import path from "path"; import { readFile } from "node:fs/promises"; -import { expect, test as base } from "../../element-web-test"; +import { expect, Fixtures, test as base } from "../../element-web-test"; -const test = base.extend({ +const test = base.extend({ // Replace the `user` fixture with one which populates the indexeddb data before starting the app. user: async ({ context, pageWithCredentials: page, credentials }, use) => { await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => { @@ -29,7 +29,6 @@ test.describe("migration", function () { test.use({ displayName: "Alice" }); test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => { - test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto."); test.slow(); // We should see a migration progress bar diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 94b1933977..337ff3d634 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -220,11 +220,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle { // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed // Note these assertions do not check the size of mx_LegacyRoomHeader_name button - const buttons = header.locator(".mx_Flex").getByRole("button"); + const buttons = header.getByRole("button").filter({ + has: page.locator("svg"), + }); await expect(buttons).toHaveCount(5); for (const button of await buttons.all()) { diff --git a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.spec.ts similarity index 100% rename from playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts rename to playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.spec.ts diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 76e57e33f7..6ac0b7226a 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -60,7 +60,7 @@ interface CredentialsWithDisplayName extends Credentials { displayName: string; } -export const test = base.extend<{ +export interface Fixtures { axe: AxeBuilder; checkA11y: () => Promise; @@ -124,7 +124,9 @@ export const test = base.extend<{ slidingSyncProxy: ProxyInstance; labsFlags: string[]; webserver: Webserver; -}>({ +} + +export const test = base.extend({ config: CONFIG_JSON, page: async ({ context, page, config, labsFlags }, use) => { await context.route(`http://localhost:8080/config.json*`, async (route) => { diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index fc09b6a22b..863e236c69 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:ef3d491214fa380918c736d9aa720992fb58829ce5c06fa3ca36d357fa1df75d"; +const DOCKER_TAG = "develop@sha256:c965896a4865479ab2628807ebf6d9c742586f3b6185a56f10077a408f1c7c3b"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png index afc5d53fab..4ba22a5220 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png index ce15e3e151..ef6112da1d 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png index bd31e502d7..ed8c75104f 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png new file mode 100644 index 0000000000..eaa68eae4a Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-bubble-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png new file mode 100644 index 0000000000..036ff61851 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/message-layout-panel.spec.ts/message-layout-panel-modern-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index 348db69cfc..00b271004e 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index 42ee5a0acb..8f11c831db 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index 92532e3d9c..6365543947 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 1e50cd3c0f..d8a5ae4056 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png index b0960a1188..58c844a54d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index a7637b6b94..d8e6da9f8f 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index a609a4cd0d..e1a4e6ef06 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png index fe50abef0c..032a8c1118 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index ac6dadc962..b31eae03f6 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index 8e833be308..1c7265ca62 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 3e9e78ca99..33ef04df3c 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 1e50cd3c0f..d8a5ae4056 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index b81a9d68a8..608b17051d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 58ba6c5703..06aa02cdf8 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/src/CreateCrossSigning.ts b/src/CreateCrossSigning.ts index e67e030f60..19b2977a9f 100644 --- a/src/CreateCrossSigning.ts +++ b/src/CreateCrossSigning.ts @@ -7,59 +7,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { logger } from "matrix-js-sdk/src/logger"; -import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; +import { AuthDict, MatrixClient, MatrixError, UIAResponse } from "matrix-js-sdk/src/matrix"; import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents"; import Modal from "./Modal"; import { _t } from "./languageHandler"; import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; -/** - * Determine if the homeserver allows uploading device keys with only password auth. - * @param cli The Matrix Client to use - * @returns True if the homeserver allows uploading device keys with only password auth, otherwise false - */ -async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise { - try { - await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys); - // We should never get here: the server should always require - // UI auth to upload device signing keys. If we do, we upload - // no keys which would be a no-op. - logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); - return false; - } catch (error) { - if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { - logger.log("uploadDeviceSigningKeys advertised no flows!"); - return false; - } - const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { - return f.stages.length === 1 && f.stages[0] === "m.login.password"; - }); - return canUploadKeysWithPasswordOnly; - } -} - /** * Ensures that cross signing keys are created and uploaded for the user. * The homeserver may require user-interactive auth to upload the keys, in - * which case the user will be prompted to authenticate. If the homeserver - * allows uploading keys with just an account password and one is provided, - * the keys will be uploaded without user interaction. + * which case the user will be prompted to authenticate. * * This function does not set up backups of the created cross-signing keys * (or message keys): the cross-signing keys are stored locally and will be * lost requiring a crypto reset, if the user logs out or loses their session. * * @param cli The Matrix Client to use - * @param isTokenLogin True if the user logged in via a token login, otherwise false - * @param accountPassword The password that the user logged in with */ -export async function createCrossSigning( - cli: MatrixClient, - isTokenLogin: boolean, - accountPassword?: string, -): Promise { +export async function createCrossSigning(cli: MatrixClient): Promise { const cryptoApi = cli.getCrypto(); if (!cryptoApi) { throw new Error("No crypto API found!"); @@ -68,19 +34,14 @@ export async function createCrossSigning( const doBootstrapUIAuth = async ( makeRequest: (authData: AuthDict) => Promise>, ): Promise => { - if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) { - await makeRequest({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: cli.getUserId(), - }, - password: accountPassword, - }); - } else if (isTokenLogin) { - // We are hoping the grace period is active + try { await makeRequest({}); - } else { + } catch (error) { + if (!(error instanceof MatrixError) || !error.data || !error.data.flows) { + // Not a UIA response + throw error; + } + const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("auth|uia|sso_title"), diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 84d83827da..e34af95962 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -295,21 +295,29 @@ export default class DeviceListener { await crypto.getUserDeviceInfo([cli.getSafeUserId()]); // cross signing isn't enabled - nag to enable it - // There are 2 different toasts for: + // There are 3 different toasts for: if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) { - // Cross-signing on account but this device doesn't trust the master key (verify this session) + // Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); this.checkKeyBackupStatus(); } else { - // No cross-signing or key backup on account (set up encryption) - await cli.waitForClientWellKnown(); - if (isSecureBackupRequired(cli) && isLoggedIn()) { - // If we're meant to set up, and Secure Backup is required, - // trigger the flow directly without a toast once logged in. - hideSetupEncryptionToast(); - accessSecretStorage(); + const backupInfo = await this.getKeyBackupInfo(); + if (backupInfo) { + // Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery. + // Since we now enable key backup at registration time, this will be the common case for + // new users. + showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY); } else { - showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + // Toast 3: No cross-signing or key backup on account (set up encryption) + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired(cli) && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); + } else { + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } } } } diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index e8122b2dbf..10672917be 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -191,8 +191,6 @@ export interface AccessSecretStorageOpts { forceReset?: boolean; /** Create new cross-signing keys. Only applicable if `forceReset` is `true`. */ resetCrossSigning?: boolean; - /** The cached account password, if available. */ - accountPassword?: string; } /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index ee120c430a..1a6abbbadd 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -431,8 +431,6 @@ export default class MatrixChat extends React.PureComponent { // if cross-signing is not yet set up, do so now if possible. InitialCryptoSetupStore.sharedInstance().startInitialCryptoSetup( cli, - Boolean(this.tokenLogin), - this.stores, this.onCompleteSecurityE2eSetupFinished, ); this.setStateForNewView({ view: Views.E2E_SETUP }); @@ -504,8 +502,6 @@ export default class MatrixChat extends React.PureComponent { UIStore.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); - - this.stores.accountPasswordStore.clearPassword(); } private onWindowResized = (): void => { @@ -1935,8 +1931,8 @@ export default class MatrixChat extends React.PureComponent { this.showScreen("forgot_password"); }; - private onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string): Promise => { - return this.onUserCompletedLoginFlow(credentials, password); + private onRegisterFlowComplete = (credentials: IMatrixClientCreds): Promise => { + return this.onUserCompletedLoginFlow(credentials); }; // returns a promise which resolves to the new MatrixClient @@ -2003,9 +1999,7 @@ export default class MatrixChat extends React.PureComponent { * Note: SSO users (and any others using token login) currently do not pass through * this, as they instead jump straight into the app after `attemptTokenLogin`. */ - private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise => { - this.stores.accountPasswordStore.setPassword(password); - + private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds): Promise => { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 0a14450e63..d9c853bc3a 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -48,10 +48,7 @@ interface IProps { // Called when the user has logged in. Params: // - The object returned by the login API - // - The user's password, if applicable, (may be cached in memory for a - // short time so the user is not required to re-enter their password - // for operations like uploading cross-signing keys). - onLoggedIn(data: IMatrixClientCreds, password: string): void; + onLoggedIn(data: IMatrixClientCreds): void; // login shouldn't know or care how registration, password recovery, etc is done. onRegisterClick(): void; @@ -199,7 +196,7 @@ export default class LoginComponent extends React.PureComponent this.loginLogic.loginViaPassword(username, phoneCountry, phoneNumber, password).then( (data) => { this.setState({ serverIsAlive: true }); // it must be, we logged in. - this.props.onLoggedIn(data, password); + this.props.onLoggedIn(data); }, (error) => { if (this.unmounted) return; diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 0ae5c93346..91fe5c5faa 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -72,10 +72,7 @@ interface IProps { mobileRegister?: boolean; // Called when the user has logged in. Params: // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken - // - The user's password, if available and applicable (may be cached in memory - // for a short time so the user is not required to re-enter their password - // for operations like uploading cross-signing keys). - onLoggedIn(params: IMatrixClientCreds, password: string): Promise; + onLoggedIn(params: IMatrixClientCreds): Promise; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -431,16 +428,13 @@ export default class Registration extends React.Component { newState.busy = false; newState.completedNoSignin = true; } else { - await this.props.onLoggedIn( - { - userId, - deviceId: (response as RegisterResponse).device_id!, - homeserverUrl: this.state.matrixClient.getHomeserverUrl(), - identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken, - }, - this.state.formVals.password!, - ); + await this.props.onLoggedIn({ + userId, + deviceId: (response as RegisterResponse).device_id!, + homeserverUrl: this.state.matrixClient.getHomeserverUrl(), + identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), + accessToken, + }); this.setupPushers(); } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 4731c593bc..af8f91533e 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -38,6 +38,9 @@ enum BackupStatus { /** there is a backup on the server but we are not backing up to it */ SERVER_BACKUP_BUT_DISABLED, + /** Key backup is set up but recovery (4s) is not */ + BACKUP_NO_RECOVERY, + /** backup is not set up locally and there is no backup on the server */ NO_BACKUP, @@ -104,7 +107,11 @@ export default class LogoutDialog extends React.Component { } if ((await crypto.getActiveSessionBackupVersion()) !== null) { - this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); + if (await crypto.isSecretStorageReady()) { + this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE }); + } else { + this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY }); + } return; } @@ -164,13 +171,17 @@ export default class LogoutDialog extends React.Component { }; /** - * Show a dialog prompting the user to set up key backup. + * Show a dialog prompting the user to set up their recovery method. * - * Either there is no backup at all ({@link BackupStatus.NO_BACKUP}), there is a backup on the server but - * we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the - * backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup. + * Either: + * * There is no backup at all ({@link BackupStatus.NO_BACKUP}) + * * There is a backup set up but recovery (4s) is not ({@link BackupStatus.BACKUP_NO_RECOVERY}) + * * There is a backup on the server but we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}) + * * We were unable to pull the backup data ({@link BackupStatus.ERROR}). + * + * In all four cases, we should prompt the user to set up a method of recovery. */ - private renderSetupBackupDialog(): React.ReactNode { + private renderSetupRecoveryMethod(): React.ReactNode { const description = (

{_t("auth|logout_dialog|setup_secure_backup_description_1")}

@@ -254,7 +265,8 @@ export default class LogoutDialog extends React.Component { case BackupStatus.NO_BACKUP: case BackupStatus.SERVER_BACKUP_BUT_DISABLED: case BackupStatus.ERROR: - return this.renderSetupBackupDialog(); + case BackupStatus.BACKUP_NO_RECOVERY: + return this.renderSetupRecoveryMethod(); } } } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index ae99754cba..242feff6d4 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -128,7 +128,8 @@ export default class TextualBody extends React.Component { if (!this.props.editState) { const stoppedEditing = prevProps.editState && !this.props.editState; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; - if (messageWasEdited || stoppedEditing) { + const urlPreviewChanged = prevProps.showUrlPreview !== this.props.showUrlPreview; + if (messageWasEdited || stoppedEditing || urlPreviewChanged) { this.applyFormatting(); } } diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index d133587fc9..4c39a2db18 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -310,78 +310,78 @@ export default function RoomHeader({ - - {additionalButtons?.map((props) => { - const label = props.label(); - return ( - - { - event.stopPropagation(); - props.onClick(); - }} - > - {typeof props.icon === "function" ? props.icon() : props.icon} - - - ); - })} + {additionalButtons?.map((props) => { + const label = props.label(); - {isViewingCall && } - - {hasActiveCallSession && !isConnectedToCall && !isViewingCall ? ( - joinCallButton - ) : ( - <> - {!isVideoRoom && videoCallButton} - {!useElementCallExclusively && !isVideoRoom && voiceCallButton} - - )} - - - { - evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); - }} - aria-label={_t("right_panel|room_summary_card|title")} - > - - - - - {showChatButton && } - - - { - evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel); - PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); - }} - aria-label={_t("common|threads")} - > - - - - {notificationsEnabled && ( - + return ( + { - evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); + aria-label={label} + onClick={(event) => { + event.stopPropagation(); + props.onClick(); }} - aria-label={_t("notifications|enable_prompt_toast_title")} > - + {typeof props.icon === "function" ? props.icon() : props.icon} - )} - + ); + })} + + {isViewingCall && } + + {hasActiveCallSession && !isConnectedToCall && !isViewingCall ? ( + joinCallButton + ) : ( + <> + {!isVideoRoom && videoCallButton} + {!useElementCallExclusively && !isVideoRoom && voiceCallButton} + + )} + + {showChatButton && } + + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel); + PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); + }} + aria-label={_t("common|threads")} + > + + + + {notificationsEnabled && ( + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); + }} + aria-label={_t("notifications|enable_prompt_toast_title")} + > + + + + )} + + + { + evt.stopPropagation(); + RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); + }} + aria-label={_t("right_panel|room_summary_card|title")} + > + + + + {!isDirectMessage && ( { {this.state.enabling ? : _t("settings|security|message_search_failed")} - {EventIndexPeg.error && ( + {EventIndexPeg.error ? (
{_t("common|advanced")} @@ -230,7 +230,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {

- )} + ) : undefined} ); } diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts index fe73661554..d77cd1e804 100644 --- a/src/contexts/SDKContext.ts +++ b/src/contexts/SDKContext.ts @@ -13,7 +13,6 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import LegacyCallHandler from "../LegacyCallHandler"; import { PosthogAnalytics } from "../PosthogAnalytics"; import { SlidingSyncManager } from "../SlidingSyncManager"; -import { AccountPasswordStore } from "../stores/AccountPasswordStore"; import { MemberListStore } from "../stores/MemberListStore"; import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore"; import RightPanelStore from "../stores/right-panel/RightPanelStore"; @@ -63,7 +62,6 @@ export class SdkContextClass { protected _SpaceStore?: SpaceStoreClass; protected _LegacyCallHandler?: LegacyCallHandler; protected _TypingStore?: TypingStore; - protected _AccountPasswordStore?: AccountPasswordStore; protected _UserProfilesStore?: UserProfilesStore; protected _OidcClientStore?: OidcClientStore; @@ -149,13 +147,6 @@ export class SdkContextClass { return this._TypingStore; } - public get accountPasswordStore(): AccountPasswordStore { - if (!this._AccountPasswordStore) { - this._AccountPasswordStore = new AccountPasswordStore(); - } - return this._AccountPasswordStore; - } - public get userProfilesStore(): UserProfilesStore { if (!this.client) { throw new Error("Unable to create UserProfilesStore without a client"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e9ac73b48b..f3c514fca3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -914,6 +914,9 @@ "warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings." }, "reset_all_button": "Forgotten or lost all recovery methods? Reset all", + "set_up_recovery": "Set up recovery", + "set_up_recovery_later": "Not now", + "set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.", "set_up_toast_description": "Safeguard against losing access to encrypted messages & data", "set_up_toast_title": "Set up Secure Backup", "setup_secure_backup": { diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 66ac807080..ef1134e0c0 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -2115,7 +2115,8 @@ "show_less": "Pokaż mniej", "show_n_more": { "one": "Pokaż %(count)s więcej", - "other": "Pokaż %(count)s więcej" + "few": "Pokaż %(count)s więcej", + "many": "Pokaż %(count)s więcej" }, "show_previews": "Pokazuj podgląd wiadomości", "sort_by": "Sortuj według", @@ -3689,7 +3690,8 @@ "close": "Zamknij podgląd", "show_n_more": { "one": "Pokaż %(count)s inny podgląd", - "other": "Pokaż %(count)s innych podglądów" + "few": "Pokaż %(count)s inne podglądy", + "many": "Pokaż %(count)s innych podglądów" } } }, diff --git a/src/stores/AccountPasswordStore.ts b/src/stores/AccountPasswordStore.ts deleted file mode 100644 index 85bb7359e1..0000000000 --- a/src/stores/AccountPasswordStore.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -const PASSWORD_TIMEOUT = 5 * 60 * 1000; // five minutes - -/** - * Store for the account password. - * This password can be used for a short time after login - * to avoid requestin the password all the time for instance during e2ee setup. - */ -export class AccountPasswordStore { - private password?: string; - private passwordTimeoutId?: ReturnType; - - public setPassword(password: string): void { - this.password = password; - clearTimeout(this.passwordTimeoutId); - this.passwordTimeoutId = setTimeout(this.clearPassword, PASSWORD_TIMEOUT); - } - - public getPassword(): string | undefined { - return this.password; - } - - public clearPassword = (): void => { - clearTimeout(this.passwordTimeoutId); - this.passwordTimeoutId = undefined; - this.password = undefined; - }; -} diff --git a/src/stores/InitialCryptoSetupStore.ts b/src/stores/InitialCryptoSetupStore.ts index 0c2e49f5ca..46ae784db4 100644 --- a/src/stores/InitialCryptoSetupStore.ts +++ b/src/stores/InitialCryptoSetupStore.ts @@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import { useEffect, useState } from "react"; import { createCrossSigning } from "../CreateCrossSigning"; -import { SdkContextClass } from "../contexts/SDKContext"; type Status = "in_progress" | "complete" | "error" | undefined; @@ -45,8 +44,6 @@ export class InitialCryptoSetupStore extends EventEmitter { private status: Status = undefined; private client?: MatrixClient; - private isTokenLogin?: boolean; - private stores?: SdkContextClass; private onFinished?: (success: boolean) => void; public static sharedInstance(): InitialCryptoSetupStore { @@ -62,18 +59,9 @@ export class InitialCryptoSetupStore extends EventEmitter { * Start the initial crypto setup process. * * @param {MatrixClient} client The client to use for the setup - * @param {boolean} isTokenLogin True if the user logged in via a token login, otherwise false - * @param {SdkContextClass} stores The stores to use for the setup */ - public startInitialCryptoSetup( - client: MatrixClient, - isTokenLogin: boolean, - stores: SdkContextClass, - onFinished: (success: boolean) => void, - ): void { + public startInitialCryptoSetup(client: MatrixClient, onFinished: (success: boolean) => void): void { this.client = client; - this.isTokenLogin = isTokenLogin; - this.stores = stores; this.onFinished = onFinished; // We just start this process: it's progress is tracked by the events rather @@ -89,7 +77,7 @@ export class InitialCryptoSetupStore extends EventEmitter { * @returns {boolean} True if a retry was initiated, otherwise false */ public retry(): boolean { - if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) return false; + if (this.client === undefined) return false; this.doSetup().catch(() => logger.error("Initial crypto setup failed")); @@ -98,12 +86,10 @@ export class InitialCryptoSetupStore extends EventEmitter { private reset(): void { this.client = undefined; - this.isTokenLogin = undefined; - this.stores = undefined; } private async doSetup(): Promise { - if (this.client === undefined || this.isTokenLogin === undefined || this.stores == undefined) { + if (this.client === undefined) { throw new Error("No setup is in progress"); } @@ -114,7 +100,14 @@ export class InitialCryptoSetupStore extends EventEmitter { this.emit("update"); try { - await createCrossSigning(this.client, this.isTokenLogin, this.stores.accountPasswordStore.getPassword()); + // Create the user's cross-signing keys + await createCrossSigning(this.client); + + // Check for any existing backup and enable key backup if there isn't one + const currentKeyBackup = await cryptoApi.checkKeyBackupAndEnable(); + if (currentKeyBackup === null) { + await cryptoApi.resetKeyBackup(); + } this.reset(); @@ -122,16 +115,6 @@ export class InitialCryptoSetupStore extends EventEmitter { this.emit("update"); this.onFinished?.(true); } catch (e) { - if (this.isTokenLogin) { - // ignore any failures, we are relying on grace period here - this.reset(); - - this.status = "complete"; - this.emit("update"); - this.onFinished?.(true); - - return; - } logger.error("Error bootstrapping cross-signing", e); this.status = "error"; this.emit("update"); diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts index a13ba26f72..bfa28c3cd2 100644 --- a/src/stores/SetupEncryptionStore.ts +++ b/src/stores/SetupEncryptionStore.ts @@ -19,7 +19,6 @@ import { Device, SecretStorage } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { AccessCancelledError, accessSecretStorage } from "../SecurityManager"; -import { SdkContextClass } from "../contexts/SDKContext"; import { asyncSome } from "../utils/arrays"; import { initialiseDehydration } from "../utils/device/dehydration"; @@ -239,7 +238,6 @@ export class SetupEncryptionStore extends EventEmitter { { forceReset: true, resetCrossSigning: true, - accountPassword: SdkContextClass.instance.accountPasswordStore.getPassword(), }, ); } catch (e) { diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 0dd54bb18f..406b51cf16 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -23,15 +23,19 @@ const getTitle = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_title"); + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_title"); } }; -const getIcon = (kind: Kind): string => { +const getIcon = (kind: Kind): string | undefined => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return "secure_backup"; + case Kind.SET_UP_RECOVERY: + return undefined; case Kind.VERIFY_THIS_SESSION: return "verification_warning"; } @@ -41,22 +45,49 @@ const getSetupCaption = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("action|continue"); + case Kind.SET_UP_RECOVERY: + return _t("action|continue"); case Kind.VERIFY_THIS_SESSION: return _t("action|verify"); } }; +const getSecondaryButtonLabel = (kind: Kind): string => { + switch (kind) { + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery_later"); + case Kind.SET_UP_ENCRYPTION: + case Kind.VERIFY_THIS_SESSION: + return _t("encryption|verification|unverified_sessions_toast_reject"); + } +}; + const getDescription = (kind: Kind): string => { switch (kind) { case Kind.SET_UP_ENCRYPTION: return _t("encryption|set_up_toast_description"); + case Kind.SET_UP_RECOVERY: + return _t("encryption|set_up_recovery_toast_description"); case Kind.VERIFY_THIS_SESSION: return _t("encryption|verify_toast_description"); } }; +/** + * The kind of toast to show. + */ export enum Kind { + /** + * Prompt the user to set up encryption + */ SET_UP_ENCRYPTION = "set_up_encryption", + /** + * Prompt the user to set up a recovery key + */ + SET_UP_RECOVERY = "set_up_recovery", + /** + * Prompt the user to verify this session + */ VERIFY_THIS_SESSION = "verify_this_session", } @@ -64,6 +95,11 @@ const onReject = (): void => { DeviceListener.sharedInstance().dismissEncryptionSetup(); }; +/** + * Show a toast prompting the user for some action related to setting up their encryption. + * + * @param kind The kind of toast to show + */ export const showToast = (kind: Kind): void => { if ( ModuleRunner.instance.extensions.cryptoSetup.setupEncryptionNeeded({ @@ -101,15 +137,17 @@ export const showToast = (kind: Kind): void => { description: getDescription(kind), primaryLabel: getSetupCaption(kind), onPrimaryClick: onAccept, - secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"), + secondaryLabel: getSecondaryButtonLabel(kind), onSecondaryClick: onReject, - destructive: "secondary", }, component: GenericToast, priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40, }); }; +/** + * Hide the encryption setup toast if it is currently being shown. + */ export const hideToast = (): void => { ToastStore.sharedInstance().dismissToast(TOAST_KEY); }; diff --git a/test/CreateCrossSigning-test.ts b/test/CreateCrossSigning-test.ts index e1762bb504..85341b8bce 100644 --- a/test/CreateCrossSigning-test.ts +++ b/test/CreateCrossSigning-test.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { HTTPError, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { createCrossSigning } from "../src/CreateCrossSigning"; @@ -21,14 +21,14 @@ describe("CreateCrossSigning", () => { }); it("should call bootstrapCrossSigning with an authUploadDeviceSigningKeys function", async () => { - await createCrossSigning(client, false, "password"); + await createCrossSigning(client); expect(client.getCrypto()?.bootstrapCrossSigning).toHaveBeenCalledWith({ authUploadDeviceSigningKeys: expect.any(Function), }); }); - it("should upload with password auth if possible", async () => { + it("should upload", async () => { client.uploadDeviceSigningKeys = jest.fn().mockRejectedValueOnce( new MatrixError({ flows: [ @@ -39,24 +39,7 @@ describe("CreateCrossSigning", () => { }), ); - await createCrossSigning(client, false, "password"); - - const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; - - const makeRequest = jest.fn(); - await authUploadDeviceSigningKeys!(makeRequest); - expect(makeRequest).toHaveBeenCalledWith({ - type: "m.login.password", - identifier: { - type: "m.id.user", - user: client.getUserId(), - }, - password: "password", - }); - }); - - it("should attempt to upload keys without auth if using token login", async () => { - await createCrossSigning(client, true, undefined); + await createCrossSigning(client); const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; @@ -65,7 +48,7 @@ describe("CreateCrossSigning", () => { expect(makeRequest).toHaveBeenCalledWith({}); }); - it("should prompt user if password upload not possible", async () => { + it("should prompt user if upload failed with UIA", async () => { const createDialog = jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: Promise.resolve([true]), close: jest.fn(), @@ -81,13 +64,32 @@ describe("CreateCrossSigning", () => { }), ); - await createCrossSigning(client, false, "password"); + await createCrossSigning(client); const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; - const makeRequest = jest.fn(); + const makeRequest = jest.fn().mockRejectedValue( + new MatrixError({ + flows: [ + { + stages: ["dummy.mystery_flow_nobody_knows"], + }, + ], + }), + ); await authUploadDeviceSigningKeys!(makeRequest); expect(makeRequest).not.toHaveBeenCalledWith(); expect(createDialog).toHaveBeenCalled(); }); + + it("should throw error if server fails with something other than UIA", async () => { + await createCrossSigning(client); + + const { authUploadDeviceSigningKeys } = mocked(client.getCrypto()!).bootstrapCrossSigning.mock.calls[0][0]; + + const error = new HTTPError("Internal Server Error", 500); + const makeRequest = jest.fn().mockRejectedValue(error); + await expect(authUploadDeviceSigningKeys!(makeRequest)).rejects.toThrow(error); + expect(makeRequest).not.toHaveBeenCalledWith(); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index f9aee512a3..39852f049e 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -134,6 +134,7 @@ export function createTestClient(): MatrixClient { restoreKeyBackupWithPassphrase: jest.fn(), loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(), storeSessionBackupPrivateKey: jest.fn(), + checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null), getKeyBackupInfo: jest.fn().mockResolvedValue(null), getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null), }), diff --git a/test/unit-tests/DeviceListener-test.ts b/test/unit-tests/DeviceListener-test.ts index ad7f14e119..1c8fe1a1c7 100644 --- a/test/unit-tests/DeviceListener-test.ts +++ b/test/unit-tests/DeviceListener-test.ts @@ -352,13 +352,13 @@ describe("DeviceListener", () => { mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc"); }); - it("shows set up encryption toast when user has a key backup available", async () => { + it("shows set up recovery toast when user has a key backup available", async () => { // non falsy response mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo); await createAndStart(); expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.SET_UP_ENCRYPTION, + SetupEncryptionToast.Kind.SET_UP_RECOVERY, ); }); }); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index fd17ccf583..d5db4f190a 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1003,7 +1003,9 @@ describe("", () => { userHasCrossSigningKeys: jest.fn().mockResolvedValue(false), // This needs to not finish immediately because we need to test the screen appears bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise), + resetKeyBackup: jest.fn(), isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false), + checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null), }; loginClient.getCrypto.mockReturnValue(mockCrypto as any); }); diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 1e0ed2248b..6e9fb7fa36 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -45,113 +45,108 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
-
- -
+ + - + - + - + + + +
@@ -263,113 +258,108 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
-
- -
+ + - + - + - + + + +
@@ -566,113 +556,108 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
-
- -
+ + - + - + - + + + +
@@ -946,113 +931,108 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
-
- -
+ + - + - + - + + + +
@@ -1334,113 +1314,108 @@ exports[`RoomView should not display the timeline when the room encryption is lo
-
- -
+ + - + - + - + + + +
@@ -1545,113 +1520,108 @@ exports[`RoomView should not display the timeline when the room encryption is lo
-
- -
+ + - + - + - + + + +
@@ -1929,86 +1899,81 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
-
- -
+ + - + - + + + +
diff --git a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx index 0557e538d0..98f758ebbd 100644 --- a/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/LogoutDialog-test.tsx @@ -42,12 +42,20 @@ describe("LogoutDialog", () => { expect(rendered.container).toMatchSnapshot(); }); - it("shows a regular dialog if backups are working", async () => { + it("shows a regular dialog if backups and recovery are working", async () => { mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); + mockCrypto.isSecretStorageReady.mockResolvedValue(true); const rendered = renderComponent(); await rendered.findByText("Are you sure you want to sign out?"); }); + it("prompts user to set up recovery if backups are enabled but recovery isn't", async () => { + mockCrypto.getActiveSessionBackupVersion.mockResolvedValue("1"); + mockCrypto.isSecretStorageReady.mockResolvedValue(false); + const rendered = renderComponent(); + await rendered.findByText("You'll lose access to your encrypted messages"); + }); + it("Prompts user to connect backup if there is a backup on the server", async () => { mockCrypto.getKeyBackupInfo.mockResolvedValue({} as KeyBackupInfo); const rendered = renderComponent(); diff --git a/test/unit-tests/components/views/messages/TextualBody-test.tsx b/test/unit-tests/components/views/messages/TextualBody-test.tsx index c7ffc4ed93..14b9453575 100644 --- a/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -375,55 +375,73 @@ describe("", () => { }); }); - it("renders url previews correctly", () => { - languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); + describe("url preview", () => { + let matrixClient: MatrixClient; - const matrixClient = getMockClientWithEventEmitter({ - getRoom: () => mkStubRoom("room_id", "room name", undefined), - getAccountData: (): MatrixClient | undefined => undefined, - getUrlPreview: (url: string) => new Promise(() => {}), - isGuest: () => false, - mxcUrlToHttp: (s: string) => s, + beforeEach(() => { + languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); + matrixClient = getMockClientWithEventEmitter({ + getRoom: () => mkStubRoom("room_id", "room name", undefined), + getAccountData: (): MatrixClient | undefined => undefined, + getUrlPreview: (url: string) => new Promise(() => {}), + isGuest: () => false, + mxcUrlToHttp: (s: string) => s, + }); + DMRoomMap.makeShared(defaultMatrixClient); }); - DMRoomMap.makeShared(defaultMatrixClient); - const ev = mkRoomTextMessage("Visit https://matrix.org/"); - const { container, rerender } = getComponent( - { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, - matrixClient, - ); + it("renders url previews correctly", () => { + const ev = mkRoomTextMessage("Visit https://matrix.org/"); + const { container, rerender } = getComponent( + { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, + matrixClient, + ); - expect(container).toHaveTextContent(ev.getContent().body); - expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/"); + expect(container).toHaveTextContent(ev.getContent().body); + expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/"); - // simulate an event edit and check the transition from the old URL preview to the new one - const ev2 = mkEvent({ - type: "m.room.message", - room: "room_id", - user: "sender", - content: { - "m.new_content": { - body: "Visit https://vector.im/ and https://riot.im/", - msgtype: "m.text", + // simulate an event edit and check the transition from the old URL preview to the new one + const ev2 = mkEvent({ + type: "m.room.message", + room: "room_id", + user: "sender", + content: { + "m.new_content": { + body: "Visit https://vector.im/ and https://riot.im/", + msgtype: "m.text", + }, }, - }, - event: true, + event: true, + }); + jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3)); + ev.makeReplaced(ev2); + + getComponent( + { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() }, + matrixClient, + rerender, + ); + + expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)"); + + const links = ["https://vector.im/", "https://riot.im/"]; + const anchorNodes = container.querySelectorAll("a"); + Array.from(anchorNodes).forEach((node, index) => { + expect(node).toHaveAttribute("href", links[index]); + }); }); - jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3)); - ev.makeReplaced(ev2); - getComponent( - { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() }, - matrixClient, - rerender, - ); + it("should listen to showUrlPreview change", () => { + const ev = mkRoomTextMessage("Visit https://matrix.org/"); - expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)"); + const { container, rerender } = getComponent( + { mxEvent: ev, showUrlPreview: false, onHeightChanged: jest.fn() }, + matrixClient, + ); + expect(container.querySelector(".mx_LinkPreviewGroup")).toBeNull(); - const links = ["https://vector.im/", "https://riot.im/"]; - const anchorNodes = container.querySelectorAll("a"); - Array.from(anchorNodes).forEach((node, index) => { - expect(node).toHaveAttribute("href", links[index]); + getComponent({ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, matrixClient, rerender); + expect(container.querySelector(".mx_LinkPreviewGroup")).toBeTruthy(); }); }); }); diff --git a/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap index a6f412a3ac..3db3fb67fb 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/RoomHeader-test.tsx.snap @@ -42,111 +42,106 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
-
- -
+ + - + - + - + + + + `; diff --git a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx index 5c77e88d93..54c2aff979 100644 --- a/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx +++ b/test/unit-tests/components/views/settings/SetIntegrationManager-test.tsx @@ -11,7 +11,6 @@ import { fireEvent, render, screen, waitFor, within } from "jest-matrix-react"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import { UIFeature } from "../../../../../src/settings/UIFeature"; import { @@ -35,13 +34,9 @@ describe("SetIntegrationManager", () => { deleteThreePid: jest.fn(), }); - let stores!: SdkContextClass; - const getComponent = () => ( - - - + ); diff --git a/test/unit-tests/stores/AccountPasswordStore-test.ts b/test/unit-tests/stores/AccountPasswordStore-test.ts deleted file mode 100644 index 00fa8e05e6..0000000000 --- a/test/unit-tests/stores/AccountPasswordStore-test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore"; - -jest.useFakeTimers(); - -describe("AccountPasswordStore", () => { - let accountPasswordStore: AccountPasswordStore; - - beforeEach(() => { - accountPasswordStore = new AccountPasswordStore(); - }); - - it("should not have a password by default", () => { - expect(accountPasswordStore.getPassword()).toBeUndefined(); - }); - - describe("when setting a password", () => { - beforeEach(() => { - accountPasswordStore.setPassword("pass1"); - }); - - it("should return the password", () => { - expect(accountPasswordStore.getPassword()).toBe("pass1"); - }); - - describe("and the password timeout exceed", () => { - beforeEach(() => { - jest.advanceTimersToNextTimer(); - }); - - it("should clear the password", () => { - expect(accountPasswordStore.getPassword()).toBeUndefined(); - }); - }); - - describe("and setting another password", () => { - beforeEach(() => { - accountPasswordStore.setPassword("pass2"); - }); - - it("should return the other password", () => { - expect(accountPasswordStore.getPassword()).toBe("pass2"); - }); - }); - }); -}); diff --git a/test/unit-tests/stores/InitialCryptoSetupStore-test.ts b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts index 64b81bade2..8cfae4d699 100644 --- a/test/unit-tests/stores/InitialCryptoSetupStore-test.ts +++ b/test/unit-tests/stores/InitialCryptoSetupStore-test.ts @@ -8,12 +8,11 @@ Please see LICENSE files in the repository root for full details. import { mocked } from "jest-mock"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { waitFor } from "jest-matrix-react"; +import { sleep } from "matrix-js-sdk/src/utils"; import { createCrossSigning } from "../../../src/CreateCrossSigning"; import { InitialCryptoSetupStore } from "../../../src/stores/InitialCryptoSetupStore"; -import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { createTestClient } from "../../test-utils"; -import { AccountPasswordStore } from "../../../src/stores/AccountPasswordStore"; jest.mock("../../../src/CreateCrossSigning", () => ({ createCrossSigning: jest.fn(), @@ -22,7 +21,6 @@ jest.mock("../../../src/CreateCrossSigning", () => ({ describe("InitialCryptoSetupStore", () => { let testStore: InitialCryptoSetupStore; let client: MatrixClient; - let stores: SdkContextClass; let createCrossSigningResolve: () => void; let createCrossSigningReject: (e: Error) => void; @@ -30,11 +28,6 @@ describe("InitialCryptoSetupStore", () => { beforeEach(() => { testStore = new InitialCryptoSetupStore(); client = createTestClient(); - stores = { - accountPasswordStore: { - getPassword: jest.fn(), - } as unknown as AccountPasswordStore, - } as unknown as SdkContextClass; mocked(createCrossSigning).mockImplementation(() => { return new Promise((resolve, reject) => { @@ -45,7 +38,7 @@ describe("InitialCryptoSetupStore", () => { }); it("should call createCrossSigning when startInitialCryptoSetup is called", async () => { - testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + testStore.startInitialCryptoSetup(client, jest.fn()); await waitFor(() => expect(createCrossSigning).toHaveBeenCalled()); }); @@ -54,7 +47,7 @@ describe("InitialCryptoSetupStore", () => { const updateSpy = jest.fn(); testStore.on("update", updateSpy); - testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + testStore.startInitialCryptoSetup(client, jest.fn()); createCrossSigningResolve(); await waitFor(() => expect(updateSpy).toHaveBeenCalled()); @@ -65,21 +58,28 @@ describe("InitialCryptoSetupStore", () => { const updateSpy = jest.fn(); testStore.on("update", updateSpy); - testStore.startInitialCryptoSetup(client, false, stores, jest.fn()); + testStore.startInitialCryptoSetup(client, jest.fn()); createCrossSigningReject(new Error("Test error")); await waitFor(() => expect(updateSpy).toHaveBeenCalled()); expect(testStore.getStatus()).toBe("error"); }); - it("should ignore failures if tokenLogin is true", async () => { - const updateSpy = jest.fn(); - testStore.on("update", updateSpy); + it("should fail to retry once complete", async () => { + testStore.startInitialCryptoSetup(client, jest.fn()); - testStore.startInitialCryptoSetup(client, true, stores, jest.fn()); + await waitFor(() => expect(createCrossSigning).toHaveBeenCalled()); + createCrossSigningResolve(); + await sleep(0); // await the next tick + expect(testStore.retry()).toBeFalsy(); + }); + + it("should retry if initial attempt failed", async () => { + testStore.startInitialCryptoSetup(client, jest.fn()); + + await waitFor(() => expect(createCrossSigning).toHaveBeenCalled()); createCrossSigningReject(new Error("Test error")); - - await waitFor(() => expect(updateSpy).toHaveBeenCalled()); - expect(testStore.getStatus()).toBe("complete"); + await sleep(0); // await the next tick + expect(testStore.retry()).toBeTruthy(); }); }); diff --git a/test/unit-tests/stores/SetupEncryptionStore-test.ts b/test/unit-tests/stores/SetupEncryptionStore-test.ts index d3d0300a21..b0bc3a73d8 100644 --- a/test/unit-tests/stores/SetupEncryptionStore-test.ts +++ b/test/unit-tests/stores/SetupEncryptionStore-test.ts @@ -11,7 +11,6 @@ import { MatrixClient, Device } from "matrix-js-sdk/src/matrix"; import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorage } from "matrix-js-sdk/src/secret-storage"; import { BootstrapCrossSigningOpts, CryptoApi, DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api"; -import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { accessSecretStorage } from "../../../src/SecurityManager"; import { SetupEncryptionStore } from "../../../src/stores/SetupEncryptionStore"; import { emitPromise, stubClient } from "../../test-utils"; @@ -21,7 +20,6 @@ jest.mock("../../../src/SecurityManager", () => ({ })); describe("SetupEncryptionStore", () => { - const cachedPassword = "p4assword"; let client: Mocked; let mockCrypto: Mocked; let mockSecretStorage: Mocked; @@ -47,11 +45,6 @@ describe("SetupEncryptionStore", () => { Object.defineProperty(client, "secretStorage", { value: mockSecretStorage }); setupEncryptionStore = new SetupEncryptionStore(); - SdkContextClass.instance.accountPasswordStore.setPassword(cachedPassword); - }); - - afterEach(() => { - SdkContextClass.instance.accountPasswordStore.clearPassword(); }); describe("start", () => { @@ -172,7 +165,6 @@ describe("SetupEncryptionStore", () => { await setupEncryptionStore.resetConfirm(); expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), { - accountPassword: cachedPassword, forceReset: true, resetCrossSigning: true, }); diff --git a/test/unit-tests/toasts/SetupEncryptionToast-test.tsx b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx new file mode 100644 index 0000000000..5ce3fab9ae --- /dev/null +++ b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx @@ -0,0 +1,24 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { render, screen } from "jest-matrix-react"; + +import ToastContainer from "../../../src/components/structures/ToastContainer"; +import { Kind, showToast } from "../../../src/toasts/SetupEncryptionToast"; + +describe("SetupEncryptionToast", () => { + beforeEach(() => { + render(); + }); + + it("should render the se up recovery toast", async () => { + showToast(Kind.SET_UP_RECOVERY); + + await expect(screen.findByText("Set up recovery")).resolves.toBeInTheDocument(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 82c2d0491f..35e7ac04b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2019,10 +2019,10 @@ emojibase "^15.3.1" emojibase-data "^15.3.1" -"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5" - integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og== +"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" + integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -8308,11 +8308,11 @@ matrix-events-sdk@0.0.1: integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "34.13.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c4ea57d42dcf8bd04c40feaa2c686487dbcab338" + version "35.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/349a86c119e6ac53d8d0677f6b6db5944c3ddcd1" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" @@ -8595,9 +8595,9 @@ murmurhash-js@^1.0.0: integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" @@ -11539,10 +11539,10 @@ typed-array-length@^1.0.6: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript@5.6.3: - version "5.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== +typescript@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== ua-parser-js@^1.0.2: version "1.0.39"