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_GENERATING_LOGIN_KEYS, STRING_GENERATING_REGISTER_KEYS, STRING_LOCAL_ENC_ENABLED, STRING_NON_MATCHING_PASSCODES, STRING_NON_MATCHING_PASSWORDS, StringUtils } from '@/strings'; import { ContentType } from '@node_modules/@standardnotes/snjs'; import { PasswordWizardType } from '@/types'; import { JSXInternal } from '@node_modules/preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; import { alertDialog } from '@Services/alertService'; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; import { RefObject } from 'react'; type Props = { appState: AppState; application: WebApplication; closeAccountMenu: () => void; }; const AccountMenu = observer(({ application, appState, closeAccountMenu }: 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(); // TODO: Vardan `showLogin` and `showRegister` were in `formData` in Angular code, check whether I need to write similarly 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 [syncError, setSyncError] = 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(undefined); const [url, setUrl] = useState(undefined); const [showPasscodeForm, setShowPasscodeForm] = useState(false); const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isErrorReportingEnabled, setIsErrorReportingEnabled] = useState(false); const [appVersion, setAppVersion] = useState(''); // TODO: Vardan: figure out how to get `appVersion` similar to original code const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); const [isBackupEncrypted, setIsBackupEncrypted] = useState(isEncryptionEnabled); const [isSyncInProgress, setIsSyncInProgress] = useState(false); const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); const user = application.getUser(); const reloadAutoLockInterval = useCallback(async () => { const interval = await application.getAutolockService().getAutoLockInterval(); setSelectedAutoLockInterval(interval); }, [application]); const errorReportingIdValue = errorReportingId(); const canAddPasscode = !application.isEphemeralSession(); const keyStorageInfo = StringUtils.keyStorageInfo(application); const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); const showBetaWarning = appState.showBetaWarning; /* const displayRegistrationForm = () => { console.log('display registration form!'); }; */ const focusWithTimeout = (inputElementRef: RefObject) => { // In case the ref element is not yet available at this moment, // we call `focus()` after timeout. setTimeout(() => { inputElementRef.current && inputElementRef.current.focus(); }, 0); }; const handleSignInClick = () => { setShowLogin(true); focusWithTimeout(emailInputRef); }; const handleRegisterClick = () => { setShowRegister(true); focusWithTimeout(emailInputRef); }; const blurAuthFields = () => { emailInputRef.current.blur(); passwordInputRef.current.blur(); }; /* // TODO: move to top type FormData = { email: string; password: string; passwordConfirmation: string; showLogin: boolean; showRegister: boolean; showPasscodeForm: boolean; isStrictSignin?: boolean; isEphemeral: boolean; shouldMergeLocal?: boolean; url: string; isAuthenticating: boolean; status: string; passcode: string; passcodeConfirmation: string; };*/ const login = async() => { setStatus(STRING_GENERATING_LOGIN_KEYS); setIsAuthenticating(true); const response = await application.signIn( email as string, password as string, 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 (passcodeConfirmation !== 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 as string, 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) => { // TODO: If I don't need `submit` form at all, get rid of `onSubmit` and thus there will be no need to `preventDefault` event.preventDefault(); if (!email || !password) { return; } blurAuthFields(); if (showLogin) { login(); } else { register(); } }; const handleHostInputChange = (event: TargetedEvent) => { const { value } = event.target as HTMLInputElement; setServer(value); application.setHost(value); }; // const handleKeyPressKeyDown = (event: KeyboardEvent) => { const handleKeyPressKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { // TODO: fix TS error for `event` handleAuthFormSubmit(event); } }; 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 = () => { console.log('handlePasswordConfirmationChange'); }; const handleMergeLocalData = () => { console.log('handleMergeLocalData'); }; 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(); // Get the latest the protection status setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }; 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 handleAddPassCode = () => { setShowPasscodeForm(true); // At this moment the passcode input is not available, therefore the ref // is null. Therefore we call `focus()` after timeout. focusWithTimeout(passcodeInputRef); }; const submitPasscodeForm = async (event: TargetedEvent | TargetedMouseEvent) => { // TODO: If I don't need `submit` form at all, get rid of `onSubmit` and thus there will be no need to `preventDefault` event.preventDefault(); if (passcode !== passcodeConfirmation) { await alertDialog({ text: STRING_NON_MATCHING_PASSCODES }); passcodeInputRef.current.focus(); 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) { passcodeInputRef.current.focus(); } } ); setPasscode(undefined); setPasscodeConfirmation(undefined); setShowPasscodeForm(false); setProtectionsDisabledUntil(getProtectionsDisabledUntil()); refreshEncryptionStatus(); }; // TODO: Vardan: check whether this (and `handleConfirmPasscodeChange`) method is required in the end 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 = () => { console.log('disableBetaWarning'); }; const hidePasswordForm = () => { setShowLogin(false); setShowRegister(false); setPassword(''); setPasswordConfirmation(undefined); }; const signOut = () => { appState.accountMenuReact.setSigningOut(true); }; // TODO: Vardan: the name `changePasscodePressed` comes from original code; it is very similar to my `handlePasscodeChange`. // Check if `handlePasscodeChange` is not required, remove it and rename `changePasscodePressed` to `handlePasscodeChange` const changePasscodePressed = () => { handleAddPassCode(); }; // TODO: Vardan: the name `removePasscodePressed` comes from original code; // Check if I rename`changePasscodePressed` to `handlePasscodeChange`, also rename `removePasscodePressed` to `handleRemovePasscode` const removePasscodePressed = async () => { await preventRefreshing( STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => { if (await application.removePasscode()) { await application .getAutolockService() .deleteAutolockPreference(); await reloadAutoLockInterval(); refreshEncryptionStatus(); } } ); setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }; const downloadDataArchive = () => { console.log('downloadDataArchive'); }; const importFileSelected = () => { console.log('importFileSelected'); }; const toggleErrorReportingEnabled = () => { if (isErrorReportingEnabled) { disableErrorReporting(); } else { enableErrorReporting(); } if (!isSyncInProgress) { window.location.reload(); } }; const openErrorReportingDialog = () => { console.log('openErrorReportingDialog'); }; // TODO: check whether this works fine (e.g. remove all tags and notes and then add one and check whether UI behaves appropriately) const notesAndTagsCount = application.getItems([ContentType.Note, ContentType.Tag]).length; const hasProtections = application.hasProtectionSources(); // TODO: Vardan: this is as per `this.autorun` from `$onInit`, check whether it works // I'm mostly concerned about having dependency, since I think it is running only once in original code // (I suppose it runs here only once, too. But need to recheck) useEffect(() => { setSyncError(appState.sync.errorMessage); setIsSyncInProgress(appState.sync.inProgress); }, [appState.sync.errorMessage, appState.sync.inProgress]); useEffect(() => { setIsErrorReportingEnabled(storage.get(StorageKey.DisableErrorReporting) === false); }, []); useEffect(() => { // TODO: in original `AccountMenu`, the `appVersion` is available in constructor (the `window.electronAppVersion` is `undefined`). // But I can't find where `appVersion` is passed to AccountMenu's constructor... The only place I found is `app.ts`, where // it sets constant `appVersion` from `bridge.appVersion` - maybe constructor takes that value from there? // Ask someone to explain that part. // Here I just take the version from `application.bridge.appVersion`, as it is done in `app.ts`. setAppVersion(`v${((window as any).electronAppVersion || application.bridge.appVersion)}`); }, [appVersion, application.bridge.appVersion]); useEffect(() => { reloadAutoLockInterval(); }, [reloadAutoLockInterval]); useEffect(() => { setIsErrorReportingEnabled(storage.get(StorageKey.DisableErrorReporting) === false); }, []); useEffect(() => { const host = application.getHost(); setServer(host); setUrl(host); // TODO: Vardan: maybe `url` is not needed at all, recheck }, [application]); useEffect(() => { refreshEncryptionStatus(); }, [refreshEncryptionStatus]); 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'}
{/* TODO: Vardan: there are `should-focus` and `sn-autofocus`, implement them */} {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 && (
{syncError && (
Sync Unreachable
Hmm...we can't seem to sync your account. The reason: {syncError}
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.
)}
{!isLoading && (
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.

)}
{isLoading && (
)}
)}
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 AccountMenuReact = toDirective( AccountMenu, { closeAccountMenu: '&' } );