diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 4d3f119cb..579500b6f 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -58,8 +58,7 @@ import { SessionsModalDirective } from './components/SessionsModal'; import { NoAccountWarningDirective } from './components/NoAccountWarning'; import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning'; import { SearchOptionsDirective } from './components/SearchOptions'; -// import { AccountMenuDirective } from './components/AccountMenu'; -import { AccountMenuDirective } from './components/AccountMenu/index'; // TODO: Vardan: remove `index` after removing `AccountMenu.tsx` +import { AccountMenuDirective } from './components/AccountMenu'; import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; import { NotesContextMenuDirective } from './components/NotesContextMenu'; diff --git a/app/assets/javascripts/components/AccountMenu.tsx b/app/assets/javascripts/components/AccountMenu.tsx deleted file mode 100644 index 1c4e30a5b..000000000 --- a/app/assets/javascripts/components/AccountMenu.tsx +++ /dev/null @@ -1,1009 +0,0 @@ -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 -); diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx index ba29146e6..7fbc00c36 100644 --- a/app/assets/javascripts/components/AccountMenu/Authentication.tsx +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -11,16 +11,11 @@ import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent; import { WebApplication } from '@/ui_models/application'; import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks'; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; -import { FunctionalComponent } from 'preact'; import { User } from '@node_modules/@standardnotes/snjs/dist/@types/services/api/responses'; -import { ApplicationEvent } from '@node_modules/@standardnotes/snjs'; -import { observer } from 'mobx-react-lite'; import { FC } from 'react'; type Props = { application: WebApplication; - // url: string | undefined; - // setUrl: StateUpdater; server: string | undefined; setServer: StateUpdater; closeAccountMenu: () => void; @@ -33,10 +28,7 @@ type Props = { } const Authentication: FC = ({ -// const Authentication = observer(({ application, - // url, - // setUrl, server, setServer, closeAccountMenu, @@ -52,7 +44,6 @@ const Authentication: FC = ({ const [isAuthenticating, setIsAuthenticating] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - // const [passwordConfirmation, setPasswordConfirmation] = useState(undefined); const [passwordConfirmation, setPasswordConfirmation] = useState(''); const [status, setStatus] = useState(undefined); const [isEmailFocused, setIsEmailFocused] = useState(false); @@ -61,7 +52,6 @@ const Authentication: FC = ({ const [isStrictSignIn, setIsStrictSignIn] = useState(false); const [shouldMergeLocal, setShouldMergeLocal] = useState(true); - // TODO: maybe create a custom hook, which gets respective arguments and does focusing useEffect(() => { if (isEmailFocused) { emailInputRef.current.focus(); @@ -73,7 +63,6 @@ const Authentication: FC = ({ useEffect(() => { if (!showLogin && !showRegister) { setPassword(''); - // setPasswordConfirmation(undefined); // TODO: Vardan: maybe change this to empty string as for password? setPasswordConfirmation(''); } }, [showLogin, showRegister]); diff --git a/app/assets/javascripts/components/AccountMenu/DataBackup.tsx b/app/assets/javascripts/components/AccountMenu/DataBackup.tsx new file mode 100644 index 000000000..33311af1a --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/DataBackup.tsx @@ -0,0 +1,157 @@ +import { isDesktopApplication } from '@/utils'; +import { alertDialog } from '@Services/alertService'; +import { + STRING_IMPORT_SUCCESS, + STRING_INVALID_IMPORT_FILE, + STRING_UNSUPPORTED_BACKUP_FILE_VERSION, + StringImportError +} from '@/strings'; +import { BackupFile } from '@node_modules/@standardnotes/snjs'; +import { useState } from '@node_modules/preact/hooks'; +import { WebApplication } from '@/ui_models/application'; +import { JSXInternal } from '@node_modules/preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import { StateUpdater } from 'preact/hooks'; +import { FC } from 'react'; + +type Props = { + application: WebApplication; + isBackupEncrypted: boolean; + isEncryptionEnabled: boolean; + setIsBackupEncrypted: StateUpdater; +} + +const DataBackup: FC = ({ + application, + isBackupEncrypted, + isEncryptionEnabled, + setIsBackupEncrypted + }) => { + + const [isImportDataLoading, setIsImportDataLoading] = useState(false); + + 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 }); + } + }; + + return ( + <> + {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. +

+ )} +
+
+ )} + + ); +}; + +export default DataBackup; diff --git a/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx b/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx new file mode 100644 index 000000000..65fbee6fc --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx @@ -0,0 +1,81 @@ +import { useState } from '@node_modules/preact/hooks'; +import { storage, StorageKey } from '@Services/localStorage'; +import { disableErrorReporting, enableErrorReporting, errorReportingId } from '@Services/errorReporting'; +import { alertDialog } from '@Services/alertService'; +import { observer } from 'mobx-react-lite'; +import { AppState } from '@/ui_models/app_state'; + +type Props = { + appState: AppState; +} + +const ErrorReporting = observer(({ appState }: Props) => { + const [isErrorReportingEnabled] = useState(() => storage.get(StorageKey.DisableErrorReporting) === false); + const [errorReportingIdValue] = useState(() => errorReportingId()); + + 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. + ` + }); + }; + + return ( +
+
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 default ErrorReporting; diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx index 66d33c4f6..ae291e7ba 100644 --- a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -26,7 +26,7 @@ const PasscodeLock: FC = ({ application, setEncryptionStatusString, setIsEncryptionEnabled, - setIsBackupEncrypted, + setIsBackupEncrypted }) => { const keyStorageInfo = StringUtils.keyStorageInfo(application); const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); @@ -56,7 +56,7 @@ const PasscodeLock: FC = ({ setSelectedAutoLockInterval(interval); }, [application]); - const refreshEncryptionStatus = () => { + const refreshEncryptionStatus = useCallback(() => { const hasUser = application.hasAccount(); const hasPasscode = application.hasPasscode(); @@ -64,16 +64,16 @@ const PasscodeLock: FC = ({ const encryptionEnabled = hasUser || hasPasscode; - const newEncryptionStatusString = hasUser + const encryptionStatusString = hasUser ? STRING_E2E_ENABLED : hasPasscode ? STRING_LOCAL_ENC_ENABLED : STRING_ENC_NOT_ENABLED; - setEncryptionStatusString(newEncryptionStatusString); + setEncryptionStatusString(encryptionStatusString); setIsEncryptionEnabled(encryptionEnabled); setIsBackupEncrypted(encryptionEnabled); - }; + }, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled]); const selectAutoLockInterval = async (interval: number) => { if (!(await application.authorizeAutolockIntervalChange())) { @@ -158,7 +158,6 @@ const PasscodeLock: FC = ({ // Add the required event observers useEffect(() => { - console.log('in PasscodeLock, observer'); const removeKeyStatusChangedObserver = application.addEventObserver( async () => { setCanAddPasscode(!application.isEphemeralSession()); @@ -170,8 +169,8 @@ const PasscodeLock: FC = ({ return () => { removeKeyStatusChangedObserver(); - } - }) + }; + }); return (
@@ -264,6 +263,6 @@ const PasscodeLock: FC = ({ )}
); -} +}; export default PasscodeLock; diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index d0979310e..7a3006fd0 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -2,29 +2,9 @@ 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 { useEffect, useState } from 'preact/hooks'; +import { isSameDay } from '@/utils'; +import { ApplicationEvent } from '@node_modules/@standardnotes/snjs'; import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; import Authentication from '@/components/AccountMenu/Authentication'; import Footer from '@/components/AccountMenu/Footer'; @@ -32,6 +12,9 @@ import User from '@/components/AccountMenu/User'; import Encryption from '@/components/AccountMenu/Encryption'; import Protections from '@/components/AccountMenu/Protections'; import PasscodeLock from '@/components/AccountMenu/PasscodeLock'; +import DataBackup from '@/components/AccountMenu/DataBackup'; +import ErrorReporting from '@/components/AccountMenu/ErrorReporting'; +import { useCallback } from '@node_modules/preact/hooks'; type Props = { appState: AppState; @@ -39,7 +22,7 @@ type Props = { }; const AccountMenu = observer(({ application, appState }: Props) => { - const getProtectionsDisabledUntil = (): string | null => { + const getProtectionsDisabledUntil = useCallback((): string | null => { const protectionExpiry = application.getProtectionSessionExpiryDate(); const now = new Date(); if (protectionExpiry > now) { @@ -62,154 +45,24 @@ const AccountMenu = observer(({ application, appState }: Props) => { return f.format(protectionExpiry); } return null; - }; - + }, [application]); 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( @@ -230,9 +83,7 @@ const AccountMenu = observer(({ application, appState }: Props) => { removeKeyStatusChangedObserver(); removeProtectionSessionExpiryDateChangedObserver(); }; - }, []); // TODO:fix dependency list (should they left empty?) - - + }, [application, getProtectionsDisabledUntil]); return (
@@ -244,8 +95,6 @@ const AccountMenu = observer(({ application, appState }: Props) => {
{ setIsEncryptionEnabled={setIsEncryptionEnabled} setIsBackupEncrypted={setIsBackupEncrypted} /> - {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. -

- - )} -
- -
- -
+ +
)}