2025-01-15 13:44:20 +01:00
/*
* 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" ;
2025-01-24 09:33:16 +01:00
import { Button , InlineSpinner , Separator } from "@vector-im/compound-web" ;
2025-01-15 13:44:20 +01:00
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" ;
2025-01-24 09:33:16 +01:00
import { AdvancedPanel } from "../../encryption/AdvancedPanel" ;
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel" ;
2025-02-03 14:47:55 +01:00
import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync" ;
2025-01-15 13:44:20 +01:00
/**
* 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.
2025-02-04 17:40:31 +00:00
* - "reset_identity_compromised": The panel to show when the user is resetting their identity, in te case where their key is compromised.
* - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key.
* - `secrets_not_cached`: The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
2025-02-03 14:47:55 +01:00
* If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails.
2025-01-15 13:44:20 +01:00
*/
2025-02-04 17:40:31 +00:00
export type State =
2025-02-03 14:47:55 +01:00
| "loading"
| "main"
| "set_up_encryption"
| "change_recovery_key"
| "set_recovery_key"
2025-02-04 17:40:31 +00:00
| "reset_identity_compromised"
| "reset_identity_forgot"
2025-02-03 14:47:55 +01:00
| "secrets_not_cached" ;
2025-01-15 13:44:20 +01:00
2025-02-04 17:40:31 +00:00
interface EncryptionUserSettingsTabProps {
/**
* If the tab should start in a state other than the deasult
*/
initialState? : State ;
}
/**
* The encryption settings tab.
*/
export function EncryptionUserSettingsTab ({ initialState = "loading" } : EncryptionUserSettingsTabProps ) : JSX . Element {
const [ state , setState ] = useState < State >( initialState );
const checkEncryptionState = useCheckEncryptionState ( state , setState );
2025-01-15 13:44:20 +01:00
let content : JSX.Element ;
switch ( state ) {
case "loading" :
content = < InlineSpinner aria-label = { _t ( "common|loading" )} />;
break ;
case "set_up_encryption" :
2025-02-03 14:47:55 +01:00
content = < SetUpEncryptionPanel onFinish = { checkEncryptionState } />;
break ;
case "secrets_not_cached" :
content = < RecoveryPanelOutOfSync onFinish = { checkEncryptionState } />;
2025-01-15 13:44:20 +01:00
break ;
case "main" :
content = (
2025-01-24 09:33:16 +01:00
<>
< RecoveryPanel
onChangeRecoveryKeyClick = {( setupNewKey ) =>
setupNewKey ? setState ( "set_recovery_key" ) : setState ( "change_recovery_key" )
}
/>
< Separator kind = "section" />
2025-02-04 17:40:31 +00:00
< AdvancedPanel onResetIdentityClick = {() => setState ( "reset_identity_compromised" )} />
2025-01-24 09:33:16 +01:00
</>
2025-01-15 13:44:20 +01:00
);
break ;
case "change_recovery_key" :
case "set_recovery_key" :
content = (
< ChangeRecoveryKey
userHasRecoveryKey = { state === "change_recovery_key" }
onCancelClick = {() => setState ( "main" )}
onFinish = {() => setState ( "main" )}
/>
);
break ;
2025-02-04 17:40:31 +00:00
case "reset_identity_compromised" :
content = (
< ResetIdentityPanel
variant = "compromised"
onCancelClick = {() => setState ( "main" )}
onFinish = {() => setState ( "main" )}
/>
);
break ;
case "reset_identity_forgot" :
content = (
< ResetIdentityPanel
variant = "forgot"
onCancelClick = {() => setState ( "main" )}
onFinish = {() => setState ( "main" )}
/>
);
2025-01-24 09:33:16 +01:00
break ;
2025-01-15 13:44:20 +01:00
}
return (
< SettingsTab className = "mx_EncryptionUserSettingsTab" data-testid = "encryptionTab" >
{ content }
</ SettingsTab >
);
}
/**
2025-02-03 14:47:55 +01:00
* Hook to check if the user needs:
* - to go through the SetupEncryption flow.
* - to enter their recovery key, if the secrets are not cached locally.
*
2025-01-15 13:44:20 +01:00
* If the user needs to set up the encryption, the state will be set to "set_up_encryption".
2025-02-03 14:47:55 +01:00
* If the user secrets are not cached, the state will be set to "secrets_not_cached".
2025-01-15 13:44:20 +01:00
* 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.
*/
2025-02-04 17:40:31 +00:00
function useCheckEncryptionState ( state : State , setState : ( state : State ) => void ) : () => Promise < void > {
2025-01-15 13:44:20 +01:00
const matrixClient = useMatrixClientContext ();
2025-02-03 14:47:55 +01:00
const checkEncryptionState = useCallback ( async () => {
2025-01-15 13:44:20 +01:00
const crypto = matrixClient . getCrypto () ! ;
const isCrossSigningReady = await crypto . isCrossSigningReady ();
2025-02-03 14:47:55 +01:00
// Check if the secrets are cached
const cachedSecrets = ( await crypto . getCrossSigningStatus ()). privateKeysCachedLocally ;
const secretsOk = cachedSecrets . masterKey && cachedSecrets . selfSigningKey && cachedSecrets . userSigningKey ;
if ( isCrossSigningReady && secretsOk ) setState ( "main" );
else if ( ! isCrossSigningReady ) setState ( "set_up_encryption" );
else setState ( "secrets_not_cached" );
2025-01-15 13:44:20 +01:00
}, [ matrixClient , setState ]);
// Initialise the state when the component is mounted
useEffect (() => {
2025-02-04 17:40:31 +00:00
if ( state === "loading" ) checkEncryptionState ();
}, [ checkEncryptionState , state ]);
2025-01-15 13:44:20 +01:00
// Also return the callback so that the component can re-run the logic.
2025-02-03 14:47:55 +01:00
return checkEncryptionState ;
2025-01-15 13:44:20 +01:00
}
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 >
);
}