import { ShowQrCodeCallbacks, ShowSasCallbacks, VerificationPhase, VerificationRequest, Verifier, } from 'matrix-js-sdk/lib/crypto-api'; import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { VerificationMethod } from 'matrix-js-sdk/lib/types'; import QRCode from 'qrcode'; import { Box, Button, config, Dialog, Header, Icon, IconButton, Icons, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, } from 'folds'; import FocusTrap from 'focus-trap-react'; import { useVerificationRequestPhase, useVerificationRequestReceived, useVerifierCancel, useVerifierShowReciprocateQr, useVerifierShowSas, } from '../hooks/useVerificationRequest'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { ContainerColor } from '../styles/ContainerColor.css'; import { useModalStyle } from '../hooks/useModalStyle'; import { QrScanner } from './QrScanner'; const DialogHeaderStyles: CSSProperties = { padding: `0 ${config.space.S200} 0 ${config.space.S400}`, borderBottomWidth: config.borderWidth.B300, }; type WaitingMessageProps = { message: string; }; function WaitingMessage({ message }: WaitingMessageProps) { return ( {message} ); } type VerificationUnexpectedProps = { message: string; onClose: () => void }; function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) { const { t } = useTranslation(); return ( {message} {t('Organisms.DeviceVerification.close')} ); } function VerificationWaitAccept() { const { t } = useTranslation(); return ( {t('Organisms.DeviceVerification.please_accept')} ); } type VerificationAcceptProps = { onAccept: () => Promise; }; function VerificationAccept({ onAccept }: VerificationAcceptProps) { const { t } = useTranslation(); const [acceptState, accept] = useAsyncCallback(onAccept); const accepting = acceptState.status === AsyncStatus.Loading; return ( {t('Organisms.DeviceVerification.click_accept')} } disabled={accepting} > {t('Organisms.DeviceVerification.accept')} ); } function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) { const { t } = useTranslation(); const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData])); const confirming = confirmState.status === AsyncStatus.Loading || confirmState.status === AsyncStatus.Success; return ( {t('Organisms.DeviceVerification.confirm_emoji')} {sasData.sas.emoji?.map(([emoji, name], index) => ( {emoji} {name} ))} } > {t('Organisms.DeviceVerification.they_match')} sasData.mismatch()} disabled={confirming} > {t('Organisms.DeviceVerification.do_not_match')} ); } type SasVerificationProps = { verifier: Verifier; onCancel: () => void; }; function SasVerification({ verifier, onCancel }: SasVerificationProps) { const { t } = useTranslation(); const [sasData, setSasData] = useState(); useVerifierShowSas(verifier, setSasData); useVerifierCancel(verifier, onCancel); useEffect(() => { verifier.verify(); }, [verifier]); if (sasData) { return ; } return ( ); } type VerificationDoneProps = { onExit: () => void; }; function VerificationDone({ onExit }: VerificationDoneProps) { const { t } = useTranslation(); return ( {t('Organisms.DeviceVerification.device_verified')} {t('Organisms.DeviceVerification.okay')} ); } type VerificationCanceledProps = { onClose: () => void; }; function VerificationCanceled({ onClose }: VerificationCanceledProps) { const { t } = useTranslation(); return ( {t('Organisms.DeviceVerification.verification_canceled')} {t('Organisms.DeviceVerification.close')} ); } function QrCodeImage({ data }: { data: Uint8ClampedArray }) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Byte-mode so the raw verification bytes round-trip (a string value would // mangle high bytes via UTF-8). QRCode.toCanvas(canvas, [{ data: new Uint8Array(data), mode: 'byte' }], { width: 220, margin: 2, color: { dark: '#000000', light: '#ffffff' }, }).catch(() => undefined); }, [data]); return ( ); } type VerificationReadyProps = { request: VerificationRequest; onStartSas: () => void; onScanned: (bytes: Uint8ClampedArray) => void; }; function VerificationReady({ request, onStartSas, onScanned }: VerificationReadyProps) { const [myQr, setMyQr] = useState(); const [scanning, setScanning] = useState(false); const canShowMine = request.otherPartySupportsMethod(VerificationMethod.ScanQrCode); const canScanTheirs = request.otherPartySupportsMethod(VerificationMethod.ShowQrCode); useEffect(() => { if (!canShowMine) return; request .generateQRCode() .then((bytes) => { if (bytes) setMyQr(bytes); }) .catch(() => undefined); }, [request, canShowMine]); if (scanning) { return setScanning(false)} />; } return ( {myQr && ( Scan this code with your other device to verify. )} {canScanTheirs && ( setScanning(true)}> Scan their QR code )} Verify with emoji instead ); } type ReciprocateVerificationProps = { verifier: Verifier; onCancel: () => void; }; function ReciprocateVerification({ verifier, onCancel }: ReciprocateVerificationProps) { const [qrCallbacks, setQrCallbacks] = useState(); const [confirmState, confirm] = useAsyncCallback( useCallback(async () => qrCallbacks?.confirm(), [qrCallbacks]), ); useVerifierShowReciprocateQr(verifier, setQrCallbacks); useVerifierCancel(verifier, onCancel); const confirming = confirmState.status === AsyncStatus.Loading || confirmState.status === AsyncStatus.Success; // The showing side gets ShowReciprocateQr callbacks after the other device // scans; the scanning side never does (it already called verify()) and just // waits for completion. if (!qrCallbacks) { return ( ); } return ( The other device scanned this code. Confirm it now shows as verified. Confirm qrCallbacks.cancel()} disabled={confirming} > Cancel ); } type DeviceVerificationProps = { request: VerificationRequest; onExit: () => void; }; export function DeviceVerification({ request, onExit }: DeviceVerificationProps) { const phase = useVerificationRequestPhase(request); const modalStyle = useModalStyle(480); const handleCancel = useCallback(() => { if (request.phase !== VerificationPhase.Done && request.phase !== VerificationPhase.Cancelled) { request.cancel(); } onExit(); }, [request, onExit]); const handleAccept = useCallback(() => request.accept(), [request]); const handleStart = useCallback(async () => { await request.startVerification(VerificationMethod.Sas); }, [request]); const handleScanned = useCallback( async (bytes: Uint8ClampedArray) => { try { const verifier = await request.scanQRCode(bytes); await verifier.verify(); } catch { // A bad/mismatched scan cancels the request; the Cancelled phase renders. } }, [request], ); return ( }> Device Verification {phase === VerificationPhase.Requested && (request.initiatedByMe ? ( ) : ( ))} {phase === VerificationPhase.Ready && ( )} {phase === VerificationPhase.Started && (request.verifier ? ( request.chosenMethod === VerificationMethod.Reciprocate ? ( ) : ( ) ) : ( ))} {phase === VerificationPhase.Done && } {phase === VerificationPhase.Cancelled && ( )} ); } export function ReceiveSelfDeviceVerification() { const [request, setRequest] = useState(); useVerificationRequestReceived(setRequest); const handleExit = useCallback(() => { setRequest(undefined); }, []); if (!request) return null; return ; }