Add Recovery section in the new user settings Encryption tab (#28673)
* Refine `SettingsSection` & `SettingsTab` * Add encryption tab * Add recovery section * Add device verification * Rename `Panel` into `State` * Update & add tests to user settings common * Add tests to `RecoveryPanel` * Add tests to `ChangeRecoveryKey` * Update CreateSecretStorageDialog-test snapshot * Add tests to `EncryptionUserSettingsTab` * Update existing screenshots of e2e tests * Add new encryption tab ownership to `@element-hq/element-crypto-web-reviewers` * Add e2e tests * Fix monospace font and add figma link to hardcoded value * Add unit to Icon * Improve e2e doc * Assert that the crypto module is defined * Add classname doc * Fix typo * Use `good` state instead of default * Rename `ChangeRecoveryKey.isSetupFlow` into `ChangeRecoveryKey.userHasKeyBackup` * Move `deleteCachedSecrets` fixture in `recovery.spec.ts` * Use one callback instead of two in `RecoveryPanel` * Fix docs and naming of `utils.createBot` * Fix typo in `RecoveryPanel` * Add more doc to the state of the `EncryptionUserSettingsTab` * Rename `verification_required` into `set_up_encryption` * Update test * ADd new license * Update comments and doc * Assert that `recoveryKey.encodedPrivateKey` is always defined * Add comments to explain how the secrets could be uncached * Use `matrixClient.secretStorage.getDefaultKeyId` instead of `matrixClient.getCrypto().checkKeyBackupAndEnable` to know if we need to set up a recovery key * Update existing screenshot to add encryption tab. * Update tests * Use new labels when changing the recovery key * Fix docs * Don't reset key backup when creating a recovery key * Fix doc
@@ -3,13 +3,17 @@
|
|||||||
/package.json @element-hq/element-web-team
|
/package.json @element-hq/element-web-team
|
||||||
/yarn.lock @element-hq/element-web-team
|
/yarn.lock @element-hq/element-web-team
|
||||||
|
|
||||||
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
||||||
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
||||||
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||||
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||||
|
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||||
|
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||||
|
|
||||||
# Ignore translations as those will be updated by GHA for Localazy download
|
# Ignore translations as those will be updated by GHA for Localazy download
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
awaitVerifier,
|
awaitVerifier,
|
||||||
checkDeviceIsConnectedKeyBackup,
|
checkDeviceIsConnectedKeyBackup,
|
||||||
checkDeviceIsCrossSigned,
|
checkDeviceIsCrossSigned,
|
||||||
|
createBot,
|
||||||
doTwoWaySasVerification,
|
doTwoWaySasVerification,
|
||||||
logIntoElement,
|
logIntoElement,
|
||||||
waitForVerificationRequest,
|
waitForVerificationRequest,
|
||||||
@@ -28,29 +29,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
let expectedBackupVersion: string;
|
let expectedBackupVersion: string;
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
// Visit the login page of the app, to load the matrix sdk
|
const res = await createBot(page, homeserver, credentials);
|
||||||
await page.goto("/#/login");
|
aliceBotClient = res.botClient;
|
||||||
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
// wait for the page to load
|
|
||||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
|
||||||
|
|
||||||
// Create a new device for alice
|
|
||||||
aliceBotClient = new Bot(page, homeserver, {
|
|
||||||
bootstrapCrossSigning: true,
|
|
||||||
bootstrapSecretStorage: true,
|
|
||||||
});
|
|
||||||
aliceBotClient.setCredentials(credentials);
|
|
||||||
|
|
||||||
// Backup is prepared in the background. Poll until it is ready.
|
|
||||||
const botClientHandle = await aliceBotClient.prepareClient();
|
|
||||||
await expect
|
|
||||||
.poll(async () => {
|
|
||||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
|
||||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
|
||||||
);
|
|
||||||
return expectedBackupVersion;
|
|
||||||
})
|
|
||||||
.not.toBe(null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
|||||||
import type {
|
import type {
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
EmojiMapping,
|
EmojiMapping,
|
||||||
|
GeneratedSecretStorageKey,
|
||||||
ShowSasCallbacks,
|
ShowSasCallbacks,
|
||||||
VerificationRequest,
|
VerificationRequest,
|
||||||
Verifier,
|
Verifier,
|
||||||
@@ -22,6 +23,46 @@ import { Client } from "../../pages/client";
|
|||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bot client using the supplied credentials, and wait for the key backup to be ready.
|
||||||
|
* @param page - the playwright `page` fixture
|
||||||
|
* @param homeserver - the homeserver to use
|
||||||
|
* @param credentials - the credentials to use for the bot client
|
||||||
|
*/
|
||||||
|
export async function createBot(
|
||||||
|
page: Page,
|
||||||
|
homeserver: HomeserverInstance,
|
||||||
|
credentials: Credentials,
|
||||||
|
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
||||||
|
// Visit the login page of the app, to load the matrix sdk
|
||||||
|
await page.goto("/#/login");
|
||||||
|
|
||||||
|
// wait for the page to load
|
||||||
|
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||||
|
|
||||||
|
// Create a new bot client
|
||||||
|
const botClient = new Bot(page, homeserver, {
|
||||||
|
bootstrapCrossSigning: true,
|
||||||
|
bootstrapSecretStorage: true,
|
||||||
|
});
|
||||||
|
botClient.setCredentials(credentials);
|
||||||
|
// Backup is prepared in the background. Poll until it is ready.
|
||||||
|
const botClientHandle = await botClient.prepareClient();
|
||||||
|
let expectedBackupVersion: string;
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||||
|
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||||
|
);
|
||||||
|
return expectedBackupVersion;
|
||||||
|
})
|
||||||
|
.not.toBe(null);
|
||||||
|
|
||||||
|
const recoveryKey = await botClient.getRecoveryKey();
|
||||||
|
|
||||||
|
return { botClient, recoveryKey, expectedBackupVersion };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* wait for the given client to receive an incoming verification request, and automatically accept it
|
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Page } from "@playwright/test";
|
||||||
|
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
|
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||||
|
import { test as base, expect } from "../../../element-web-test";
|
||||||
|
export { expect };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up for the encryption tab test
|
||||||
|
*/
|
||||||
|
export const test = base.extend<{
|
||||||
|
util: Helpers;
|
||||||
|
}>({
|
||||||
|
util: async ({ page, app, bot }, use) => {
|
||||||
|
await use(new Helpers(page, app));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class Helpers {
|
||||||
|
constructor(
|
||||||
|
private page: Page,
|
||||||
|
private app: ElementAppPage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the encryption tab
|
||||||
|
*/
|
||||||
|
openEncryptionTab() {
|
||||||
|
return this.app.settings.openUserSettings("Encryption");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through the device verification flow using the recovery key.
|
||||||
|
*/
|
||||||
|
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
|
||||||
|
// Select the security phrase
|
||||||
|
await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||||
|
await this.enterRecoveryKey(recoveryKey);
|
||||||
|
await this.page.getByRole("button", { name: "Done" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the recovery key in the dialog
|
||||||
|
* @param recoveryKey
|
||||||
|
*/
|
||||||
|
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
|
||||||
|
// Select to use recovery key
|
||||||
|
await this.page.getByRole("button", { name: "use your Security Key" }).click();
|
||||||
|
|
||||||
|
// Fill the recovery key
|
||||||
|
const dialog = this.page.locator(".mx_Dialog");
|
||||||
|
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the encryption tab content
|
||||||
|
*/
|
||||||
|
getEncryptionTabContent() {
|
||||||
|
return this.page.getByTestId("encryptionTab");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the default key id of the secret storage to `null`
|
||||||
|
*/
|
||||||
|
async removeSecretStorageDefaultKeyId() {
|
||||||
|
const client = await this.app.client.prepareClient();
|
||||||
|
await client.evaluate(async (client) => {
|
||||||
|
await client.secretStorage.setDefaultKeyId(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the security key from the clipboard and fill in the input field
|
||||||
|
* Then click on the finish button
|
||||||
|
* @param title - The title of the dialog
|
||||||
|
* @param confirmButtonLabel - The label of the confirm button
|
||||||
|
* @param screenshot
|
||||||
|
*/
|
||||||
|
async confirmRecoveryKey(title: string, confirmButtonLabel: string, screenshot: `${string}.png`) {
|
||||||
|
const dialog = this.getEncryptionTabContent();
|
||||||
|
await expect(dialog.getByText(title, { exact: true })).toBeVisible();
|
||||||
|
await expect(dialog).toMatchScreenshot(screenshot);
|
||||||
|
|
||||||
|
const handle = await this.page.evaluateHandle(() => navigator.clipboard.readText());
|
||||||
|
const clipboardContent = await handle.jsonValue();
|
||||||
|
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||||
|
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
|
||||||
|
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test, expect } from ".";
|
||||||
|
import {
|
||||||
|
checkDeviceIsConnectedKeyBackup,
|
||||||
|
checkDeviceIsCrossSigned,
|
||||||
|
createBot,
|
||||||
|
verifySession,
|
||||||
|
} from "../../crypto/utils";
|
||||||
|
|
||||||
|
test.describe("Recovery section in Encryption tab", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
let recoveryKey: GeneratedSecretStorageKey;
|
||||||
|
let expectedBackupVersion: string;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
|
const res = await createBot(page, homeserver, credentials);
|
||||||
|
recoveryKey = res.recoveryKey;
|
||||||
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||||
|
const dialog = await util.openEncryptionTab();
|
||||||
|
|
||||||
|
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||||
|
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||||
|
await expect(verifyButton).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||||
|
await verifyButton.click();
|
||||||
|
|
||||||
|
await util.verifyDevice(recoveryKey);
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||||
|
|
||||||
|
// Check that our device is now cross-signed
|
||||||
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
|
// Check that the current device is connected to key backup
|
||||||
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
|
await app.closeDialog();
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should change the recovery key",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, homeserver, credentials, util, context }) => {
|
||||||
|
await verifySession(app, "new passphrase");
|
||||||
|
const dialog = await util.openEncryptionTab();
|
||||||
|
|
||||||
|
// The user can only change the recovery key
|
||||||
|
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
|
||||||
|
await expect(changeButton).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||||
|
await changeButton.click();
|
||||||
|
|
||||||
|
// Display the new recovery key and click on the copy button
|
||||||
|
await expect(dialog.getByText("Change recovery key?")).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("change-key-1-encryption-tab.png", {
|
||||||
|
mask: [dialog.getByTestId("recoveryKey")],
|
||||||
|
});
|
||||||
|
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// Confirm the recovery key
|
||||||
|
await util.confirmRecoveryKey(
|
||||||
|
"Enter your new recovery key",
|
||||||
|
"Confirm new recovery key",
|
||||||
|
"change-key-2-encryption-tab.png",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("should setup the recovery key", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||||
|
await verifySession(app, "new passphrase");
|
||||||
|
await util.removeSecretStorageDefaultKeyId();
|
||||||
|
|
||||||
|
// The key backup is deleted and the user needs to set it up
|
||||||
|
const dialog = await util.openEncryptionTab();
|
||||||
|
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
|
||||||
|
await expect(setupButton).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
|
||||||
|
await setupButton.click();
|
||||||
|
|
||||||
|
// Display an informative panel about the recovery key
|
||||||
|
await expect(dialog.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-1-encryption-tab.png");
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// Display the new recovery key and click on the copy button
|
||||||
|
await expect(dialog.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||||
|
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-2-encryption-tab.png", {
|
||||||
|
mask: [dialog.getByTestId("recoveryKey")],
|
||||||
|
});
|
||||||
|
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||||
|
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// Confirm the recovery key
|
||||||
|
await util.confirmRecoveryKey(
|
||||||
|
"Enter your recovery key to confirm",
|
||||||
|
"Finish set up",
|
||||||
|
"set-up-key-3-encryption-tab.png",
|
||||||
|
);
|
||||||
|
|
||||||
|
// The recovery key is now set up and the user can change it
|
||||||
|
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||||
|
|
||||||
|
await app.closeDialog();
|
||||||
|
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, "1", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||||
|
//
|
||||||
|
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||||
|
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||||
|
test(
|
||||||
|
"should enter the recovery key when the secrets are not cached",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, util }) => {
|
||||||
|
await verifySession(app, "new passphrase");
|
||||||
|
// We need to delete the cached secrets
|
||||||
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
|
await util.openEncryptionTab();
|
||||||
|
// We ask the user to enter the recovery key
|
||||||
|
const dialog = util.getEncryptionTabContent();
|
||||||
|
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||||
|
await expect(enterKeyButton).toBeVisible();
|
||||||
|
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||||
|
await enterKeyButton.click();
|
||||||
|
|
||||||
|
// Fill the recovery key
|
||||||
|
await util.enterRecoveryKey(recoveryKey);
|
||||||
|
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||||
|
|
||||||
|
// Check that our device is now cross-signed
|
||||||
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
|
// Check that the current device is connected to key backup
|
||||||
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
|
await app.closeDialog();
|
||||||
|
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the cached secrets from the indexedDB
|
||||||
|
* This is a workaround to simulate the case where the secrets are not cached.
|
||||||
|
*/
|
||||||
|
async function deleteCachedSecrets(page: Page) {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const removeCachedSecrets = new Promise((resolve) => {
|
||||||
|
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
|
||||||
|
request.onsuccess = async (event: Event & { target: { result: IDBDatabase } }) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
|
||||||
|
request.onsuccess = () => {
|
||||||
|
db.close();
|
||||||
|
resolve(undefined);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await removeCachedSecrets;
|
||||||
|
});
|
||||||
|
await page.reload();
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
@@ -596,7 +596,9 @@ legend {
|
|||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(
|
||||||
|
.mx_EncryptionUserSettingsTab button
|
||||||
|
),
|
||||||
.mx_Dialog input[type="submit"],
|
.mx_Dialog input[type="submit"],
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||||
.mx_Dialog_buttons input[type="submit"] {
|
.mx_Dialog_buttons input[type="submit"] {
|
||||||
@@ -616,8 +618,8 @@ legend {
|
|||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(
|
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(
|
||||||
.mx_ShareDialog button
|
.mx_EncryptionUserSettingsTab button
|
||||||
):last-child {
|
):last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
@@ -625,7 +627,9 @@ legend {
|
|||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus,
|
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(
|
||||||
|
.mx_EncryptionUserSettingsTab button
|
||||||
|
):focus,
|
||||||
.mx_Dialog input[type="submit"]:focus,
|
.mx_Dialog input[type="submit"]:focus,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
||||||
.mx_Dialog_buttons input[type="submit"]:focus {
|
.mx_Dialog_buttons input[type="submit"]:focus {
|
||||||
@@ -637,7 +641,9 @@ legend {
|
|||||||
.mx_Dialog_buttons
|
.mx_Dialog_buttons
|
||||||
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
|
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(
|
||||||
|
.mx_EncryptionUserSettingsTab button
|
||||||
|
),
|
||||||
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||||
color: var(--cpd-color-text-on-solid-primary);
|
color: var(--cpd-color-text-on-solid-primary);
|
||||||
background-color: var(--cpd-color-bg-action-primary-rest);
|
background-color: var(--cpd-color-bg-action-primary-rest);
|
||||||
@@ -650,7 +656,7 @@ legend {
|
|||||||
.mx_Dialog_buttons
|
.mx_Dialog_buttons
|
||||||
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
|
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
|
||||||
.mx_ThemeChoicePanel_CustomTheme button
|
.mx_ThemeChoicePanel_CustomTheme button
|
||||||
):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(.mx_EncryptionUserSettingsTab button),
|
||||||
.mx_Dialog_buttons input[type="submit"].danger {
|
.mx_Dialog_buttons input[type="submit"].danger {
|
||||||
background-color: var(--cpd-color-bg-critical-primary);
|
background-color: var(--cpd-color-bg-critical-primary);
|
||||||
border: solid 1px var(--cpd-color-bg-critical-primary);
|
border: solid 1px var(--cpd-color-bg-critical-primary);
|
||||||
@@ -666,7 +672,9 @@ legend {
|
|||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled,
|
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(
|
||||||
|
.mx_EncryptionUserSettingsTab button
|
||||||
|
):disabled,
|
||||||
.mx_Dialog input[type="submit"]:disabled,
|
.mx_Dialog input[type="submit"]:disabled,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
||||||
.mx_Dialog_buttons input[type="submit"]:disabled {
|
.mx_Dialog_buttons input[type="submit"]:disabled {
|
||||||
|
|||||||
@@ -347,10 +347,14 @@
|
|||||||
@import "./views/settings/_SetIdServer.pcss";
|
@import "./views/settings/_SetIdServer.pcss";
|
||||||
@import "./views/settings/_SetIntegrationManager.pcss";
|
@import "./views/settings/_SetIntegrationManager.pcss";
|
||||||
@import "./views/settings/_SettingsFieldset.pcss";
|
@import "./views/settings/_SettingsFieldset.pcss";
|
||||||
|
@import "./views/settings/_SettingsHeader.pcss";
|
||||||
|
@import "./views/settings/_SettingsSubheader.pcss";
|
||||||
@import "./views/settings/_SpellCheckLanguages.pcss";
|
@import "./views/settings/_SpellCheckLanguages.pcss";
|
||||||
@import "./views/settings/_ThemeChoicePanel.pcss";
|
@import "./views/settings/_ThemeChoicePanel.pcss";
|
||||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||||
@import "./views/settings/_UserProfileSettings.pcss";
|
@import "./views/settings/_UserProfileSettings.pcss";
|
||||||
|
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
||||||
|
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_SettingsHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
/* Override margin from common.pcss */
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font: var(--cpd-font-body-sm-medium);
|
||||||
|
color: var(--cpd-color-text-action-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_SettingsSubheader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
font: var(--cpd-font-body-sm-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsSubheader_success {
|
||||||
|
color: var(--cpd-color-text-success-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsSubheader_error {
|
||||||
|
color: var(--cpd-color-text-critical-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ChangeRecoveryKey {
|
||||||
|
.mx_InformationPanel_description {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ChangeRecoveryKey_Form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-8x);
|
||||||
|
|
||||||
|
.mx_ChangeRecoveryKey_footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_KeyPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"header button" auto
|
||||||
|
"content button" auto / 1fr;
|
||||||
|
|
||||||
|
column-gap: var(--cpd-space-3x);
|
||||||
|
row-gap: var(--cpd-space-1x);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
grid-area: header;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
grid-area: content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
|
.mx_KeyPanel_key {
|
||||||
|
font-family: Inconsolata, monospace;
|
||||||
|
/*
|
||||||
|
* From figma https://www.figma.com/design/qTWRfItpO3RdCjnTKPu4mL/Settings?node-id=375-77471&t=t7lozYrSI1AVZZ3U-4
|
||||||
|
*/
|
||||||
|
height: 70px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: var(--cpd-space-2x);
|
||||||
|
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||||
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
margin: 0 var(--cpd-space-1x);
|
||||||
|
grid-area: button;
|
||||||
|
color: var(--cpd-color-icon-secondary-alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_KeyForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-8x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ChangeRecoveryKey_footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_EncryptionCard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-8x);
|
||||||
|
padding: var(--cpd-space-10x);
|
||||||
|
border-radius: var(--cpd-space-4x);
|
||||||
|
/* From figma */
|
||||||
|
box-shadow: 0 1.2px 2.4px 0 rgba(27, 29, 34, 0.15);
|
||||||
|
border: 1px solid var(--cpd-color-gray-400);
|
||||||
|
|
||||||
|
.mx_EncryptionCard_header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,20 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
a {
|
a {
|
||||||
color: $links;
|
color: $links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_SettingsSection_newUi {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-6x);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsSection_header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-3x);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsSection_subSections {
|
.mx_SettingsSection_subSections {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
color: $links;
|
color: $links;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form:not(.mx_EncryptionUserSettingsTab form) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $spacing-8;
|
gap: $spacing-8;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import VisibilityOnIcon from "@vector-im/compound-design-tokens/assets/web/icons
|
|||||||
import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications";
|
import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications";
|
||||||
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
|
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
|
||||||
import KeyboardIcon from "@vector-im/compound-design-tokens/assets/web/icons/keyboard";
|
import KeyboardIcon from "@vector-im/compound-design-tokens/assets/web/icons/keyboard";
|
||||||
|
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||||
import SidebarIcon from "@vector-im/compound-design-tokens/assets/web/icons/sidebar";
|
import SidebarIcon from "@vector-im/compound-design-tokens/assets/web/icons/sidebar";
|
||||||
import MicOnIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on";
|
import MicOnIcon from "@vector-im/compound-design-tokens/assets/web/icons/mic-on";
|
||||||
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock";
|
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock";
|
||||||
@@ -44,6 +45,7 @@ import { NonEmptyArray } from "../../../@types/common";
|
|||||||
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
import { useSettingValue } from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
|
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
|
||||||
|
import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserSettingsTab";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
initialTabId?: UserTab;
|
initialTabId?: UserTab;
|
||||||
@@ -75,6 +77,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
|
|||||||
return _t("settings|voip|dialog_title", undefined, subs);
|
return _t("settings|voip|dialog_title", undefined, subs);
|
||||||
case UserTab.Security:
|
case UserTab.Security:
|
||||||
return _t("settings|security|dialog_title", undefined, subs);
|
return _t("settings|security|dialog_title", undefined, subs);
|
||||||
|
case UserTab.Encryption:
|
||||||
|
return _t("settings|encryption|dialog_title", undefined, subs);
|
||||||
case UserTab.Labs:
|
case UserTab.Labs:
|
||||||
return _t("settings|labs|dialog_title", undefined, subs);
|
return _t("settings|labs|dialog_title", undefined, subs);
|
||||||
case UserTab.Mjolnir:
|
case UserTab.Mjolnir:
|
||||||
@@ -179,6 +183,10 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tabs.push(
|
||||||
|
new Tab(UserTab.Encryption, _td("settings|encryption|title"), <KeyIcon />, <EncryptionUserSettingsTab />),
|
||||||
|
);
|
||||||
|
|
||||||
if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) {
|
if (showLabsFlags() || SettingsStore.getFeatureSettingNames().some((k) => SettingsStore.getBetaInfo(k))) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
new Tab(UserTab.Labs, _td("common|labs"), <LabsIcon />, <LabsUserSettingsTab />, "UserSettingsLabs"),
|
new Tab(UserTab.Labs, _td("common|labs"), <LabsIcon />, <LabsUserSettingsTab />, "UserSettingsLabs"),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export enum UserTab {
|
|||||||
Sidebar = "USER_SIDEBAR_TAB",
|
Sidebar = "USER_SIDEBAR_TAB",
|
||||||
Voice = "USER_VOICE_TAB",
|
Voice = "USER_VOICE_TAB",
|
||||||
Security = "USER_SECURITY_TAB",
|
Security = "USER_SECURITY_TAB",
|
||||||
|
Encryption = "USER_ENCRYPTION_TAB",
|
||||||
Labs = "USER_LABS_TAB",
|
Labs = "USER_LABS_TAB",
|
||||||
Mjolnir = "USER_MJOLNIR_TAB",
|
Mjolnir = "USER_MJOLNIR_TAB",
|
||||||
Help = "USER_HELP_TAB",
|
Help = "USER_HELP_TAB",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { JSX } from "react";
|
||||||
|
import { Heading } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The heading for a settings section.
|
||||||
|
*/
|
||||||
|
interface SettingsHeaderProps {
|
||||||
|
/**
|
||||||
|
* Whether the user has a recommended tag.
|
||||||
|
*/
|
||||||
|
hasRecommendedTag?: boolean;
|
||||||
|
/**
|
||||||
|
* The label for the header.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsHeader({ hasRecommendedTag = false, label }: SettingsHeaderProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Heading className="mx_SettingsHeader" as="h2" size="sm" weight="semibold">
|
||||||
|
{label} {hasRecommendedTag && <span>{_t("common|recommended")}</span>}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { JSX } from "react";
|
||||||
|
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
|
||||||
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface SettingsSubheaderProps {
|
||||||
|
/**
|
||||||
|
* The subheader text.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
/**
|
||||||
|
* The state of the subheader.
|
||||||
|
*/
|
||||||
|
state: "success" | "error";
|
||||||
|
/**
|
||||||
|
* The message to display next to the state icon.
|
||||||
|
*/
|
||||||
|
stateMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styled subheader for settings.
|
||||||
|
*/
|
||||||
|
export function SettingsSubheader({ label, state, stateMessage }: SettingsSubheaderProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsSubheader">
|
||||||
|
{label}
|
||||||
|
<span
|
||||||
|
className={classNames({
|
||||||
|
mx_SettingsSubheader_success: state === "success",
|
||||||
|
mx_SettingsSubheader_error: state === "error",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{state === "success" ? (
|
||||||
|
<CheckCircleIcon width="20px" height="20px" />
|
||||||
|
) : (
|
||||||
|
<ErrorIcon width="20px" height="20px" />
|
||||||
|
)}
|
||||||
|
{stateMessage}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* 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, { FormEventHandler, JSX, MouseEventHandler, useState } from "react";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
ErrorMessage,
|
||||||
|
Field,
|
||||||
|
IconButton,
|
||||||
|
Label,
|
||||||
|
Root,
|
||||||
|
Text,
|
||||||
|
TextControl,
|
||||||
|
} from "@vector-im/compound-web";
|
||||||
|
import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { EncryptionCard } from "./EncryptionCard";
|
||||||
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
|
import { copyPlaintext } from "../../../../utils/strings";
|
||||||
|
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible states of the component.
|
||||||
|
* - `inform_user`: The user is informed about the recovery key.
|
||||||
|
* - `save_key_setup_flow`: The user is asked to save the new recovery key during the setup flow.
|
||||||
|
* - `save_key_change_flow`: The user is asked to save the new recovery key during the change key flow.
|
||||||
|
* - `confirm_key_setup_flow`: The user is asked to confirm the new recovery key during the set up flow.
|
||||||
|
* - `confirm_key_change_flow`: The user is asked to confirm the new recovery key during the change key flow.
|
||||||
|
*/
|
||||||
|
type State =
|
||||||
|
| "inform_user"
|
||||||
|
| "save_key_setup_flow"
|
||||||
|
| "save_key_change_flow"
|
||||||
|
| "confirm_key_setup_flow"
|
||||||
|
| "confirm_key_change_flow";
|
||||||
|
|
||||||
|
interface ChangeRecoveryKeyProps {
|
||||||
|
/**
|
||||||
|
* If true, the component will display the flow to change the recovery key.
|
||||||
|
* If false,the component will display the flow to set up a new recovery key.
|
||||||
|
*/
|
||||||
|
userHasRecoveryKey: boolean;
|
||||||
|
/**
|
||||||
|
* Called when the recovery key is successfully changed.
|
||||||
|
*/
|
||||||
|
onFinish: () => void;
|
||||||
|
/**
|
||||||
|
* Called when the cancel button is clicked or when we go back in the breadcrumbs.
|
||||||
|
*/
|
||||||
|
onCancelClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component to set up or change the recovery key.
|
||||||
|
*/
|
||||||
|
export function ChangeRecoveryKey({
|
||||||
|
userHasRecoveryKey,
|
||||||
|
onFinish,
|
||||||
|
onCancelClick,
|
||||||
|
}: ChangeRecoveryKeyProps): JSX.Element | null {
|
||||||
|
const matrixClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
// If the user is setting up recovery for the first time, we first show them a panel explaining what
|
||||||
|
// "recovery" is about. Otherwise, we jump straight to showing the user the new key.
|
||||||
|
const [state, setState] = useState<State>(userHasRecoveryKey ? "save_key_change_flow" : "inform_user");
|
||||||
|
|
||||||
|
// We create a new recovery key, the recovery key will be displayed to the user
|
||||||
|
const recoveryKey = useAsyncMemo(() => matrixClient.getCrypto()!.createRecoveryKeyFromPassphrase(), []);
|
||||||
|
// Waiting for the recovery key to be generated
|
||||||
|
if (!recoveryKey) return null;
|
||||||
|
|
||||||
|
let content: JSX.Element;
|
||||||
|
switch (state) {
|
||||||
|
case "inform_user":
|
||||||
|
// Show a panel explaining what "recovery" is for, and what a recovery key does.
|
||||||
|
content = (
|
||||||
|
<InformationPanel
|
||||||
|
onContinueClick={() => setState("save_key_setup_flow")}
|
||||||
|
onCancelClick={onCancelClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "save_key_setup_flow":
|
||||||
|
case "save_key_change_flow":
|
||||||
|
// Show a generated recovery key and ask the user to save it.
|
||||||
|
content = (
|
||||||
|
<KeyPanel
|
||||||
|
// encodedPrivateKey is always defined, the optional typing is incorrect
|
||||||
|
recoveryKey={recoveryKey.encodedPrivateKey!}
|
||||||
|
onConfirmClick={() =>
|
||||||
|
setState((currentState) =>
|
||||||
|
currentState === "save_key_change_flow"
|
||||||
|
? "confirm_key_change_flow"
|
||||||
|
: "confirm_key_setup_flow",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onCancelClick={onCancelClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "confirm_key_setup_flow":
|
||||||
|
case "confirm_key_change_flow":
|
||||||
|
// Ask the user to enter the recovery key they just saved to confirm it.
|
||||||
|
content = (
|
||||||
|
<KeyForm
|
||||||
|
// encodedPrivateKey is always defined, the optional typing is incorrect
|
||||||
|
recoveryKey={recoveryKey.encodedPrivateKey!}
|
||||||
|
onCancelClick={onCancelClick}
|
||||||
|
onSubmit={async () => {
|
||||||
|
const crypto = matrixClient.getCrypto();
|
||||||
|
if (!crypto) return onFinish();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// We need to enable the cache to avoid to prompt the user to enter the new key
|
||||||
|
// when we will try to access the secret storage during the bootstrap
|
||||||
|
await withSecretStorageKeyCache(() =>
|
||||||
|
crypto.bootstrapSecretStorage({
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
createSecretStorageKey: async () => recoveryKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onFinish();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to bootstrap secret storage", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
submitButtonLabel={
|
||||||
|
state === "confirm_key_setup_flow"
|
||||||
|
? _t("settings|encryption|recovery|set_up_recovery_confirm_button")
|
||||||
|
: _t("settings|encryption|recovery|change_recovery_confirm_button")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
_t("settings|encryption|title"),
|
||||||
|
userHasRecoveryKey
|
||||||
|
? _t("settings|encryption|recovery|change_recovery_key")
|
||||||
|
: _t("settings|encryption|recovery|set_up_recovery"),
|
||||||
|
];
|
||||||
|
const labels = getLabels(state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb
|
||||||
|
backLabel={_t("action|back")}
|
||||||
|
onBackClick={onCancelClick}
|
||||||
|
pages={pages}
|
||||||
|
onPageClick={onCancelClick}
|
||||||
|
/>
|
||||||
|
<EncryptionCard title={labels.title} description={labels.description} className="mx_ChangeRecoveryKey">
|
||||||
|
{content}
|
||||||
|
</EncryptionCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Labels = {
|
||||||
|
/**
|
||||||
|
* The title of the card.
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The description of the card.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the header title and description for the given state.
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
function getLabels(state: State): Labels {
|
||||||
|
switch (state) {
|
||||||
|
case "inform_user":
|
||||||
|
return {
|
||||||
|
title: _t("settings|encryption|recovery|set_up_recovery"),
|
||||||
|
description: _t("settings|encryption|recovery|set_up_recovery_description", {
|
||||||
|
changeRecoveryKeyButton: _t("settings|encryption|recovery|change_recovery_key"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case "save_key_setup_flow":
|
||||||
|
return {
|
||||||
|
title: _t("settings|encryption|recovery|set_up_recovery_save_key_title"),
|
||||||
|
description: _t("settings|encryption|recovery|set_up_recovery_save_key_description"),
|
||||||
|
};
|
||||||
|
case "save_key_change_flow":
|
||||||
|
return {
|
||||||
|
title: _t("settings|encryption|recovery|change_recovery_key_title"),
|
||||||
|
description: _t("settings|encryption|recovery|change_recovery_key_description"),
|
||||||
|
};
|
||||||
|
case "confirm_key_setup_flow":
|
||||||
|
return {
|
||||||
|
title: _t("settings|encryption|recovery|set_up_recovery_confirm_title"),
|
||||||
|
description: _t("settings|encryption|recovery|set_up_recovery_confirm_description"),
|
||||||
|
};
|
||||||
|
case "confirm_key_change_flow":
|
||||||
|
return {
|
||||||
|
title: _t("settings|encryption|recovery|change_recovery_confirm_title"),
|
||||||
|
description: _t("settings|encryption|recovery|change_recovery_confirm_description"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InformationPanelProps {
|
||||||
|
/**
|
||||||
|
* Called when the continue button is clicked.
|
||||||
|
*/
|
||||||
|
onContinueClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
/**
|
||||||
|
* Called when the cancel button is clicked.
|
||||||
|
*/
|
||||||
|
onCancelClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The panel to display information about the recovery key.
|
||||||
|
*/
|
||||||
|
function InformationPanel({ onContinueClick, onCancelClick }: InformationPanelProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text as="span" weight="medium" className="mx_InformationPanel_description">
|
||||||
|
{_t("settings|encryption|recovery|set_up_recovery_secondary_description")}
|
||||||
|
</Text>
|
||||||
|
<div className="mx_ChangeRecoveryKey_footer">
|
||||||
|
<Button onClick={onContinueClick}>{_t("action|continue")}</Button>
|
||||||
|
<Button kind="tertiary" onClick={onCancelClick}>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyPanelProps {
|
||||||
|
/**
|
||||||
|
* Called when the confirm button is clicked.
|
||||||
|
*/
|
||||||
|
onConfirmClick: MouseEventHandler;
|
||||||
|
/**
|
||||||
|
* Called when the cancel button is clicked.
|
||||||
|
*/
|
||||||
|
onCancelClick: MouseEventHandler;
|
||||||
|
/**
|
||||||
|
* The recovery key to display.
|
||||||
|
*/
|
||||||
|
recoveryKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The panel to display the recovery key.
|
||||||
|
*/
|
||||||
|
function KeyPanel({ recoveryKey, onConfirmClick, onCancelClick }: KeyPanelProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx_KeyPanel">
|
||||||
|
<Text as="span" weight="medium">
|
||||||
|
{_t("settings|encryption|recovery|save_key_title")}
|
||||||
|
</Text>
|
||||||
|
<div>
|
||||||
|
<Text as="span" className="mx_KeyPanel_key" data-testid="recoveryKey">
|
||||||
|
{recoveryKey}
|
||||||
|
</Text>
|
||||||
|
<Text as="span" size="sm">
|
||||||
|
{_t("settings|encryption|recovery|save_key_description")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<IconButton aria-label={_t("action|copy")} size="28px" onClick={() => copyPlaintext(recoveryKey)}>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="mx_ChangeRecoveryKey_footer">
|
||||||
|
<Button onClick={onConfirmClick}>{_t("action|continue")}</Button>
|
||||||
|
<Button kind="tertiary" onClick={onCancelClick}>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyFormProps {
|
||||||
|
/**
|
||||||
|
* Called when the cancel button is clicked.
|
||||||
|
*/
|
||||||
|
onCancelClick: MouseEventHandler;
|
||||||
|
/**
|
||||||
|
* Called when the form is submitted.
|
||||||
|
*/
|
||||||
|
onSubmit: FormEventHandler;
|
||||||
|
/**
|
||||||
|
* The recovery key to confirm.
|
||||||
|
*/
|
||||||
|
recoveryKey: string;
|
||||||
|
/**
|
||||||
|
* The label for the submit button.
|
||||||
|
*/
|
||||||
|
submitButtonLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form to confirm the recovery key.
|
||||||
|
* The finish button is disabled until the key is filled and valid.
|
||||||
|
* The entered key is valid if it matches the recovery key.
|
||||||
|
*/
|
||||||
|
function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: KeyFormProps): JSX.Element {
|
||||||
|
// Undefined by default, as the key is not filled yet
|
||||||
|
const [isKeyValid, setIsKeyValid] = useState<boolean>();
|
||||||
|
const isKeyInvalidAndFilled = isKeyValid === false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
className="mx_KeyForm"
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onSubmit(evt);
|
||||||
|
}}
|
||||||
|
onChange={async (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
// We don't have any file in the form, we can cast it as string safely
|
||||||
|
const filledKey = new FormData(evt.currentTarget).get("recoveryKey") as string | "";
|
||||||
|
setIsKeyValid(filledKey.trim() === recoveryKey);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field name="recoveryKey" serverInvalid={isKeyInvalidAndFilled}>
|
||||||
|
<Label>{_t("settings|encryption|recovery|enter_recovery_key")}</Label>
|
||||||
|
|
||||||
|
<TextControl required={true} />
|
||||||
|
{isKeyInvalidAndFilled && (
|
||||||
|
<ErrorMessage>{_t("settings|encryption|recovery|enter_key_error")}</ErrorMessage>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<div className="mx_ChangeRecoveryKey_footer">
|
||||||
|
<Button disabled={!isKeyValid}>{submitButtonLabel}</Button>
|
||||||
|
<Button kind="tertiary" onClick={onCancelClick}>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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, { JSX, PropsWithChildren } from "react";
|
||||||
|
import { BigIcon, Heading } from "@vector-im/compound-web";
|
||||||
|
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
interface EncryptionCardProps {
|
||||||
|
/**
|
||||||
|
* CSS class name to apply to the card.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* The title of the card.
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The description of the card.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A styled card for encryption settings.
|
||||||
|
*/
|
||||||
|
export function EncryptionCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<EncryptionCardProps>): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className={classNames("mx_EncryptionCard", className)}>
|
||||||
|
<div className="mx_EncryptionCard_header">
|
||||||
|
<BigIcon>
|
||||||
|
<KeyIcon />
|
||||||
|
</BigIcon>
|
||||||
|
<Heading as="h2" size="sm" weight="semibold">
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
<span>{description}</span>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { JSX, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Button, InlineSpinner } from "@vector-im/compound-web";
|
||||||
|
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||||
|
|
||||||
|
import { SettingsSection } from "../shared/SettingsSection";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
|
import { SettingsHeader } from "../SettingsHeader";
|
||||||
|
import { accessSecretStorage } from "../../../../SecurityManager";
|
||||||
|
import { SettingsSubheader } from "../SettingsSubheader";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible states of the recovery panel.
|
||||||
|
* - `loading`: We are checking the recovery key and the secrets.
|
||||||
|
* - `missing_recovery_key`: The user has no recovery key.
|
||||||
|
* - `secrets_not_cached`: The user has a recovery key but the secrets are not cached.
|
||||||
|
* This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||||
|
* - `good`: The user has a recovery key and the secrets are cached.
|
||||||
|
*/
|
||||||
|
type State = "loading" | "missing_recovery_key" | "secrets_not_cached" | "good";
|
||||||
|
|
||||||
|
interface RecoveryPanelProps {
|
||||||
|
/**
|
||||||
|
* Callback for when the user wants to set up or change their recovery key.
|
||||||
|
*
|
||||||
|
* @param setupNewKey - set if the user does not already have a recovery key (and has therefore clicked on
|
||||||
|
* "Set up recovery" rather than "Change recovery key").
|
||||||
|
*/
|
||||||
|
onChangeRecoveryKeyClick: (setupNewKey: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component allows the user to set up or change their recovery key.
|
||||||
|
*/
|
||||||
|
export function RecoveryPanel({ onChangeRecoveryKeyClick }: RecoveryPanelProps): JSX.Element {
|
||||||
|
const [state, setState] = useState<State>("loading");
|
||||||
|
const isMissingRecoveryKey = state === "missing_recovery_key";
|
||||||
|
|
||||||
|
const matrixClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
const checkEncryption = useCallback(async () => {
|
||||||
|
const crypto = matrixClient.getCrypto()!;
|
||||||
|
|
||||||
|
// Check if the user has a recovery key
|
||||||
|
const hasRecoveryKey = Boolean(await matrixClient.secretStorage.getDefaultKeyId());
|
||||||
|
if (!hasRecoveryKey) return setState("missing_recovery_key");
|
||||||
|
|
||||||
|
// Check if the secrets are cached
|
||||||
|
const cachedSecrets = (await crypto.getCrossSigningStatus()).privateKeysCachedLocally;
|
||||||
|
const secretsOk = cachedSecrets.masterKey && cachedSecrets.selfSigningKey && cachedSecrets.userSigningKey;
|
||||||
|
if (!secretsOk) return setState("secrets_not_cached");
|
||||||
|
|
||||||
|
setState("good");
|
||||||
|
}, [matrixClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkEncryption();
|
||||||
|
}, [checkEncryption]);
|
||||||
|
|
||||||
|
let content: JSX.Element;
|
||||||
|
switch (state) {
|
||||||
|
case "loading":
|
||||||
|
content = <InlineSpinner aria-label={_t("common|loading")} />;
|
||||||
|
break;
|
||||||
|
case "missing_recovery_key":
|
||||||
|
content = (
|
||||||
|
<Button size="sm" kind="primary" Icon={KeyIcon} onClick={() => onChangeRecoveryKeyClick(true)}>
|
||||||
|
{_t("settings|encryption|recovery|set_up_recovery")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "secrets_not_cached":
|
||||||
|
content = (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
kind="primary"
|
||||||
|
Icon={KeyIcon}
|
||||||
|
onClick={async () => await accessSecretStorage(checkEncryption)}
|
||||||
|
>
|
||||||
|
{_t("settings|encryption|recovery|enter_recovery_key")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "good":
|
||||||
|
content = (
|
||||||
|
<Button size="sm" kind="secondary" Icon={KeyIcon} onClick={() => onChangeRecoveryKeyClick(false)}>
|
||||||
|
{_t("settings|encryption|recovery|change_recovery_key")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
legacy={false}
|
||||||
|
heading={
|
||||||
|
<SettingsHeader
|
||||||
|
hasRecommendedTag={isMissingRecoveryKey}
|
||||||
|
label={_t("settings|encryption|recovery|title")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
subHeading={<Subheader state={state} />}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubheaderProps {
|
||||||
|
/**
|
||||||
|
* The state of the recovery panel.
|
||||||
|
*/
|
||||||
|
state: State;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subheader for the recovery panel.
|
||||||
|
*/
|
||||||
|
function Subheader({ state }: SubheaderProps): JSX.Element {
|
||||||
|
// If the secrets are not cached, we display a warning message.
|
||||||
|
if (state !== "secrets_not_cached") return <>{_t("settings|encryption|recovery|description")}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSubheader
|
||||||
|
label={_t("settings|encryption|recovery|description")}
|
||||||
|
state="error"
|
||||||
|
stateMessage={_t("settings|encryption|recovery|key_storage_warning")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,19 +10,24 @@ import classnames from "classnames";
|
|||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes } from "react";
|
||||||
|
|
||||||
import Heading from "../../typography/Heading";
|
import Heading from "../../typography/Heading";
|
||||||
|
import { SettingsHeader } from "../SettingsHeader";
|
||||||
|
|
||||||
export interface SettingsSectionProps extends HTMLAttributes<HTMLDivElement> {
|
export interface SettingsSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
heading?: string | React.ReactNode;
|
heading?: string | React.ReactNode;
|
||||||
|
subHeading?: string | React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
legacy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHeading(heading: string | React.ReactNode | undefined): React.ReactNode | undefined {
|
function renderHeading(heading: string | React.ReactNode | undefined, legacy: boolean): React.ReactNode | undefined {
|
||||||
switch (typeof heading) {
|
switch (typeof heading) {
|
||||||
case "string":
|
case "string":
|
||||||
return (
|
return legacy ? (
|
||||||
<Heading as="h2" size="3">
|
<Heading as="h2" size="3">
|
||||||
{heading}
|
{heading}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
) : (
|
||||||
|
<SettingsHeader label={heading} />
|
||||||
);
|
);
|
||||||
case "undefined":
|
case "undefined":
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -48,9 +53,29 @@ function renderHeading(heading: string | React.ReactNode | undefined): React.Rea
|
|||||||
* </SettingsTab>
|
* </SettingsTab>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const SettingsSection: React.FC<SettingsSectionProps> = ({ className, heading, children, ...rest }) => (
|
export const SettingsSection: React.FC<SettingsSectionProps> = ({
|
||||||
<div {...rest} className={classnames("mx_SettingsSection", className)}>
|
className,
|
||||||
{renderHeading(heading)}
|
heading,
|
||||||
<div className="mx_SettingsSection_subSections">{children}</div>
|
subHeading,
|
||||||
|
legacy = true,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
className={classnames("mx_SettingsSection", className, {
|
||||||
|
mx_SettingsSection_newUi: !legacy,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{heading &&
|
||||||
|
(subHeading ? (
|
||||||
|
<div className="mx_SettingsSection_header">
|
||||||
|
{renderHeading(heading, legacy)}
|
||||||
|
{subHeading}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderHeading(heading, legacy)
|
||||||
|
))}
|
||||||
|
{legacy ? <div className="mx_SettingsSection_subSections">{children}</div> : children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
export interface SettingsTabProps extends Omit<HTMLAttributes<HTMLDivElement>, "className"> {
|
export interface SettingsTabProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Added to the classList of the root element
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +34,8 @@ export interface SettingsTabProps extends Omit<HTMLAttributes<HTMLDivElement>, "
|
|||||||
* </SettingsTab>
|
* </SettingsTab>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
const SettingsTab: React.FC<SettingsTabProps> = ({ children, ...rest }) => (
|
const SettingsTab: React.FC<SettingsTabProps> = ({ children, className, ...rest }) => (
|
||||||
<div {...rest} className="mx_SettingsTab">
|
<div {...rest} className={classNames("mx_SettingsTab", className)}>
|
||||||
<div className="mx_SettingsTab_sections">{children}</div>
|
<div className="mx_SettingsTab_sections">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { JSX, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Button, InlineSpinner } from "@vector-im/compound-web";
|
||||||
|
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
|
||||||
|
|
||||||
|
import SettingsTab from "../SettingsTab";
|
||||||
|
import { RecoveryPanel } from "../../encryption/RecoveryPanel";
|
||||||
|
import { ChangeRecoveryKey } from "../../encryption/ChangeRecoveryKey";
|
||||||
|
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||||
|
import { _t } from "../../../../../languageHandler";
|
||||||
|
import Modal from "../../../../../Modal";
|
||||||
|
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
||||||
|
import { SettingsSection } from "../../shared/SettingsSection";
|
||||||
|
import { SettingsSubheader } from "../../SettingsSubheader";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state in the encryption settings tab.
|
||||||
|
* - "loading": We are checking if the device is verified.
|
||||||
|
* - "main": The main panel with all the sections (Key storage, recovery, advanced).
|
||||||
|
* - "set_up_encryption": The panel to show when the user is setting up their encryption.
|
||||||
|
* This happens when the user doesn't have cross-signing enabled, or their current device is not verified.
|
||||||
|
* - "change_recovery_key": The panel to show when the user is changing their recovery key.
|
||||||
|
* This happens when the user has a recovery key and the user clicks on "Change recovery key" button of the RecoveryPanel.
|
||||||
|
* - "set_recovery_key": The panel to show when the user is setting up their recovery key.
|
||||||
|
* This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel.
|
||||||
|
*/
|
||||||
|
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key";
|
||||||
|
|
||||||
|
export function EncryptionUserSettingsTab(): JSX.Element {
|
||||||
|
const [state, setState] = useState<State>("loading");
|
||||||
|
const setUpEncryptionRequired = useSetUpEncryptionRequired(setState);
|
||||||
|
|
||||||
|
let content: JSX.Element;
|
||||||
|
switch (state) {
|
||||||
|
case "loading":
|
||||||
|
content = <InlineSpinner aria-label={_t("common|loading")} />;
|
||||||
|
break;
|
||||||
|
case "set_up_encryption":
|
||||||
|
content = <SetUpEncryptionPanel onFinish={setUpEncryptionRequired} />;
|
||||||
|
break;
|
||||||
|
case "main":
|
||||||
|
content = (
|
||||||
|
<RecoveryPanel
|
||||||
|
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||||
|
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "change_recovery_key":
|
||||||
|
case "set_recovery_key":
|
||||||
|
content = (
|
||||||
|
<ChangeRecoveryKey
|
||||||
|
userHasRecoveryKey={state === "change_recovery_key"}
|
||||||
|
onCancelClick={() => setState("main")}
|
||||||
|
onFinish={() => setState("main")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsTab className="mx_EncryptionUserSettingsTab" data-testid="encryptionTab">
|
||||||
|
{content}
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if the user needs to go through the SetupEncryption flow.
|
||||||
|
* If the user needs to set up the encryption, the state will be set to "set_up_encryption".
|
||||||
|
* Otherwise, the state will be set to "main".
|
||||||
|
*
|
||||||
|
* The state is set once when the component is first mounted.
|
||||||
|
* Also returns a callback function which can be called to re-run the logic.
|
||||||
|
*
|
||||||
|
* @param setState - callback passed from the EncryptionUserSettingsTab to set the current `State`.
|
||||||
|
* @returns a callback function, which will re-run the logic and update the state.
|
||||||
|
*/
|
||||||
|
function useSetUpEncryptionRequired(setState: (state: State) => void): () => Promise<void> {
|
||||||
|
const matrixClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
const setUpEncryptionRequired = useCallback(async () => {
|
||||||
|
const crypto = matrixClient.getCrypto()!;
|
||||||
|
const isCrossSigningReady = await crypto.isCrossSigningReady();
|
||||||
|
if (isCrossSigningReady) setState("main");
|
||||||
|
else setState("set_up_encryption");
|
||||||
|
}, [matrixClient, setState]);
|
||||||
|
|
||||||
|
// Initialise the state when the component is mounted
|
||||||
|
useEffect(() => {
|
||||||
|
setUpEncryptionRequired();
|
||||||
|
}, [setUpEncryptionRequired]);
|
||||||
|
|
||||||
|
// Also return the callback so that the component can re-run the logic.
|
||||||
|
return setUpEncryptionRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetUpEncryptionPanelProps {
|
||||||
|
/**
|
||||||
|
* Callback to call when the user has finished setting up encryption.
|
||||||
|
*/
|
||||||
|
onFinish: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel to show when the user needs to go through the SetupEncryption flow.
|
||||||
|
*/
|
||||||
|
function SetUpEncryptionPanel({ onFinish }: SetUpEncryptionPanelProps): JSX.Element {
|
||||||
|
// Strictly speaking, the SetupEncryptionDialog may make the user do things other than
|
||||||
|
// verify their device (in particular, if they manage to get here without cross-signing keys existing);
|
||||||
|
// however the common case is that they will be asked to verify, so we just show buttons and headings
|
||||||
|
// that talk about verification.
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
legacy={false}
|
||||||
|
heading={_t("settings|encryption|device_not_verified_title")}
|
||||||
|
subHeading={
|
||||||
|
<SettingsSubheader
|
||||||
|
stateMessage={_t("settings|encryption|device_not_verified_description")}
|
||||||
|
state="error"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
Icon={ComputerIcon}
|
||||||
|
onClick={() => Modal.createDialog(SetupEncryptionDialog, { onFinished: onFinish })}
|
||||||
|
>
|
||||||
|
{_t("settings|encryption|device_not_verified_button")}
|
||||||
|
</Button>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -539,6 +539,7 @@
|
|||||||
"qr_code": "QR Code",
|
"qr_code": "QR Code",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
"reactions": "Reactions",
|
"reactions": "Reactions",
|
||||||
|
"recommended": "Recommended",
|
||||||
"report_a_bug": "Report a bug",
|
"report_a_bug": "Report a bug",
|
||||||
"room": "Room",
|
"room": "Room",
|
||||||
"room_name": "Room name",
|
"room_name": "Room name",
|
||||||
@@ -2416,6 +2417,36 @@
|
|||||||
"emoji_autocomplete": "Enable Emoji suggestions while typing",
|
"emoji_autocomplete": "Enable Emoji suggestions while typing",
|
||||||
"enable_markdown": "Enable Markdown",
|
"enable_markdown": "Enable Markdown",
|
||||||
"enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.",
|
"enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.",
|
||||||
|
"encryption": {
|
||||||
|
"device_not_verified_button": "Verify this device",
|
||||||
|
"device_not_verified_description": "You need to verify this device in order to view your encryption settings.",
|
||||||
|
"device_not_verified_title": "Device not verified",
|
||||||
|
"dialog_title": "<strong>Settings:</strong> Encryption",
|
||||||
|
"recovery": {
|
||||||
|
"change_recovery_confirm_button": "Confirm new recovery key",
|
||||||
|
"change_recovery_confirm_description": "Enter your new recovery key below to finish. Your old one will no longer work.",
|
||||||
|
"change_recovery_confirm_title": "Enter your new recovery key",
|
||||||
|
"change_recovery_key": "Change recovery key",
|
||||||
|
"change_recovery_key_description": "Write down this new recovery key somewhere safe. Then click Continue to confirm the change.",
|
||||||
|
"change_recovery_key_title": "Change recovery key?",
|
||||||
|
"description": "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.",
|
||||||
|
"enter_key_error": "The recovery key you entered is not correct.",
|
||||||
|
"enter_recovery_key": "Enter recovery key",
|
||||||
|
"key_storage_warning": "Your key storage is out of sync. Click the button below to fix the problem.",
|
||||||
|
"save_key_description": "Do not share this with anyone!",
|
||||||
|
"save_key_title": "Recovery key",
|
||||||
|
"set_up_recovery": "Set up recovery",
|
||||||
|
"set_up_recovery_confirm_button": "Finish set up",
|
||||||
|
"set_up_recovery_confirm_description": "Enter the recovery key shown on the previous screen to finish setting up recovery.",
|
||||||
|
"set_up_recovery_confirm_title": "Enter your recovery key to confirm",
|
||||||
|
"set_up_recovery_description": "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘%(changeRecoveryKeyButton)s’.",
|
||||||
|
"set_up_recovery_save_key_description": "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.",
|
||||||
|
"set_up_recovery_save_key_title": "Save your recovery key somewhere safe",
|
||||||
|
"set_up_recovery_secondary_description": "After clicking continue, we’ll generate a recovery key for you.",
|
||||||
|
"title": "Recovery"
|
||||||
|
},
|
||||||
|
"title": "Encryption"
|
||||||
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"account_management_section": "Account management",
|
"account_management_section": "Account management",
|
||||||
"account_section": "Account",
|
"account_section": "Account",
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export function createTestClient(): MatrixClient {
|
|||||||
isStored: jest.fn().mockReturnValue(false),
|
isStored: jest.fn().mockReturnValue(false),
|
||||||
checkKey: jest.fn().mockResolvedValue(false),
|
checkKey: jest.fn().mockResolvedValue(false),
|
||||||
hasKey: jest.fn().mockReturnValue(false),
|
hasKey: jest.fn().mockReturnValue(false),
|
||||||
|
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||||
},
|
},
|
||||||
|
|
||||||
store: {
|
store: {
|
||||||
@@ -127,7 +128,10 @@ export function createTestClient(): MatrixClient {
|
|||||||
bootstrapCrossSigning: jest.fn(),
|
bootstrapCrossSigning: jest.fn(),
|
||||||
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
|
||||||
isKeyBackupTrusted: jest.fn().mockResolvedValue({}),
|
isKeyBackupTrusted: jest.fn().mockResolvedValue({}),
|
||||||
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({}),
|
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({
|
||||||
|
privateKey: new Uint8Array(32),
|
||||||
|
encodedPrivateKey: "encoded private key",
|
||||||
|
}),
|
||||||
bootstrapSecretStorage: jest.fn(),
|
bootstrapSecretStorage: jest.fn(),
|
||||||
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
isDehydrationSupported: jest.fn().mockResolvedValue(false),
|
||||||
restoreKeyBackup: jest.fn(),
|
restoreKeyBackup: jest.fn(),
|
||||||
@@ -137,6 +141,16 @@ export function createTestClient(): MatrixClient {
|
|||||||
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
|
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
|
||||||
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
|
||||||
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
|
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
|
||||||
|
getCrossSigningStatus: jest.fn().mockResolvedValue({
|
||||||
|
publicKeysOnDevice: false,
|
||||||
|
privateKeysInSecretStorage: false,
|
||||||
|
privateKeysCachedLocally: {
|
||||||
|
masterKey: false,
|
||||||
|
selfSigningKey: false,
|
||||||
|
userSigningKey: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
isCrossSigningReady: jest.fn().mockResolvedValue(false),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPushActionsForEvent: jest.fn(),
|
getPushActionsForEvent: jest.fn(),
|
||||||
|
|||||||
@@ -225,6 +225,32 @@ NodeList [
|
|||||||
Security & Privacy
|
Security & Privacy
|
||||||
</span>
|
</span>
|
||||||
</li>,
|
</li>,
|
||||||
|
<li
|
||||||
|
aria-controls="mx_tabpanel_USER_ENCRYPTION_TAB"
|
||||||
|
aria-selected="false"
|
||||||
|
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||||
|
data-testid="settings-tab-USER_ENCRYPTION_TAB"
|
||||||
|
role="tab"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 5 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 7 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 7 14Zm0 4c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.117 0 2.13.275 3.037.825A6.212 6.212 0 0 1 12.2 9h8.375a1.033 1.033 0 0 1 .725.3l2 2c.1.1.17.208.212.325.042.117.063.242.063.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-3.175 3.175a.946.946 0 0 1-.3.2c-.117.05-.233.083-.35.1a.832.832 0 0 1-.35-.025.884.884 0 0 1-.325-.175L17.5 15l-1.425 1.075a.945.945 0 0 1-.887.15.859.859 0 0 1-.288-.15L13.375 15H12.2a6.212 6.212 0 0 1-2.162 2.175C9.128 17.725 8.117 18 7 18Zm0-2c.933 0 1.754-.283 2.463-.85A4.032 4.032 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.032 4.032 0 0 0-1.412-2.15C8.754 8.283 7.933 8 7 8c-1.1 0-2.042.392-2.825 1.175C3.392 9.958 3 10.9 3 12s.392 2.042 1.175 2.825C4.958 15.608 5.9 16 7 16Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="mx_TabbedView_tabLabel_text"
|
||||||
|
id="mx_tabpanel_USER_ENCRYPTION_TAB_label"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</span>
|
||||||
|
</li>,
|
||||||
<li
|
<li
|
||||||
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
||||||
aria-selected="false"
|
aria-selected="false"
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ exports[`CreateSecretStorageDialog handles the happy path 2`] = `
|
|||||||
<div
|
<div
|
||||||
class="mx_CreateSecretStorageDialog_recoveryKey"
|
class="mx_CreateSecretStorageDialog_recoveryKey"
|
||||||
>
|
>
|
||||||
<code />
|
<code>
|
||||||
|
encoded private key
|
||||||
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_CreateSecretStorageDialog_recoveryKeyButtons"
|
class="mx_CreateSecretStorageDialog_recoveryKeyButtons"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import { SettingsHeader } from "../../../../../src/components/views/settings/SettingsHeader";
|
||||||
|
|
||||||
|
describe("<SettingsHeader />", () => {
|
||||||
|
it("should render the component", () => {
|
||||||
|
const { asFragment } = render(<SettingsHeader label="Settings Header" />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the component with the recommended tag", () => {
|
||||||
|
const { asFragment } = render(<SettingsHeader label="Settings Header" hasRecommendedTag={true} />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import { SettingsSubheader } from "../../../../../src/components/views/settings/SettingsSubheader";
|
||||||
|
|
||||||
|
describe("<SettingsSubheader />", () => {
|
||||||
|
it("should display a check icon when in success", () => {
|
||||||
|
const { asFragment } = render(<SettingsSubheader state="success" stateMessage="Success!" />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display an error icon when in error", () => {
|
||||||
|
const { asFragment } = render(<SettingsSubheader state="error" stateMessage="Error!" />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display a label", () => {
|
||||||
|
const { asFragment } = render(<SettingsSubheader label="My label" state="success" stateMessage="Success!" />);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SettingsHeader /> should render the component 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Settings Header
|
||||||
|
</h2>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SettingsHeader /> should render the component with the recommended tag 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Settings Header
|
||||||
|
<span>
|
||||||
|
Recommended
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SettingsSubheader /> should display a check icon when in success 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubheader"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_SettingsSubheader_success"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m10.6 13.8-2.15-2.15a.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275.948.948 0 0 0-.275.7.95.95 0 0 0 .275.7L9.9 15.9c.2.2.433.3.7.3.267 0 .5-.1.7-.3l5.65-5.65a.948.948 0 0 0 .275-.7.948.948 0 0 0-.275-.7.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275L10.6 13.8ZM12 22a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Success!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SettingsSubheader /> should display a label 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubheader"
|
||||||
|
>
|
||||||
|
My label
|
||||||
|
<span
|
||||||
|
class="mx_SettingsSubheader_success"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m10.6 13.8-2.15-2.15a.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275.948.948 0 0 0-.275.7.95.95 0 0 0 .275.7L9.9 15.9c.2.2.433.3.7.3.267 0 .5-.1.7-.3l5.65-5.65a.948.948 0 0 0 .275-.7.948.948 0 0 0-.275-.7.948.948 0 0 0-.7-.275.948.948 0 0 0-.7.275L10.6 13.8ZM12 22a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Success!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SettingsSubheader /> should display an error icon when in error 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubheader"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_SettingsSubheader_error"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Error!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen, waitFor } from "jest-matrix-react";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { ChangeRecoveryKey } from "../../../../../../src/components/views/settings/encryption/ChangeRecoveryKey";
|
||||||
|
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||||
|
import { copyPlaintext } from "../../../../../../src/utils/strings";
|
||||||
|
|
||||||
|
jest.mock("../../../../../../src/utils/strings", () => ({
|
||||||
|
copyPlaintext: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("<ChangeRecoveryKey />", () => {
|
||||||
|
let matrixClient: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
matrixClient = createTestClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent(userHasRecoveryKey = true, onFinish = jest.fn(), onCancelClick = jest.fn()) {
|
||||||
|
return render(
|
||||||
|
<ChangeRecoveryKey
|
||||||
|
userHasRecoveryKey={userHasRecoveryKey}
|
||||||
|
onFinish={onFinish}
|
||||||
|
onCancelClick={onCancelClick}
|
||||||
|
/>,
|
||||||
|
withClientContextRenderOptions(matrixClient),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("flow to setup a recovery key", () => {
|
||||||
|
it("should display information about the recovery key", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const onCancelClick = jest.fn();
|
||||||
|
const { asFragment } = renderComponent(false, jest.fn(), onCancelClick);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
"Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
|
expect(onCancelClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the recovery key", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const onCancelClick = jest.fn();
|
||||||
|
const { asFragment } = renderComponent(false, jest.fn(), onCancelClick);
|
||||||
|
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||||
|
|
||||||
|
expect(screen.getByText("Save your recovery key somewhere safe")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("encoded private key")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
// Test copy button
|
||||||
|
await user.click(screen.getByRole("button", { name: "Copy" }));
|
||||||
|
expect(copyPlaintext).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
|
expect(onCancelClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ask the user to enter the recovery key", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const onFinish = jest.fn();
|
||||||
|
const { asFragment } = renderComponent(false, onFinish);
|
||||||
|
// Display the recovery key to save
|
||||||
|
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||||
|
// Display the form to confirm the recovery key
|
||||||
|
await waitFor(() => user.click(screen.getByRole("button", { name: "Continue" })));
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText("Enter your recovery key to confirm")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
// The finish button should be disabled by default
|
||||||
|
const finishButton = screen.getByRole("button", { name: "Finish set up" });
|
||||||
|
expect(finishButton).toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox");
|
||||||
|
// If the user enters an incorrect recovery key, the finish button should be disabled
|
||||||
|
// and we display an error message
|
||||||
|
await userEvent.type(input, "wrong recovery key");
|
||||||
|
expect(finishButton).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByText("The recovery key you entered is not correct.")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
// If the user enters the correct recovery key, the finish button should be enabled
|
||||||
|
await userEvent.type(input, "encoded private key");
|
||||||
|
await waitFor(() => expect(finishButton).not.toHaveAttribute("aria-disabled", "true"));
|
||||||
|
|
||||||
|
await user.click(finishButton);
|
||||||
|
expect(onFinish).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flow to change the recovery key", () => {
|
||||||
|
it("should display the recovery key", async () => {
|
||||||
|
const { asFragment } = renderComponent();
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText("Change recovery key?")).toBeInTheDocument());
|
||||||
|
expect(screen.getByText("encoded private key")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import { EncryptionCard } from "../../../../../../src/components/views/settings/encryption/EncryptionCard";
|
||||||
|
|
||||||
|
describe("<EncryptionCard />", () => {
|
||||||
|
it("should render", () => {
|
||||||
|
const { asFragment } = render(
|
||||||
|
<EncryptionCard title="My title" description="My description">
|
||||||
|
Encryption card children
|
||||||
|
</EncryptionCard>,
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { render, screen } from "jest-matrix-react";
|
||||||
|
import { waitFor } from "@testing-library/dom";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||||
|
import { RecoveryPanel } from "../../../../../../src/components/views/settings/encryption/RecoveryPanel";
|
||||||
|
import { accessSecretStorage } from "../../../../../../src/SecurityManager";
|
||||||
|
|
||||||
|
jest.mock("../../../../../../src/SecurityManager", () => ({
|
||||||
|
accessSecretStorage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("<RecoveryPanel />", () => {
|
||||||
|
let matrixClient: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
matrixClient = createTestClient();
|
||||||
|
mocked(accessSecretStorage).mockClear().mockResolvedValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderRecoverPanel(onChangeRecoveryKeyClick = jest.fn()) {
|
||||||
|
return render(
|
||||||
|
<RecoveryPanel onChangeRecoveryKeyClick={onChangeRecoveryKeyClick} />,
|
||||||
|
withClientContextRenderOptions(matrixClient),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be in loading state when checking the recovery key and the cached keys", () => {
|
||||||
|
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockImplementation(() => new Promise(() => {}));
|
||||||
|
|
||||||
|
const { asFragment } = renderRecoverPanel();
|
||||||
|
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ask to set up a recovery key when there is no recovery key", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const onChangeRecoveryKeyClick = jest.fn();
|
||||||
|
const { asFragment } = renderRecoverPanel(onChangeRecoveryKeyClick);
|
||||||
|
|
||||||
|
await waitFor(() => screen.getByRole("button", { name: "Set up recovery" }));
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Set up recovery" }));
|
||||||
|
expect(onChangeRecoveryKeyClick).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ask to enter the recovery key when secrets are not cached", async () => {
|
||||||
|
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { asFragment } = renderRecoverPanel();
|
||||||
|
|
||||||
|
await waitFor(() => screen.getByRole("button", { name: "Enter recovery key" }));
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Enter recovery key" }));
|
||||||
|
expect(accessSecretStorage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow to change the recovery key when everything is good", async () => {
|
||||||
|
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
|
||||||
|
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||||
|
privateKeysInSecretStorage: true,
|
||||||
|
publicKeysOnDevice: true,
|
||||||
|
privateKeysCachedLocally: {
|
||||||
|
masterKey: true,
|
||||||
|
selfSigningKey: true,
|
||||||
|
userSigningKey: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const onChangeRecoveryKeyClick = jest.fn();
|
||||||
|
const { asFragment } = renderRecoverPanel(onChangeRecoveryKeyClick);
|
||||||
|
await waitFor(() => screen.getByRole("button", { name: "Change recovery key" }));
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Change recovery key" }));
|
||||||
|
expect(onChangeRecoveryKeyClick).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ChangeRecoveryKey /> flow to change the recovery key should display the recovery key 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<nav
|
||||||
|
class="_breadcrumb_ikpbb_17"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Back"
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<ol
|
||||||
|
class="_pages_ikpbb_26"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="_link_ue21z_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="small"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="_last-page_ikpbb_39"
|
||||||
|
>
|
||||||
|
Change recovery key
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
Change recovery key?
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Write down this new recovery key somewhere safe. Then click Continue to confirm the change.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_KeyPanel"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69"
|
||||||
|
>
|
||||||
|
Recovery key
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59 mx_KeyPanel_key"
|
||||||
|
data-testid="recoveryKey"
|
||||||
|
>
|
||||||
|
encoded private key
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||||
|
>
|
||||||
|
Do not share this with anyone!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Copy"
|
||||||
|
class="_icon-button_bh2qc_17"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 5H5v9h1a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1a1 1 0 1 1-2 0V5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 10a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2v-9Zm2 0v9h9v-9h-9Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_ChangeRecoveryKey_footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="tertiary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ChangeRecoveryKey /> flow to setup a recovery key should ask the user to enter the recovery key 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<nav
|
||||||
|
class="_breadcrumb_ikpbb_17"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Back"
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<ol
|
||||||
|
class="_pages_ikpbb_26"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="_link_ue21z_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="small"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="_last-page_ikpbb_39"
|
||||||
|
>
|
||||||
|
Set up recovery
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
Enter your recovery key to confirm
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Enter the recovery key shown on the previous screen to finish setting up recovery.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
class="_root_ssths_24 mx_KeyForm"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_field_ssths_34"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="_label_ssths_67"
|
||||||
|
for="radix-:r0:"
|
||||||
|
>
|
||||||
|
Enter recovery key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="_control_9gon8_18"
|
||||||
|
id="radix-:r0:"
|
||||||
|
name="recoveryKey"
|
||||||
|
required=""
|
||||||
|
title=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_ChangeRecoveryKey_footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="true"
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Finish set up
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="tertiary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ChangeRecoveryKey /> flow to setup a recovery key should ask the user to enter the recovery key 2`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<nav
|
||||||
|
class="_breadcrumb_ikpbb_17"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Back"
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<ol
|
||||||
|
class="_pages_ikpbb_26"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="_link_ue21z_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="small"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="_last-page_ikpbb_39"
|
||||||
|
>
|
||||||
|
Set up recovery
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
Enter your recovery key to confirm
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Enter the recovery key shown on the previous screen to finish setting up recovery.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
class="_root_ssths_24 mx_KeyForm"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_field_ssths_34"
|
||||||
|
data-invalid="true"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="_label_ssths_67"
|
||||||
|
data-invalid="true"
|
||||||
|
for="radix-:r0:"
|
||||||
|
>
|
||||||
|
Enter recovery key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
aria-describedby="radix-:r1:"
|
||||||
|
aria-invalid="true"
|
||||||
|
class="_control_9gon8_18"
|
||||||
|
data-invalid="true"
|
||||||
|
id="radix-:r0:"
|
||||||
|
name="recoveryKey"
|
||||||
|
required=""
|
||||||
|
title=""
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="_message_ssths_93 _error-message_ssths_103"
|
||||||
|
id="radix-:r1:"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
The recovery key you entered is not correct.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_ChangeRecoveryKey_footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="true"
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Finish set up
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="tertiary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ChangeRecoveryKey /> flow to setup a recovery key should display information about the recovery key 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<nav
|
||||||
|
class="_breadcrumb_ikpbb_17"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Back"
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<ol
|
||||||
|
class="_pages_ikpbb_26"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="_link_ue21z_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="small"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="_last-page_ikpbb_39"
|
||||||
|
>
|
||||||
|
Set up recovery
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
Set up recovery
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 mx_InformationPanel_description"
|
||||||
|
>
|
||||||
|
After clicking continue, we’ll generate a recovery key for you.
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_ChangeRecoveryKey_footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="tertiary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<ChangeRecoveryKey /> flow to setup a recovery key should display the recovery key 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<nav
|
||||||
|
class="_breadcrumb_ikpbb_17"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Back"
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m13.3 17.3-4.6-4.6a.877.877 0 0 1-.213-.325A1.106 1.106 0 0 1 8.425 12c0-.133.02-.258.062-.375A.878.878 0 0 1 8.7 11.3l4.6-4.6a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L10.8 12l3.9 3.9a.949.949 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<ol
|
||||||
|
class="_pages_ikpbb_26"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="_link_ue21z_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="small"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Encryption
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
class="_last-page_ikpbb_39"
|
||||||
|
>
|
||||||
|
Set up recovery
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard mx_ChangeRecoveryKey"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
Save your recovery key somewhere safe
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_KeyPanel"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69"
|
||||||
|
>
|
||||||
|
Recovery key
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59 mx_KeyPanel_key"
|
||||||
|
data-testid="recoveryKey"
|
||||||
|
>
|
||||||
|
encoded private key
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40"
|
||||||
|
>
|
||||||
|
Do not share this with anyone!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Copy"
|
||||||
|
class="_icon-button_bh2qc_17"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 5H5v9h1a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1a1 1 0 1 1-2 0V5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 10a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2v-9Zm2 0v9h9v-9h-9Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_ChangeRecoveryKey_footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17"
|
||||||
|
data-kind="tertiary"
|
||||||
|
data-size="lg"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<EncryptionCard /> should render 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_EncryptionCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_content_md016_17"
|
||||||
|
data-size="large"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.475 16.9A5.863 5.863 0 0 1 7 18c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.35 0 2.53.383 3.537 1.15 1.009.767 1.713 1.717 2.113 2.85h7.95a1.033 1.033 0 0 1 .725.3l1.025 1.025a.99.99 0 0 1 .2.288c.05.108.075.229.075.362a1.066 1.066 0 0 1-.25.7l-2.25 2.575a.973.973 0 0 1-1.038.313 1.033 1.033 0 0 1-.337-.188L17 14l-1.3 1.3c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212L13 14h-.35a5.81 5.81 0 0 1-2.175 2.9Zm-4.887-3.487c.391.39.862.587 1.412.587.55 0 1.02-.196 1.412-.588C8.804 13.021 9 12.55 9 12c0-.55-.196-1.02-.588-1.412A1.926 1.926 0 0 0 7 10c-.55 0-1.02.196-1.412.588A1.926 1.926 0 0 0 5 12c0 .55.196 1.02.588 1.412Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102"
|
||||||
|
>
|
||||||
|
My title
|
||||||
|
</h2>
|
||||||
|
<span>
|
||||||
|
My description
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
Encryption card children
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RecoveryPanel /> should allow to change the recovery key when everything is good 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Recovery
|
||||||
|
</h2>
|
||||||
|
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 5 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 7 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 7 14Zm0 4c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.117 0 2.13.275 3.037.825A6.212 6.212 0 0 1 12.2 9h8.375a1.033 1.033 0 0 1 .725.3l2 2c.1.1.17.208.212.325.042.117.063.242.063.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-3.175 3.175a.946.946 0 0 1-.3.2c-.117.05-.233.083-.35.1a.832.832 0 0 1-.35-.025.884.884 0 0 1-.325-.175L17.5 15l-1.425 1.075a.945.945 0 0 1-.887.15.859.859 0 0 1-.288-.15L13.375 15H12.2a6.212 6.212 0 0 1-2.162 2.175C9.128 17.725 8.117 18 7 18Zm0-2c.933 0 1.754-.283 2.463-.85A4.032 4.032 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.032 4.032 0 0 0-1.412-2.15C8.754 8.283 7.933 8 7 8c-1.1 0-2.042.392-2.825 1.175C3.392 9.958 3 10.9 3 12s.392 2.042 1.175 2.825C4.958 15.608 5.9 16 7 16Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Change recovery key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RecoveryPanel /> should ask to enter the recovery key when secrets are not cached 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Recovery
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubheader"
|
||||||
|
>
|
||||||
|
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||||
|
<span
|
||||||
|
class="mx_SettingsSubheader_error"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Your key storage is out of sync. Click the button below to fix the problem.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 5 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 7 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 7 14Zm0 4c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.117 0 2.13.275 3.037.825A6.212 6.212 0 0 1 12.2 9h8.375a1.033 1.033 0 0 1 .725.3l2 2c.1.1.17.208.212.325.042.117.063.242.063.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-3.175 3.175a.946.946 0 0 1-.3.2c-.117.05-.233.083-.35.1a.832.832 0 0 1-.35-.025.884.884 0 0 1-.325-.175L17.5 15l-1.425 1.075a.945.945 0 0 1-.887.15.859.859 0 0 1-.288-.15L13.375 15H12.2a6.212 6.212 0 0 1-2.162 2.175C9.128 17.725 8.117 18 7 18Zm0-2c.933 0 1.754-.283 2.463-.85A4.032 4.032 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.032 4.032 0 0 0-1.412-2.15C8.754 8.283 7.933 8 7 8c-1.1 0-2.042.392-2.825 1.175C3.392 9.958 3 10.9 3 12s.392 2.042 1.175 2.825C4.958 15.608 5.9 16 7 16Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Enter recovery key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RecoveryPanel /> should ask to set up a recovery key when there is no recovery key 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Recovery
|
||||||
|
<span>
|
||||||
|
Recommended
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 14c-.55 0-1.02-.196-1.412-.588A1.926 1.926 0 0 1 5 12c0-.55.196-1.02.588-1.412A1.926 1.926 0 0 1 7 10c.55 0 1.02.196 1.412.588.392.391.588.862.588 1.412 0 .55-.196 1.02-.588 1.412A1.926 1.926 0 0 1 7 14Zm0 4c-1.667 0-3.083-.583-4.25-1.75C1.583 15.083 1 13.667 1 12c0-1.667.583-3.083 1.75-4.25C3.917 6.583 5.333 6 7 6c1.117 0 2.13.275 3.037.825A6.212 6.212 0 0 1 12.2 9h8.375a1.033 1.033 0 0 1 .725.3l2 2c.1.1.17.208.212.325.042.117.063.242.063.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-3.175 3.175a.946.946 0 0 1-.3.2c-.117.05-.233.083-.35.1a.832.832 0 0 1-.35-.025.884.884 0 0 1-.325-.175L17.5 15l-1.425 1.075a.945.945 0 0 1-.887.15.859.859 0 0 1-.288-.15L13.375 15H12.2a6.212 6.212 0 0 1-2.162 2.175C9.128 17.725 8.117 18 7 18Zm0-2c.933 0 1.754-.283 2.463-.85A4.032 4.032 0 0 0 10.875 13H14l1.45 1.025L17.5 12.5l1.775 1.375L21.15 12l-1-1h-9.275a4.032 4.032 0 0 0-1.412-2.15C8.754 8.283 7.933 8 7 8c-1.1 0-2.042.392-2.825 1.175C3.392 9.958 3 10.9 3 12s.392 2.042 1.175 2.825C4.958 15.608 5.9 16 7 16Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Set up recovery
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RecoveryPanel /> should be in loading state when checking the recovery key and the cached keys 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Recovery
|
||||||
|
</h2>
|
||||||
|
Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
aria-label="Loading…"
|
||||||
|
class="_icon_1ye7b_27"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
style="width: 20px; height: 20px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2Z"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { waitFor } from "@testing-library/dom";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { EncryptionUserSettingsTab } from "../../../../../../../src/components/views/settings/tabs/user/EncryptionUserSettingsTab";
|
||||||
|
import { createTestClient, withClientContextRenderOptions } from "../../../../../../test-utils";
|
||||||
|
import Modal from "../../../../../../../src/Modal";
|
||||||
|
|
||||||
|
describe("<EncryptionUserSettingsTab />", () => {
|
||||||
|
let matrixClient: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
matrixClient = createTestClient();
|
||||||
|
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockResolvedValue(true);
|
||||||
|
// Recovery key is available
|
||||||
|
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue("default key");
|
||||||
|
// Secrets are cached
|
||||||
|
jest.spyOn(matrixClient.getCrypto()!, "getCrossSigningStatus").mockResolvedValue({
|
||||||
|
privateKeysInSecretStorage: true,
|
||||||
|
publicKeysOnDevice: true,
|
||||||
|
privateKeysCachedLocally: {
|
||||||
|
masterKey: true,
|
||||||
|
selfSigningKey: true,
|
||||||
|
userSigningKey: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
return render(<EncryptionUserSettingsTab />, withClientContextRenderOptions(matrixClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should display a loading state when the encryption state is computed", () => {
|
||||||
|
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockImplementation(() => new Promise(() => {}));
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display a verify button when the encryption is not set up", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
jest.spyOn(matrixClient.getCrypto()!, "isCrossSigningReady").mockResolvedValue(false);
|
||||||
|
|
||||||
|
const { asFragment } = renderComponent();
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText("You need to verify this device in order to view your encryption settings."),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
const spy = jest.spyOn(Modal, "createDialog").mockReturnValue({} as any);
|
||||||
|
await user.click(screen.getByText("Verify this device"));
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the recovery panel when the encryption is set up", async () => {
|
||||||
|
renderComponent();
|
||||||
|
await waitFor(() => expect(screen.getByText("Recovery")).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the change recovery key panel when the user clicks on the change recovery button", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const { asFragment } = renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
const button = screen.getByRole("button", { name: "Change recovery key" });
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
user.click(button);
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(screen.getByText("Change recovery key")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the set up recovery key when the user clicks on the set up recovery key button", async () => {
|
||||||
|
jest.spyOn(matrixClient.secretStorage, "getDefaultKeyId").mockResolvedValue(null);
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const { asFragment } = renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
const button = screen.getByRole("button", { name: "Set up recovery" });
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
user.click(button);
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(screen.getByText("Set up recovery")).toBeInTheDocument());
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<EncryptionUserSettingsTab /> should display a verify button when the encryption is not set up 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab mx_EncryptionUserSettingsTab"
|
||||||
|
data-testid="encryptionTab"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab_sections"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection mx_SettingsSection_newUi"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSection_header"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="_typography_yh5dq_162 _font-heading-sm-semibold_yh5dq_102 mx_SettingsHeader"
|
||||||
|
>
|
||||||
|
Device not verified
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubheader"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_SettingsSubheader_error"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
You need to verify this device in order to view your encryption settings.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||||
|
data-kind="primary"
|
||||||
|
data-size="sm"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 18c-.55 0-1.02-.196-1.413-.587A1.926 1.926 0 0 1 2 16V5c0-.55.196-1.02.587-1.413A1.926 1.926 0 0 1 4 3h16c.55 0 1.02.196 1.413.587.39.393.587.863.587 1.413v11c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 20 18H4Zm0-2h16V5H4v11Zm-2 5a.967.967 0 0 1-.712-.288A.968.968 0 0 1 1 20c0-.283.096-.52.288-.712A.967.967 0 0 1 2 19h20c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.712A.968.968 0 0 1 22 21H2Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Verify this device
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<EncryptionUserSettingsTab /> should display the change recovery key panel when the user clicks on the change recovery button 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab mx_EncryptionUserSettingsTab"
|
||||||
|
data-testid="encryptionTab"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab_sections"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<EncryptionUserSettingsTab /> should display the set up recovery key when the user clicks on the set up recovery key button 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab mx_EncryptionUserSettingsTab"
|
||||||
|
data-testid="encryptionTab"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsTab_sections"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||