import React, { useCallback, useEffect, useState } from 'react'; import { useAtomValue, useSetAtom } from 'jotai'; import { SyncState } from 'matrix-js-sdk'; import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api'; import { Icons } from 'folds'; import { ErrorBoundary } from 'react-error-boundary'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useCrossSigningActive } from '../../hooks/useCrossSigning'; import { useDeviceVerificationStatus, VerificationStatus, } from '../../hooks/useDeviceVerificationStatus'; import { useKeyBackupInfo, useKeyBackupStatusChange } from '../../hooks/useKeyBackup'; import { useAlive } from '../../hooks/useAlive'; import { useSyncState } from '../../hooks/useSyncState'; import { settingsRequestAtom } from '../../state/settingsRequest'; import { serverStatusAtom } from '../../state/serverStatus'; import { pickBanner } from '../../utils/kumaStatus'; import { dismissNudge, NUDGE_COPY, NUDGE_KEY_PREFIX, nudgeFor, NudgeRecord, parseNudgeRecord, shouldShowNudge, } from '../../utils/securityNudge'; import { StatusStrip } from '../server-status/ServerStatusBanner'; /** This device's backup connection: undefined until known, then true/false. */ const useBackupActive = (crypto: CryptoApi): boolean | undefined => { const alive = useAlive(); const [active, setActive] = useState(); useEffect(() => { crypto.getActiveSessionBackupVersion().then((v) => { if (alive()) setActive(typeof v === 'string'); }); }, [crypto, alive]); useKeyBackupStatusChange(useCallback((enabled: boolean) => setActive(enabled), [])); return active; }; const readRecord = (key: string): NudgeRecord => { let raw: string | null = null; try { raw = localStorage.getItem(key); } catch { /* storage unavailable */ } const record = parseNudgeRecord(raw, Date.now()); if (!raw) { try { localStorage.setItem(key, JSON.stringify(record)); } catch { /* best effort */ } } return record; }; function SecurityBannerFor({ crypto }: { crypto: CryptoApi }) { const mx = useMatrixClient(); const deviceId = mx.getDeviceId() ?? undefined; const crossSigning = useCrossSigningActive(); const status = useDeviceVerificationStatus(crypto, mx.getSafeUserId(), deviceId); const backupInfo = useKeyBackupInfo(crypto); const backupActive = useBackupActive(crypto); const { status: serverStatus, syncLost } = useAtomValue(serverStatusAtom); const requestSettings = useSetAtom(settingsRequestAtom); // Not urgent: wait until sync has settled (two SYNCING in a row), so it // never stacks under the "Connecting…" strip at startup. const [settled, setSettled] = useState(false); useSyncState( mx, useCallback((state: SyncState | null, prev?: SyncState | null) => { setSettled(state === SyncState.Syncing && prev === SyncState.Syncing); }, []), ); const key = `${NUDGE_KEY_PREFIX}${deviceId ?? 'unknown'}`; const [record, setRecord] = useState(() => readRecord(key)); let deviceVerified: boolean | undefined; if (status === VerificationStatus.Verified) deviceVerified = true; else if (status === VerificationStatus.Unverified) deviceVerified = false; const kind = nudgeFor({ crossSigning, deviceVerified, backupOnServer: backupInfo === undefined ? undefined : backupInfo !== null, backupActive, }); // One strip at a time: outages, maintenance and a lost connection win. const otherStrip = syncLost || !!pickBanner(serverStatus, { syncLost, where: 'client' }); if (!settled || !deviceId || !kind || otherStrip || !shouldShowNudge(kind, record, Date.now())) return null; const copy = NUDGE_COPY[kind]; const dismiss = () => { const next = dismissNudge(kind, record, Date.now()); try { localStorage.setItem(key, JSON.stringify(next)); } catch { /* dismissed for this session only */ } setRecord(next); }; return ( requestSettings('devices') }} dismissLabel="Not now" onDismiss={dismiss} /> ); } /** * [Gitea #110, #123] The device-security nudge: verify this device, connect * it to the key backup, or set up key backup. See utils/securityNudge.ts. */ function SecurityBannerInner() { const mx = useMatrixClient(); const crypto = mx.getCrypto(); if (!crypto) return null; return ; } // A nudge must never take the app down: any error just hides it. export function SecurityBanner() { return ( ); }