mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-02 20:38:52 +00:00
Apply new design and display logic to logout confirmation dialog (#33426)
* apply new design to logout dialog * factor out check for other verified devices * only show recovery warning when user has no other verified devices * fix playwright tests * tweak style to better match design * another playwright test fix * fix playwright * Look for the remove button within the dialog * Use testid to locate 'Remove this device' button * move rendering to sub-components, rather than embedded functions * use <Type> element * use <Text> for the <a> element --------- Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
@@ -7,11 +7,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.
|
||||
*/
|
||||
|
||||
import React, { lazy } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import React, { type JSX } from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Button, Text } from "@vector-im/compound-web";
|
||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
||||
import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||
import SignOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/sign-out";
|
||||
|
||||
import Modal from "../../../Modal";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
@@ -21,39 +24,16 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import QuestionDialog from "./QuestionDialog";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { BackupStatus, useKeyBackupStatus } from "../../../hooks/useKeyBackupStatus";
|
||||
import { useHasOtherVerifiedDevices } from "../../../hooks/useHasOtherVerifiedDevices";
|
||||
import { EncryptionCard } from "../settings/encryption/EncryptionCard";
|
||||
import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButtons";
|
||||
import { EncryptionCardEmphasisedContent } from "../settings/encryption/EncryptionCardEmphasisedContent";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
enum BackupStatus {
|
||||
/** we're trying to figure out if there is an active backup */
|
||||
LOADING,
|
||||
|
||||
/** crypto is disabled in this client (so no need to back up) */
|
||||
NO_CRYPTO,
|
||||
|
||||
/** Key backup is active and working */
|
||||
BACKUP_ACTIVE,
|
||||
|
||||
/** there is a backup on the server but we are not backing up to it */
|
||||
SERVER_BACKUP_BUT_DISABLED,
|
||||
|
||||
/** Key backup is set up but recovery (4s) is not */
|
||||
BACKUP_NO_RECOVERY,
|
||||
|
||||
/** backup is not set up locally and there is no backup on the server */
|
||||
NO_BACKUP,
|
||||
|
||||
/** there was an error fetching the state */
|
||||
ERROR,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
backupStatus: BackupStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the `LogoutDialog` should be shown instead of the simple logout flow.
|
||||
* The `LogoutDialog` will check the crypto recovery status of the account and
|
||||
@@ -61,191 +41,122 @@ interface IState {
|
||||
*/
|
||||
export async function shouldShowLogoutDialog(cli: MatrixClient): Promise<boolean> {
|
||||
const crypto = cli?.getCrypto();
|
||||
if (!crypto) return false;
|
||||
|
||||
// If any room is encrypted, we need to show the advanced logout flow
|
||||
const allRooms = cli!.getRooms();
|
||||
for (const room of allRooms) {
|
||||
const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId);
|
||||
if (isE2e) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !!crypto;
|
||||
}
|
||||
|
||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
onFinished: function () {},
|
||||
};
|
||||
export default function LogoutDialog(props: IProps): JSX.Element {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const backupStatus = useKeyBackupStatus(client);
|
||||
const hasOtherVerifiedDevices = useHasOtherVerifiedDevices(client);
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
backupStatus: BackupStatus.LOADING,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.startLoadBackupStatus();
|
||||
}
|
||||
|
||||
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */
|
||||
private startLoadBackupStatus(): void {
|
||||
this.loadBackupStatus().catch((e) => {
|
||||
logger.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
backupStatus: BackupStatus.ERROR,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadBackupStatus(): Promise<void> {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) {
|
||||
this.setState({ backupStatus: BackupStatus.NO_CRYPTO });
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await crypto.getActiveSessionBackupVersion()) !== null) {
|
||||
if (await crypto.isSecretStorageReady()) {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
|
||||
} else {
|
||||
this.setState({ backupStatus: BackupStatus.BACKUP_NO_RECOVERY });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// backup is not active. see if there is a backup version on the server we ought to back up to.
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
|
||||
}
|
||||
|
||||
private onExportE2eKeysClicked = (): void => {
|
||||
Modal.createDialog(
|
||||
lazy(() => import("../../../async-components/views/dialogs/security/ExportE2eKeysDialog")),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.safeGet(),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
private onFinished = (confirmed?: boolean): void => {
|
||||
const onFinished = (confirmed?: boolean): void => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({ action: "logout" });
|
||||
}
|
||||
// close dialog
|
||||
this.props.onFinished(!!confirmed);
|
||||
props.onFinished(!!confirmed);
|
||||
};
|
||||
|
||||
private onSetRecoveryMethodClick = (): void => {
|
||||
// Open the user settings dialog to the encryption tab and start the flow to reset encryption
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
};
|
||||
dis.dispatch(payload);
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
private onLogoutConfirm = (): void => {
|
||||
const onLogoutConfirm = (): void => {
|
||||
dis.dispatch({ action: "logout" });
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished(true);
|
||||
props.onFinished(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show a dialog prompting the user to set up their recovery method.
|
||||
*
|
||||
* Either:
|
||||
* * There is no backup at all ({@link BackupStatus.NO_BACKUP})
|
||||
* * There is a backup set up but recovery (4s) is not ({@link BackupStatus.BACKUP_NO_RECOVERY})
|
||||
* * There is a backup on the server but we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED})
|
||||
* * We were unable to pull the backup data ({@link BackupStatus.ERROR}).
|
||||
*
|
||||
* In all four cases, we should prompt the user to set up a method of recovery.
|
||||
*/
|
||||
private renderSetupRecoveryMethod(): React.ReactNode {
|
||||
const description = (
|
||||
<div>
|
||||
<p>{_t("auth|logout_dialog|setup_secure_backup_description_1")}</p>
|
||||
<p>{_t("auth|logout_dialog|setup_secure_backup_description_2")}</p>
|
||||
<p>{_t("encryption|setup_secure_backup|explainer")}</p>
|
||||
</div>
|
||||
);
|
||||
const onGoToSettings = (): void => {
|
||||
// Open the user settings dialog to the encryption tab and start the flow to get recovery key
|
||||
const payload: OpenToTabPayload = {
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Encryption,
|
||||
props: {
|
||||
initialEncryptionState: "set_recovery_key",
|
||||
},
|
||||
};
|
||||
dis.dispatch(payload);
|
||||
|
||||
const dialogContent = (
|
||||
<div>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{description}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("common|go_to_settings")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this.onLogoutConfirm}>{_t("auth|logout_dialog|skip_key_backup")}</button>
|
||||
</DialogButtons>
|
||||
<details>
|
||||
<summary className="mx_LogoutDialog_ExportKeyAdvanced">{_t("common|advanced")}</summary>
|
||||
<p>
|
||||
<button onClick={this.onExportE2eKeysClicked}>{_t("auth|logout_dialog|megolm_export")}</button>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
// Not quite a standard question dialog as the primary button cancels
|
||||
// the action and does something else instead, whilst non-default button
|
||||
// confirms the action.
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("auth|logout_dialog|setup_key_backup_title")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
>
|
||||
{dialogContent}
|
||||
</BaseDialog>
|
||||
);
|
||||
props.onFinished(false);
|
||||
};
|
||||
|
||||
if (hasOtherVerifiedDevices === undefined) {
|
||||
return <Loading onFinished={onFinished} />;
|
||||
} else if (hasOtherVerifiedDevices) {
|
||||
return <ConfirmLogout onFinished={onFinished} />;
|
||||
}
|
||||
switch (backupStatus) {
|
||||
case BackupStatus.LOADING:
|
||||
return <Loading onFinished={onFinished} />;
|
||||
|
||||
public render(): React.ReactNode {
|
||||
switch (this.state.backupStatus) {
|
||||
case BackupStatus.LOADING:
|
||||
// while we're deciding if we have backups, show a spinner
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("action|sign_out")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={this.onFinished}
|
||||
case BackupStatus.NO_CRYPTO:
|
||||
case BackupStatus.BACKUP_ACTIVE:
|
||||
return <ConfirmLogout onFinished={onFinished} />;
|
||||
|
||||
case BackupStatus.NO_BACKUP:
|
||||
case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
|
||||
case BackupStatus.ERROR:
|
||||
case BackupStatus.BACKUP_NO_RECOVERY: {
|
||||
return (
|
||||
<BaseDialog
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={onFinished}
|
||||
className="mx_LogoutDialog"
|
||||
>
|
||||
<EncryptionCard
|
||||
Icon={ErrorIcon}
|
||||
destructive={true}
|
||||
title={_t("auth|logout_dialog|setup_key_backup_title")}
|
||||
className="mx_EncryptionCard_noBorder"
|
||||
>
|
||||
<Spinner />
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
case BackupStatus.NO_CRYPTO:
|
||||
case BackupStatus.BACKUP_ACTIVE:
|
||||
return (
|
||||
<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("action|sign_out")}
|
||||
description={_t("auth|logout_dialog|description")}
|
||||
button={_t("action|sign_out")}
|
||||
onFinished={this.onFinished}
|
||||
/>
|
||||
);
|
||||
|
||||
case BackupStatus.NO_BACKUP:
|
||||
case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
|
||||
case BackupStatus.ERROR:
|
||||
case BackupStatus.BACKUP_NO_RECOVERY:
|
||||
return this.renderSetupRecoveryMethod();
|
||||
<EncryptionCardEmphasisedContent>
|
||||
<Text>{_t("auth|logout_dialog|setup_secure_backup_description")}</Text>
|
||||
<Text as="a" target="_blank" href="https://element.io/en/help#encryption16">
|
||||
{_t("action|learn_more")} <PopOutIcon />
|
||||
</Text>
|
||||
</EncryptionCardEmphasisedContent>
|
||||
<EncryptionCardButtons>
|
||||
<Button onClick={onGoToSettings} Icon={KeyIcon}>
|
||||
{_t("settings|encryption|recovery|set_up_recovery")}
|
||||
</Button>
|
||||
<Button kind="tertiary" destructive={true} onClick={onLogoutConfirm} Icon={SignOutIcon}>
|
||||
{_t("auth|logout_dialog|skip_key_backup")}
|
||||
</Button>
|
||||
</EncryptionCardButtons>
|
||||
</EncryptionCard>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SubComponentProps {
|
||||
onFinished: (confirmed?: boolean) => void;
|
||||
}
|
||||
|
||||
// Dialog contents to show a spinner while deciding whether to prompt the
|
||||
// user to set up recovery
|
||||
function Loading(props: SubComponentProps): JSX.Element {
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("action|sign_out")}
|
||||
contentId="mx_Dialog_content"
|
||||
hasCancel={true}
|
||||
onFinished={props.onFinished}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog contents to confirm whether the user is sure if they want to log
|
||||
// out.
|
||||
function ConfirmLogout(props: SubComponentProps): JSX.Element {
|
||||
return (
|
||||
<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("action|sign_out")}
|
||||
description={_t("auth|logout_dialog|description")}
|
||||
button={_t("action|sign_out")}
|
||||
onFinished={props.onFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user