From 8a2fa13d4e58bce77fbbd3174855b505764174fa Mon Sep 17 00:00:00 2001 From: VardanHakobyan Date: Thu, 10 Jun 2021 18:38:44 +0400 Subject: [PATCH 01/24] refactor: split AccountMenu to smaller components Notes: - remove `url` and keep only `server`, as they are duplicating each other --- app/assets/javascripts/app.ts | 3 +- .../components/AccountMenu/Authentication.tsx | 397 ++++++++++++++++++ .../components/AccountMenu/Encryption.tsx | 36 ++ .../components/AccountMenu/Footer.tsx | 76 ++++ .../components/AccountMenu/PasscodeLock.tsx | 269 ++++++++++++ .../components/AccountMenu/Protections.tsx | 46 ++ .../components/AccountMenu/User.tsx | 71 ++++ .../components/AccountMenu/index.tsx | 387 +++++++++++++++++ 8 files changed, 1284 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/components/AccountMenu/Authentication.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/Encryption.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/Footer.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/Protections.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/User.tsx create mode 100644 app/assets/javascripts/components/AccountMenu/index.tsx diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 579500b6f..4d3f119cb 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -58,7 +58,8 @@ 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'; +import { AccountMenuDirective } from './components/AccountMenu/index'; // TODO: Vardan: remove `index` after removing `AccountMenu.tsx` import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; import { NotesContextMenuDirective } from './components/NotesContextMenu'; diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx new file mode 100644 index 000000000..82def0bf9 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -0,0 +1,397 @@ +import { confirmDialog } from '@Services/alertService'; +import { + STRING_ACCOUNT_MENU_UNCHECK_MERGE, + STRING_GENERATING_LOGIN_KEYS, + STRING_GENERATING_REGISTER_KEYS, + STRING_NON_MATCHING_PASSWORDS +} from '@/strings'; +import { JSXInternal } from '@node_modules/preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +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; + notesAndTagsCount: number; + showLogin: boolean; + setShowLogin: StateUpdater; + showRegister: boolean; + setShowRegister: StateUpdater; + user: User | undefined; +} + +const Authentication: FC = ({ +// const Authentication = observer(({ + application, + // url, + // setUrl, + server, + setServer, + closeAccountMenu, + notesAndTagsCount, + showLogin, + setShowLogin, + showRegister, + setShowRegister, + user + }: Props) => { + + const [showAdvanced, setShowAdvanced] = useState(false); + 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); + + const [isEphemeral, setIsEphemeral] = useState(false); + 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(); + setIsEmailFocused(false); + } + }, [isEmailFocused]); + + // Reset password and confirmation fields when hiding the form + useEffect(() => { + if (!showLogin && !showRegister) { + setPassword(''); + // setPasswordConfirmation(undefined); // TODO: Vardan: maybe change this to empty string as for password? + setPasswordConfirmation(''); + } + }, [showLogin, showRegister]); + + const handleHostInputChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + // setUrl(value); + setServer(value); + application.setHost(value); + }; + + const emailInputRef = useRef(); + const passwordInputRef = useRef(); + const passwordConfirmationInputRef = useRef(); + + 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( + // const response = await handleSignIn( + email, + 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); + // await handleAlert(error.message); + } + + setIsAuthenticating(false); + }; + + const register = async () => { + if (passwordConfirmation !== password) { + application.alertService.alert(STRING_NON_MATCHING_PASSWORDS); + // handleAlert(STRING_NON_MATCHING_PASSWORDS); + return; + } + setStatus(STRING_GENERATING_REGISTER_KEYS); + setIsAuthenticating(true); + + const response = await application.register( + // const response = await handleRegister( + email, + password, + isEphemeral, + shouldMergeLocal + ); + + const error = response.error; + if (error) { + setStatus(undefined); + setIsAuthenticating(false); + + application.alertService.alert(error.message); + // handleAlert(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 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); + } + }; + + return ( + <> + {!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 && ( + + )} +
+ )} + +
+ )} + ); +}; + +export default Authentication; diff --git a/app/assets/javascripts/components/AccountMenu/Encryption.tsx b/app/assets/javascripts/components/AccountMenu/Encryption.tsx new file mode 100644 index 000000000..b90056f98 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Encryption.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; + +type Props = { + isEncryptionEnabled: boolean; + notesAndTagsCount: number; + encryptionStatusString: string | undefined; +} + +const Encryption: FC = ({ + isEncryptionEnabled, + notesAndTagsCount, + encryptionStatusString, + }) => { + const getEncryptionStatusForNotes = () => { + const length = notesAndTagsCount; + return `${length}/${length} notes and tags encrypted`; + }; + + return ( +
+
+ Encryption +
+ {isEncryptionEnabled && ( +
+ {getEncryptionStatusForNotes()} +
+ )} +

+ {encryptionStatusString} +

+
+ ); +} + +export default Encryption; diff --git a/app/assets/javascripts/components/AccountMenu/Footer.tsx b/app/assets/javascripts/components/AccountMenu/Footer.tsx new file mode 100644 index 000000000..11705f9c2 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Footer.tsx @@ -0,0 +1,76 @@ +import { FunctionalComponent } from 'preact'; +import { AppState } from '@/ui_models/app_state'; +import { StateUpdater, useState } from '@node_modules/preact/hooks'; +import { WebApplication } from '@/ui_models/application'; +import { User } from '@node_modules/@standardnotes/snjs/dist/@types/services/api/responses'; +import { observer } from '@node_modules/mobx-react-lite'; + +type Props = { + appState: AppState; + application: WebApplication; + showLogin: boolean; + setShowLogin: StateUpdater; + showRegister: boolean; + setShowRegister: StateUpdater; + user: User | undefined; +} + +const Footer = observer(({ + appState, + application, + showLogin, + setShowLogin, + showRegister, + setShowRegister, + user + }: Props) => { + + const showBetaWarning = appState.showBetaWarning; + + + const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`); + + const disableBetaWarning = () => { + appState.disableBetaWarning(); + }; + + const signOut = () => { + appState.accountMenu.setSigningOut(true); + }; + + const hidePasswordForm = () => { + setShowLogin(false); + setShowRegister(false); + // TODO: Vardan: this comes from main `index.tsx` and the below commented parts should reset password and confirmation. + // Check whether it works when I don't call them explicitly. + // setPassword(''); + // setPasswordConfirmation(undefined); + }; + + return ( +
+
+
+ {appVersion} + {showBetaWarning && ( + + ( + Hide beta warning + ) + + )} +
+ {(showLogin || showRegister) && ( + Cancel + )} + {!showLogin && !showRegister && ( + + {user ? 'Sign out' : 'Clear session data'} + + )} +
+
+ ); +}); + +export default Footer; diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx new file mode 100644 index 000000000..66d33c4f6 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -0,0 +1,269 @@ +import { FC } from 'react'; +import { + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED, + STRING_NON_MATCHING_PASSCODES, + StringUtils +} from '@/strings'; +import { WebApplication } from '@/ui_models/application'; +import { preventRefreshing } from '@/utils'; +import { JSXInternal } from '@node_modules/preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import { alertDialog } from '@Services/alertService'; +import { useCallback, useEffect, useRef, useState } from '@node_modules/preact/hooks'; +import { ApplicationEvent } from '@node_modules/@standardnotes/snjs'; +import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; +import { StateUpdater } from 'preact/hooks'; + +type Props = { + application: WebApplication; + setEncryptionStatusString: StateUpdater; + setIsEncryptionEnabled: StateUpdater; + setIsBackupEncrypted: StateUpdater; +}; + +const PasscodeLock: FC = ({ + application, + setEncryptionStatusString, + setIsEncryptionEnabled, + setIsBackupEncrypted, + }) => { + const keyStorageInfo = StringUtils.keyStorageInfo(application); + const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); + + const passcodeInputRef = useRef(); + + const [passcode, setPasscode] = useState(undefined); + const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); + const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); + const [isPasscodeFocused, setIsPasscodeFocused] = useState(false); + const [showPasscodeForm, setShowPasscodeForm] = useState(false); + const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); + const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); + + + const handleAddPassCode = () => { + setShowPasscodeForm(true); + setIsPasscodeFocused(true); + }; + + const changePasscodePressed = () => { + handleAddPassCode(); + }; + + 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 selectAutoLockInterval = async (interval: number) => { + if (!(await application.authorizeAutolockIntervalChange())) { + return; + } + await application.getAutolockService().setAutoLockInterval(interval); + reloadAutoLockInterval(); + }; + + 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 handlePasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscode(value); + }; + + const handleConfirmPasscodeChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setPasscodeConfirmation(value); + }; + + 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(); + }; + + useEffect(() => { + refreshEncryptionStatus(); + }, [refreshEncryptionStatus]); + + // `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial + // value of `selectedAutoLockInterval` + useEffect(() => { + reloadAutoLockInterval(); + }, [reloadAutoLockInterval]); + + useEffect(() => { + if (isPasscodeFocused) { + passcodeInputRef.current.focus(); + setIsPasscodeFocused(false); + } + }, [isPasscodeFocused]); + + // Add the required event observers + useEffect(() => { + console.log('in PasscodeLock, observer'); + const removeKeyStatusChangedObserver = application.addEventObserver( + async () => { + setCanAddPasscode(!application.isEphemeralSession()); + setHasPasscode(application.hasPasscode()); + setShowPasscodeForm(false); + }, + ApplicationEvent.KeyStatusChanged + ); + + return () => { + removeKeyStatusChangedObserver(); + } + }) + + return ( +
+
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.
+ +
+ + )} +
+ ); +} + +export default PasscodeLock; diff --git a/app/assets/javascripts/components/AccountMenu/Protections.tsx b/app/assets/javascripts/components/AccountMenu/Protections.tsx new file mode 100644 index 000000000..fc472549c --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Protections.tsx @@ -0,0 +1,46 @@ +import { WebApplication } from '@/ui_models/application'; +import { FC } from 'react'; + +type Props = { + application: WebApplication; + protectionsDisabledUntil: string | null; +}; + +const Protections: FC = ({ + application, + protectionsDisabledUntil + }) => { + const enableProtections = () => { + application.clearProtectionSession(); + }; + + return ( +
+
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 && ( +
+ +
+ )} +
+ ); +} + +export default Protections; diff --git a/app/assets/javascripts/components/AccountMenu/User.tsx b/app/assets/javascripts/components/AccountMenu/User.tsx new file mode 100644 index 000000000..f7f172338 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/User.tsx @@ -0,0 +1,71 @@ +import { observer } from 'mobx-react-lite'; +import { AppState } from '@/ui_models/app_state'; +import { PasswordWizardType } from '@/types'; +import { WebApplication } from '@/ui_models/application'; + +type Props = { + email: string; + server: string | undefined; + appState: AppState; + application: WebApplication; + closeAccountMenu: () => void; +} + +const User = observer(({ + email, + server, + appState, + application, + closeAccountMenu + }: Props) => { + const openPasswordWizard = () => { + closeAccountMenu(); + application.presentPasswordWizard(PasswordWizardType.ChangePassword); + }; + + const openSessionsModal = () => { + closeAccountMenu(); + appState.openSessionsModal(); + }; + + return ( +
+ {appState.sync.errorMessage && ( +
+
Sync Unreachable
+
+ Hmm...we can't seem to sync your account. + The reason: {appState.sync.errorMessage} +
+ + Need help? + +
+ )} +
+
+
+ {email} +
+
+ {server} +
+
+
+ + ); +}); + +export default User; diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx new file mode 100644 index 000000000..d0979310e --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -0,0 +1,387 @@ +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 +); From abb0b6595eebd1aab881fe123eaae76f2f7d5da3 Mon Sep 17 00:00:00 2001 From: VardanHakobyan Date: Fri, 11 Jun 2021 15:01:56 +0400 Subject: [PATCH 02/24] style: split AccountMenu to smaller components - change link to button for "Advanced Options" - show cursor as pointer on checkbox and its label --- .../components/AccountMenu/Authentication.tsx | 13 +++++-------- app/assets/stylesheets/_ui.scss | 4 ++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/components/AccountMenu/Authentication.tsx b/app/assets/javascripts/components/AccountMenu/Authentication.tsx index 82def0bf9..0237ed3c6 100644 --- a/app/assets/javascripts/components/AccountMenu/Authentication.tsx +++ b/app/assets/javascripts/components/AccountMenu/Authentication.tsx @@ -110,7 +110,6 @@ const Authentication: FC = ({ setIsAuthenticating(true); const response = await application.signIn( - // const response = await handleSignIn( email, password, isStrictSignIn, @@ -148,7 +147,6 @@ const Authentication: FC = ({ setIsAuthenticating(true); const response = await application.register( - // const response = await handleRegister( email, password, isEphemeral, @@ -290,11 +288,11 @@ const Authentication: FC = ({ ref={passwordConfirmationInputRef} />} {showAdvanced && (
@@ -308,14 +306,13 @@ const Authentication: FC = ({ name="server" placeholder="Server URL" onChange={handleHostInputChange} - // value={url} value={server} required />
{showLogin && (