// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ChangeEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useState, useRef, } from 'react'; import lodash from 'lodash'; import classNames from 'classnames'; import type { LocalizerType } from '../types/I18N.std.js'; import { FlowingSettingsControl as FlowingControl, SettingsRow, } from './PreferencesUtil.dom.js'; import { ButtonVariant } from './Button.dom.js'; import { getOSAuthErrorString, SIGNAL_BACKUPS_LEARN_MORE_URL, } from './PreferencesBackups.dom.js'; import { I18n } from './I18n.dom.js'; import type { SettingsLocation } from '../types/Nav.std.js'; import { SettingsPage } from '../types/Nav.std.js'; import { ToastType } from '../types/Toast.dom.js'; import type { ShowToastAction } from '../state/ducks/toast.preload.js'; import { Modal } from './Modal.dom.js'; import { strictAssert } from '../util/assert.std.js'; import type { PromptOSAuthReasonType, PromptOSAuthResultType, } from '../util/os/promptOSAuthMain.main.js'; import { ConfirmationDialog } from './ConfirmationDialog.dom.js'; import { AxoButton } from '../axo/AxoButton.dom.js'; const { noop } = lodash; export function PreferencesLocalBackups({ accountEntropyPool, backupKeyViewed, i18n, localBackupFolder, onBackupKeyViewedChange, settingsLocation, pickLocalBackupFolder, promptOSAuth, setSettingsLocation, showToast, }: { accountEntropyPool: string | undefined; backupKeyViewed: boolean; i18n: LocalizerType; localBackupFolder: string | undefined; onBackupKeyViewedChange: (keyViewed: boolean) => void; settingsLocation: SettingsLocation; pickLocalBackupFolder: () => Promise; promptOSAuth: ( reason: PromptOSAuthReasonType ) => Promise; setSettingsLocation: (settingsLocation: SettingsLocation) => void; showToast: ShowToastAction; }): JSX.Element { const [authError, setAuthError] = React.useState>(); const [isAuthPending, setIsAuthPending] = useState(false); if (!localBackupFolder) { return ( ); } const isReferencingBackupKey = settingsLocation.page === SettingsPage.LocalBackupsKeyReference; if (!backupKeyViewed || isReferencingBackupKey) { strictAssert(accountEntropyPool, 'AEP is required for backup key viewer'); return ( { if (backupKeyViewed) { setSettingsLocation({ page: SettingsPage.LocalBackups, }); } else { onBackupKeyViewedChange(true); } }} showToast={showToast} /> ); } const learnMoreLink = (parts: Array) => ( {parts} ); return ( <>
{i18n('icu:Preferences__local-backups-section__description')}
{i18n('icu:Preferences__local-backups-folder__change')}
{ setAuthError(undefined); try { setIsAuthPending(true); const result = await promptOSAuth('view-aep'); if (result === 'success' || result === 'unsupported') { setSettingsLocation({ page: SettingsPage.LocalBackupsKeyReference, }); } else { setAuthError(result); } } finally { setIsAuthPending(false); } }} > {i18n('icu:Preferences__view-key')}
{authError && ( setAuthError(undefined)} cancelButtonVariant={ButtonVariant.Secondary} cancelText={i18n('icu:ok')} > {getOSAuthErrorString(authError) ?? i18n('icu:error')} )} ); } function LocalBackupsSetupFolderPicker({ i18n, pickLocalBackupFolder, }: { i18n: LocalizerType; pickLocalBackupFolder: () => Promise; }): JSX.Element { return (
{i18n('icu:Preferences--local-backups-setup-folder')}
{i18n('icu:Preferences--local-backups-setup-folder-description')}
{i18n('icu:Preferences__button--choose-folder')}
); } type BackupKeyStep = 'view' | 'confirm' | 'caution' | 'reference'; function LocalBackupsBackupKeyViewer({ accountEntropyPool, i18n, isReferencing, onBackupKeyViewed, showToast, }: { accountEntropyPool: string; i18n: LocalizerType; isReferencing: boolean; onBackupKeyViewed: () => void; showToast: ShowToastAction; }): JSX.Element { const [isBackupKeyConfirmed, setIsBackupKeyConfirmed] = useState(false); const [step, setStep] = useState( isReferencing ? 'reference' : 'view' ); const isStepViewOrReference = step === 'view' || step === 'reference'; const backupKey = useMemo(() => { return accountEntropyPool .replace(/\s/g, '') .replace(/.{4}(?=.)/g, '$& ') .toUpperCase(); }, [accountEntropyPool]); const onCopyBackupKey = useCallback( async function handleCopyBackupKey(e: React.MouseEvent) { e.preventDefault(); await window.navigator.clipboard.writeText(backupKey); showToast({ toastType: ToastType.CopiedBackupKey }); }, [backupKey, showToast] ); const learnMoreLink = (parts: Array) => ( {parts} ); let title: string; let description: JSX.Element | string; let footerLeft: JSX.Element | undefined; let footerRight: JSX.Element; if (isStepViewOrReference) { title = i18n('icu:Preferences--local-backups-record-backup-key'); description = ( ); if (step === 'view') { footerRight = ( setStep('confirm')} > {i18n('icu:Preferences--local-backups-setup-next')} ); } else { footerRight = ( {i18n('icu:Preferences--local-backups-view-backup-key-done')} ); } } else { title = i18n('icu:Preferences--local-backups-confirm-backup-key'); description = i18n( 'icu:Preferences--local-backups-confirm-backup-key-description' ); footerLeft = ( setStep('view')} > {i18n('icu:Preferences--local-backups-see-backup-key-again')} ); footerRight = ( setStep('caution')} > {i18n('icu:Preferences--local-backups-setup-next')} ); } return (
{step === 'caution' && ( {i18n( 'icu:Preferences__local-backups-confirm-key-modal-continue' )} } onClose={() => setStep('confirm')} padded={false} >
{i18n('icu:Preferences__local-backups-confirm-key-modal-title')}
{i18n('icu:Preferences__local-backups-confirm-key-modal-body')}
)}
{title}
{description}
setIsBackupKeyConfirmed(isValid)} isStepViewOrReference={isStepViewOrReference} />
{isStepViewOrReference && (
{i18n('icu:Preferences__local-backups-copy-key')}
)}
{footerLeft}
{footerRight}
); } function LocalBackupsBackupKeyTextarea({ backupKey, i18n, onValidate, isStepViewOrReference, }: { backupKey: string; i18n: LocalizerType; onValidate: (isValid: boolean) => void; isStepViewOrReference: boolean; }): JSX.Element { const backupKeyTextareaRef = useRef(null); const [backupKeyInput, setBackupKeyInput] = useState(''); useEffect(() => { if (backupKeyTextareaRef.current) { backupKeyTextareaRef.current.focus(); } }, [backupKeyTextareaRef, isStepViewOrReference]); const backupKeyNoSpaces = React.useMemo(() => { return backupKey.replace(/\s/g, ''); }, [backupKey]); const handleTextareaChange = useCallback( (ev: ChangeEvent) => { const { value } = ev.target; const valueUppercaseNoSpaces = value.replace(/\s/g, '').toUpperCase(); const valueForUI = valueUppercaseNoSpaces.replace(/.{4}(?=.)/g, '$& '); setBackupKeyInput(valueForUI); onValidate(valueUppercaseNoSpaces === backupKeyNoSpaces); }, [backupKeyNoSpaces, onValidate] ); return (