Periodically nag the user if their device remains unverified (#33346)
* nag the user to verify their device 2 days after they dismiss the toast * lint * recheck doesn't need to be awaited * call setDeviceState
This commit is contained in:
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type LogSpan, type BaseLogger, type Logger } from "matrix-js-sdk/src/logger";
|
||||
import { LogSpan, type BaseLogger, type Logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
type MatrixEvent,
|
||||
type MatrixClient,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
RoomStateEvent,
|
||||
ClientEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||
|
||||
import { type DeviceListener, type DeviceState } from ".";
|
||||
import {
|
||||
@@ -40,6 +41,11 @@ export const BACKUP_DISABLED_ACCOUNT_DATA_KEY = "m.org.matrix.custom.backup_disa
|
||||
*/
|
||||
export const RECOVERY_ACCOUNT_DATA_KEY = "io.element.recovery";
|
||||
|
||||
/**
|
||||
* We remind the user to verify their device 2 days after they dismiss the toast.
|
||||
*/
|
||||
const DEVICE_VERIFICATION_NAG_INTERVAL = 2 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Handles all of DeviceListener's work that relates to the current device.
|
||||
*/
|
||||
@@ -124,6 +130,19 @@ export class DeviceListenerCurrentDevice {
|
||||
* them again until they refresh or restart the app.
|
||||
*/
|
||||
public dismissEncryptionSetup(): void {
|
||||
// If the user dismissed the "verify this session" toast, then we will
|
||||
// re-show it later if the device still isn't verified.
|
||||
if (this.deviceState === "verify_this_session") {
|
||||
setTimeout(() => {
|
||||
if (this.deviceState === "verify_this_session") {
|
||||
const logSpan = new LogSpan(this.logger, "nag_" + secureRandomString(4));
|
||||
logSpan.info("Re-showing device verification toast");
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.setDeviceState("verify_this_session", logSpan);
|
||||
}
|
||||
}, DEVICE_VERIFICATION_NAG_INTERVAL);
|
||||
}
|
||||
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this.deviceListener.recheck();
|
||||
}
|
||||
@@ -218,7 +237,7 @@ export class DeviceListenerCurrentDevice {
|
||||
} else {
|
||||
// Everything is OK - no need to show a toast
|
||||
logSpan.info("No toast needed");
|
||||
await this.setDeviceState("ok", logSpan);
|
||||
this.setDeviceState("ok", logSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,14 +271,14 @@ export class DeviceListenerCurrentDevice {
|
||||
logSpan.warn(fullMessage, ...logItems);
|
||||
}
|
||||
|
||||
await this.setDeviceState(newState, logSpan);
|
||||
this.setDeviceState(newState, logSpan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the device, and perform any actions necessary in
|
||||
* response to the state changing.
|
||||
*/
|
||||
private async setDeviceState(newState: DeviceState, logSpan: LogSpan): Promise<void> {
|
||||
private setDeviceState(newState: DeviceState, logSpan: LogSpan): void {
|
||||
this.deviceState = newState;
|
||||
|
||||
this.deviceListener.currentDeviceChangedEmitter.onStateChanged(newState);
|
||||
|
||||
@@ -381,6 +381,50 @@ describe("DeviceListener", () => {
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-shows toast after two days", async () => {
|
||||
const instance = await createAndStart();
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
|
||||
// 1.5 days after the toast was dismissed, we don't re-show the
|
||||
// toast yet.
|
||||
jest.advanceTimersByTime(1.5 * 24 * 60 * 60 * 1000);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledTimes(1);
|
||||
|
||||
// 2 days after the toast was dismissed, we re-show the toast.
|
||||
jest.advanceTimersByTime(0.5 * 24 * 60 * 60 * 1000);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledTimes(2);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("doesn't re-show toast if the device is now verified", async () => {
|
||||
const instance = await createAndStart();
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
instance.dismissEncryptionSetup();
|
||||
await flushPromises();
|
||||
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
|
||||
|
||||
// If the device becomes verified before the end of two days, we
|
||||
// don't re-show the toast.
|
||||
mockCrypto!.getDeviceVerificationStatus.mockResolvedValue(
|
||||
new DeviceVerificationStatus({
|
||||
trustCrossSignedDevices: true,
|
||||
crossSigningVerified: true,
|
||||
}),
|
||||
);
|
||||
instance.recheck();
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(2 * 24 * 60 * 60 * 1000);
|
||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("does not show any toasts when secret storage is being accessed", async () => {
|
||||
mocked(isSecretStorageBeingAccessed).mockReturnValue(true);
|
||||
await createAndStart();
|
||||
|
||||
Reference in New Issue
Block a user