import { observer } from 'mobx-react-lite'; import { toDirective } from '@/components/utils'; import { AppState } from '@/ui_models/app_state'; import { WebApplication } from '@/ui_models/application'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils'; import { storage, StorageKey } from '@Services/localStorage'; import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; import { STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_IMPORT_SUCCESS, STRING_INVALID_IMPORT_FILE, STRING_LOCAL_ENC_ENABLED, STRING_NON_MATCHING_PASSCODES, STRING_UNSUPPORTED_BACKUP_FILE_VERSION, StringImportError, StringUtils } from '@/strings'; import { ApplicationEvent, BackupFile } from '@node_modules/@standardnotes/snjs'; import { JSXInternal } from '@node_modules/preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; import { alertDialog } from '@Services/alertService'; import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; import Authentication from '@/components/AccountMenu/Authentication'; import Footer from '@/components/AccountMenu/Footer'; import User from '@/components/AccountMenu/User'; import Encryption from '@/components/AccountMenu/Encryption'; import Protections from '@/components/AccountMenu/Protections'; import PasscodeLock from '@/components/AccountMenu/PasscodeLock'; type Props = { appState: AppState; application: WebApplication; }; const AccountMenu = observer(({ application, appState }: Props) => { const getProtectionsDisabledUntil = (): string | null => { const protectionExpiry = application.getProtectionSessionExpiryDate(); const now = new Date(); if (protectionExpiry > now) { let f: Intl.DateTimeFormat; if (isSameDay(protectionExpiry, now)) { f = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: 'numeric' }); } else { f = new Intl.DateTimeFormat(undefined, { weekday: 'long', day: 'numeric', month: 'short', hour: 'numeric', minute: 'numeric' }); } return f.format(protectionExpiry); } return null; }; const [showLogin, setShowLogin] = useState(false); const [showRegister, setShowRegister] = useState(false); // const [password, setPassword] = useState(''); // const [passwordConfirmation, setPasswordConfirmation] = useState(undefined); const [encryptionStatusString, setEncryptionStatusString] = useState(undefined); const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); const [server, setServer] = useState(application.getHost()); // const [url, setUrl] = useState(application.getHost()); const [isImportDataLoading, setIsImportDataLoading] = useState(false); const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); const [isBackupEncrypted, setIsBackupEncrypted] = useState(isEncryptionEnabled); const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); const [user, setUser] = useState(application.getUser()); const [hasProtections] = useState(application.hasProtectionSources()); const { notesAndTagsCount } = appState.accountMenu; const errorReportingIdValue = errorReportingId(); const closeAccountMenu = () => { appState.accountMenu.closeAccountMenu(); }; /* const hidePasswordForm = () => { setShowLogin(false); setShowRegister(false); // TODO: Vardan: check whether the uncommented parts below don't brake anything // (I commented them on trying to move those 2 setters into `Authentication` component) // setPassword(''); // setPasswordConfirmation(undefined); }; */ const downloadDataArchive = () => { application.getArchiveService().downloadBackup(isBackupEncrypted); }; const readFile = async (file: File): Promise => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target!.result as string); resolve(data); } catch (e) { application.alertService.alert(STRING_INVALID_IMPORT_FILE); } }; reader.readAsText(file); }); }; const performImport = async (data: BackupFile) => { setIsImportDataLoading(true); const result = await application.importData(data); setIsImportDataLoading(false); if (!result) { return; } let statusText = STRING_IMPORT_SUCCESS; if ('error' in result) { statusText = result.error; } else if (result.errorCount) { statusText = StringImportError(result.errorCount); } void alertDialog({ text: statusText }); }; const importFileSelected = async (event: TargetedEvent) => { const { files } = (event.target as HTMLInputElement); if (!files) { return; } const file = files[0]; const data = await readFile(file); if (!data) { return; } const version = data.version || data.keyParams?.version || data.auth_params?.version; if (!version) { await performImport(data); return; } if ( application.protocolService.supportedVersions().includes(version) ) { await performImport(data); } else { setIsImportDataLoading(false); void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION }); } }; const toggleErrorReportingEnabled = () => { if (isErrorReportingEnabled) { disableErrorReporting(); } else { enableErrorReporting(); } if (!appState.sync.inProgress) { window.location.reload(); } }; const openErrorReportingDialog = () => { alertDialog({ title: 'Data sent during automatic error reporting', text: ` We use Bugsnag to automatically report errors that occur while the app is running. See this article, paragraph 'Browser' under 'Sending diagnostic data', to see what data is included in error reports.

Error reports never include IP addresses and are fully anonymized. We use error reports to be alerted when something in our code is causing unexpected errors and crashes in your application experience. ` }); }; // Add the required event observers useEffect(() => { const removeKeyStatusChangedObserver = application.addEventObserver( async () => { setUser(application.getUser()); }, ApplicationEvent.KeyStatusChanged ); const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( async () => { setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }, ApplicationEvent.ProtectionSessionExpiryDateChanged ); return () => { removeKeyStatusChangedObserver(); removeProtectionSessionExpiryDateChangedObserver(); }; }, []); // TODO:fix dependency list (should they left empty?) return (
Account
Close
{!showLogin && !showRegister && (
{user && ( )} {hasProtections && ( )} {isImportDataLoading ? (
) : (
Data Backups
Download a backup of all your data.
{isEncryptionEnabled && (
)}
{isDesktopApplication() && (

Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.

)}
)}
Error Reporting
Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'}

Help us improve Standard Notes by automatically submitting anonymized error reports.

{errorReportingIdValue && ( <>

Your random identifier is strong {errorReportingIdValue}

Disabling error reporting will remove that identifier from your local storage, and a new identifier will be created should you decide to enable error reporting again in the future.

)}
)}
); }); export const AccountMenuDirective = toDirective( AccountMenu );