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_ACCOUNT_MENU_UNCHECK_MERGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_GENERATING_LOGIN_KEYS, STRING_GENERATING_REGISTER_KEYS, STRING_IMPORT_SUCCESS, STRING_INVALID_IMPORT_FILE, STRING_LOCAL_ENC_ENABLED, STRING_NON_MATCHING_PASSCODES, STRING_NON_MATCHING_PASSWORDS, STRING_UNSUPPORTED_BACKUP_FILE_VERSION, StringImportError, StringUtils } from '@/strings'; import { ApplicationEvent, BackupFile } from '@node_modules/@standardnotes/snjs'; import { PasswordWizardType } from '@/types'; import { JSXInternal } from '@node_modules/preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; import { alertDialog, confirmDialog } from '@Services/alertService'; import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; 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 passcodeInputRef = useRef(); const emailInputRef = useRef(); const passwordInputRef = useRef(); const passwordConfirmationInputRef = useRef(); const [showLogin, setShowLogin] = useState(false); const [showRegister, setShowRegister] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [passwordConfirmation, setPasswordConfirmation] = useState(undefined); const [status, setStatus] = useState(undefined); const [isEphemeral, setIsEphemeral] = useState(false); const [isStrictSignIn, setIsStrictSignIn] = useState(false); const [passcode, setPasscode] = useState(undefined); const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); const [encryptionStatusString, setEncryptionStatusString] = useState(undefined); const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); const [shouldMergeLocal, setShouldMergeLocal] = useState(true); const [server, setServer] = useState(application.getHost()); const [url, setUrl] = useState(application.getHost()); const [showPasscodeForm, setShowPasscodeForm] = useState(false); const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); const [isImportDataLoading, setIsImportDataLoading] = useState(false); const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`); const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); const [isBackupEncrypted, setIsBackupEncrypted] = useState(isEncryptionEnabled); const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); const [user, setUser] = useState(application.getUser()); const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); const [hasProtections] = useState(application.hasProtectionSources()); const [isEmailFocused, setIsEmailFocused] = useState(false); const [isPasscodeFocused, setIsPasscodeFocused] = useState(false); const { notesAndTagsCount } = appState.accountMenu; const refreshCredentialState = () => { setUser(application.getUser()); setCanAddPasscode(!application.isEphemeralSession()); setHasPasscode(application.hasPasscode()); setShowPasscodeForm(false); }; const loadHost = () => { const host = application.getHost(); setServer(host); setUrl(host); }; const reloadAutoLockInterval = useCallback(async () => { const interval = await application.getAutolockService().getAutoLockInterval(); setSelectedAutoLockInterval(interval); }, [application]); const refreshEncryptionStatus = () => { const hasUser = application.hasAccount(); const hasPasscode = application.hasPasscode(); setHasPasscode(hasPasscode); const encryptionEnabled = hasUser || hasPasscode; const newEncryptionStatusString = hasUser ? STRING_E2E_ENABLED : hasPasscode ? STRING_LOCAL_ENC_ENABLED : STRING_ENC_NOT_ENABLED; setEncryptionStatusString(newEncryptionStatusString); setIsEncryptionEnabled(encryptionEnabled); setIsBackupEncrypted(encryptionEnabled); }; const errorReportingIdValue = errorReportingId(); const keyStorageInfo = StringUtils.keyStorageInfo(application); const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); const showBetaWarning = appState.showBetaWarning; const closeAccountMenu = () => { appState.accountMenu.closeAccountMenu(); }; const handleSignInClick = () => { setShowLogin(true); setIsEmailFocused(true); }; const handleRegisterClick = () => { setShowRegister(true); setIsEmailFocused(true); }; const blurAuthFields = () => { emailInputRef.current.blur(); passwordInputRef.current.blur(); passwordConfirmationInputRef.current?.blur(); }; const login = async () => { setStatus(STRING_GENERATING_LOGIN_KEYS); setIsAuthenticating(true); const response = await application.signIn( email as string, password, isStrictSignIn, isEphemeral, shouldMergeLocal ); const error = response.error; if (!error) { setIsAuthenticating(false); setPassword(''); closeAccountMenu(); return; } setShowLogin(true); setStatus(undefined); setPassword(''); if (error.message) { await application.alertService.alert(error.message); } setIsAuthenticating(false); }; const register = async () => { if (passwordConfirmation !== password) { application.alertService.alert(STRING_NON_MATCHING_PASSWORDS); return; } setStatus(STRING_GENERATING_REGISTER_KEYS); setIsAuthenticating(true); const response = await application.register( email as string, password, isEphemeral, shouldMergeLocal ); const error = response.error; if (error) { setStatus(undefined); setIsAuthenticating(false); application.alertService.alert(error.message); } else { setIsAuthenticating(false); closeAccountMenu(); } }; const handleAuthFormSubmit = (event: TargetedEvent | TargetedMouseEvent | TargetedKeyboardEvent ) => { event.preventDefault(); if (!email || !password) { return; } blurAuthFields(); if (showLogin) { login(); } else { register(); } }; const handleHostInputChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setUrl(value); application.setHost(value); }; const handleKeyPressKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { handleAuthFormSubmit(event as TargetedKeyboardEvent); } }; const handlePasswordChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setPassword(value); }; const handleEmailChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setEmail(value); }; const handlePasswordConfirmationChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setPasswordConfirmation(value); }; const handleMergeLocalData = async (event: TargetedEvent) => { const { checked } = event.target as HTMLInputElement; if (!checked) { setShouldMergeLocal(checked); const confirmResult = await confirmDialog({ text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, confirmButtonStyle: 'danger' }); setShouldMergeLocal(!confirmResult); } }; const openPasswordWizard = () => { closeAccountMenu(); application.presentPasswordWizard(PasswordWizardType.ChangePassword); }; const openSessionsModal = () => { closeAccountMenu(); appState.openSessionsModal(); }; const getEncryptionStatusForNotes = () => { const length = notesAndTagsCount; return `${length}/${length} notes and tags encrypted`; }; const enableProtections = () => { application.clearProtectionSession(); }; const handleAddPassCode = () => { setShowPasscodeForm(true); setIsPasscodeFocused(true); }; const submitPasscodeForm = async (event: TargetedEvent | TargetedMouseEvent) => { event.preventDefault(); if (passcode !== passcodeConfirmation) { await alertDialog({ text: STRING_NON_MATCHING_PASSCODES }); setIsPasscodeFocused(true); return; } await preventRefreshing( STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => { const successful = application.hasPasscode() ? await application.changePasscode(passcode as string) : await application.addPasscode(passcode as string); if (!successful) { setIsPasscodeFocused(true); } } ); setPasscode(undefined); setPasscodeConfirmation(undefined); setShowPasscodeForm(false); refreshEncryptionStatus(); }; const handlePasscodeChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setPasscode(value); }; const handleConfirmPasscodeChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setPasscodeConfirmation(value); }; const selectAutoLockInterval = async (interval: number) => { if (!(await application.authorizeAutolockIntervalChange())) { return; } await application.getAutolockService().setAutoLockInterval(interval); reloadAutoLockInterval(); }; const disableBetaWarning = () => { appState.disableBetaWarning(); }; const hidePasswordForm = () => { setShowLogin(false); setShowRegister(false); setPassword(''); setPasswordConfirmation(undefined); }; const signOut = () => { appState.accountMenu.setSigningOut(true); }; const changePasscodePressed = () => { handleAddPassCode(); }; const removePasscodePressed = async () => { await preventRefreshing( STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => { if (await application.removePasscode()) { await application .getAutolockService() .deleteAutolockPreference(); await reloadAutoLockInterval(); refreshEncryptionStatus(); } } ); }; 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 removeAppLaunchedObserver = application.addEventObserver( async () => { refreshCredentialState(); loadHost(); reloadAutoLockInterval(); refreshEncryptionStatus(); }, ApplicationEvent.Launched ); const removeKeyStatusChangedObserver = application.addEventObserver( async () => { refreshCredentialState(); }, ApplicationEvent.KeyStatusChanged ); const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( async () => { setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }, ApplicationEvent.ProtectionSessionExpiryDateChanged ); return () => { removeAppLaunchedObserver(); removeKeyStatusChangedObserver(); removeProtectionSessionExpiryDateChangedObserver(); } }, []); // `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial // value of `selectedAutoLockInterval` useEffect(() => { reloadAutoLockInterval(); }, [reloadAutoLockInterval]); useEffect(() => { refreshEncryptionStatus(); }, [refreshEncryptionStatus]); useEffect(() => { if (isEmailFocused) { emailInputRef.current.focus(); setIsEmailFocused(false); } if (isPasscodeFocused) { passcodeInputRef.current.focus(); setIsPasscodeFocused(false); } }, [isEmailFocused, isPasscodeFocused]); return (
Account
Close
{!user && !showLogin && !showRegister && (
Sign in or register to enable sync and end-to-end encryption.
Standard Notes is free on every platform, and comes standard with sync and encryption.
)} {(showLogin || showRegister) && (
{showLogin ? 'Sign In' : 'Register'}
{showRegister && } {showAdvanced && (
Advanced Options
{showLogin && ( )}
)} {!isAuthenticating && (
)} {showRegister && (
No Password Reset.
Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
)} {status && (
{status}
)} {!isAuthenticating && (
{notesAndTagsCount > 0 && ( )}
)}
)} {!showLogin && !showRegister && (
{user && (
{appState.sync.errorMessage && (
Sync Unreachable
Hmm...we can't seem to sync your account. The reason: {appState.sync.errorMessage}
Need help?
)}
{user.email}
{server}
)}
Encryption
{isEncryptionEnabled && (
{getEncryptionStatusForNotes()}
)}

{encryptionStatusString}

{hasProtections && (
Protections
{protectionsDisabledUntil && (
Protections are disabled until {protectionsDisabledUntil}
)} {!protectionsDisabledUntil && (
Protections are enabled
)}

Actions like viewing protected notes, exporting decrypted backups, or revoking an active session, require additional authentication like entering your account password or application passcode.

{protectionsDisabledUntil && (
)}
)}
Passcode Lock
{!hasPasscode && (
{canAddPasscode && ( <> {!showPasscodeForm && (
)}

Add a passcode to lock the application and encrypt on-device key storage.

{keyStorageInfo && (

{keyStorageInfo}

)} )} {!canAddPasscode && (

Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the "Stay signed in" option checked.

)}
)} {showPasscodeForm && (
)} {hasPasscode && !showPasscodeForm && ( <>
Passcode lock is enabled
Options
Autolock
{passcodeAutoLockOptions.map(option => { return ( selectAutoLockInterval(option.value)}> {option.label} ); })}
The autolock timer begins when the window or tab loses focus.
)}
{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.

)}
)}
{appVersion} {showBetaWarning && ( ( Hide beta warning ) )}
{(showLogin || showRegister) && ( Cancel )} {!showLogin && !showRegister && ( {user ? 'Sign out' : 'Clear session data'} )}
); }); export const AccountMenuDirective = toDirective( AccountMenu );