/* Copyright 2019-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, { type ReactNode } from "react"; import { type GeneratedSas, type ShowSasCallbacks, type Verifier, VerifierEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import { mediaFromMxc } from "../../../customisations/Media"; import VerificationComplete from "../verification/VerificationComplete"; import VerificationCancelled from "../verification/VerificationCancelled"; import BaseAvatar from "../avatars/BaseAvatar"; import Spinner from "../elements/Spinner"; import VerificationShowSas from "../verification/VerificationShowSas"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; const PHASE_START = 0; const PHASE_SHOW_SAS = 1; const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2; const PHASE_VERIFIED = 3; const PHASE_CANCELLED = 4; interface IProps { verifier: Verifier; onFinished(verified?: boolean): void; } interface IState { phase: number; sasVerified: boolean; opponentProfile: { // eslint-disable-next-line camelcase avatar_url?: string; displayname?: string; } | null; opponentProfileError: Error | null; sas: GeneratedSas | null; } export default class IncomingSasDialog extends React.Component { private showSasEvent: ShowSasCallbacks | null; public constructor(props: IProps) { super(props); let phase = PHASE_START; if (this.props.verifier.hasBeenCancelled) { logger.log("Verifier was cancelled in the background."); phase = PHASE_CANCELLED; } this.showSasEvent = null; this.state = { phase: phase, sasVerified: false, opponentProfile: null, opponentProfileError: null, sas: null, }; } public componentDidMount(): void { this.props.verifier.on(VerifierEvent.ShowSas, this.onVerifierShowSas); this.props.verifier.on(VerifierEvent.Cancel, this.onVerifierCancel); this.fetchOpponentProfile(); } public componentWillUnmount(): void { if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { this.props.verifier.cancel(new Error("User cancel")); } this.props.verifier.removeListener(VerifierEvent.ShowSas, this.onVerifierShowSas); } private async fetchOpponentProfile(): Promise { try { const prof = await MatrixClientPeg.safeGet().getProfileInfo(this.props.verifier.userId); this.setState({ opponentProfile: prof, }); } catch (e) { this.setState({ opponentProfileError: e as Error, }); } } private onFinished = (): void => { this.props.onFinished(this.state.phase === PHASE_VERIFIED); }; private onCancelClick = (): void => { this.props.onFinished(this.state.phase === PHASE_VERIFIED); }; private onContinueClick = (): void => { this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM }); this.props.verifier .verify() .then(() => { this.setState({ phase: PHASE_VERIFIED }); }) .catch((e) => { logger.log("Verification failed", e); }); }; private onVerifierShowSas = (e: ShowSasCallbacks): void => { this.showSasEvent = e; this.setState({ phase: PHASE_SHOW_SAS, sas: e.sas, }); }; private onVerifierCancel = (): void => { this.setState({ phase: PHASE_CANCELLED, }); }; private onSasMatchesClick = (): void => { this.showSasEvent?.confirm(); this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, }); }; private onVerifiedDoneClick = (): void => { this.props.onFinished(true); }; private renderPhaseStart(): ReactNode { const isSelf = this.props.verifier.userId === MatrixClientPeg.safeGet().getUserId(); let profile; const oppProfile = this.state.opponentProfile; if (oppProfile) { const url = oppProfile.avatar_url ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile = (

{oppProfile.displayname}

); } else if (this.state.opponentProfileError) { profile = (

{this.props.verifier.userId}

); } else { profile = ; } const userDetailText = [

{_t("encryption|verification|incoming_sas_user_dialog_text_1")}

,

{_t( // NB. Below wording adjusted to singular 'session' until we have // cross-signing "encryption|verification|incoming_sas_user_dialog_text_2", )}

, ]; const selfDetailText = [

{_t("encryption|verification|incoming_sas_device_dialog_text_1")}

,

{_t("encryption|verification|incoming_sas_device_dialog_text_2")}

, ]; return (
{profile} {isSelf ? selfDetailText : userDetailText}
); } private renderPhaseShowSas(): ReactNode { if (!this.showSasEvent) return null; return ( ); } private renderPhaseWaitForPartnerToConfirm(): ReactNode { return (

{_t("encryption|verification|incoming_sas_dialog_waiting")}

); } private renderPhaseVerified(): ReactNode { return ; } private renderPhaseCancelled(): ReactNode { return ; } public render(): ReactNode { let body; switch (this.state.phase) { case PHASE_START: body = this.renderPhaseStart(); break; case PHASE_SHOW_SAS: body = this.renderPhaseShowSas(); break; case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: body = this.renderPhaseWaitForPartnerToConfirm(); break; case PHASE_VERIFIED: body = this.renderPhaseVerified(); break; case PHASE_CANCELLED: body = this.renderPhaseCancelled(); break; } return ( {body} ); } }