From a342a3a224cb0eedd6bef06ff01d9179431746e1 Mon Sep 17 00:00:00 2001 From: Vardan Hakobyan Date: Wed, 12 Jan 2022 18:48:46 +0400 Subject: [PATCH] feat: add "Email Backups" to "Backups" section (#778) * feat: add "Email Backups" to "Backups" section * chore: remove comment * chore: better wording * chore: put working snjs version * chore: better wording * style: reuse existing css classes and add the missing one * feat: add "No email backup" option * refactor: move the function outside of the useEffect, remove unused utility function * feat (WIP): move CloudLink to backups section * chore: versions bump, type fixes * fix: handle the case when the setting update fails * style: remove dashed border from the confirmation code, UI improvements * feat: implement removing integration, improve interaction on different events * feat: implement non-interactive textarea for showing and copying the code * fix: fix TS errors * feat: implement "Perform backup" logic - remove the code for copying the confirmation code for backup integration - also remove unnecessary parameters passed to Provider * feat: don't show "CloudLink" in preferences pane * chore: show error in console on exception * refactor: better naming, add `coverage` folder to gitignore * fix: return correct setting name * refactor: use async/await for the sake of consistency * chore: remove duplicate line * feat: get urls for cloud backup from snjs * chore: update dependencies * refactor: set both `token` and `frequency` settings when enabling cloud integration; get only `frequency` when checking the integration status * refactor: once the setting is successfully saved, don't get its value from backend; instead, use its value that's still in frontend * feat: move "Receive a notification email if a cloud backup fails." into cloud backups section * fix: text correction * fix: get correct cloud integration url from snjs based on prod/dev environment --- .../javascripts/components/Dropdown.tsx | 6 +- .../preferences/PreferencesMenu.ts | 13 +- .../preferences/PreferencesView.tsx | 120 +++++----- .../javascripts/preferences/panes/Backups.tsx | 23 ++ .../preferences/panes/Security.tsx | 3 +- .../DataBackups.tsx | 98 ++++++-- .../panes/backups-segments/EmailBackups.tsx | 188 +++++++++++++++ .../cloud-backups/CloudBackupProvider.tsx | 222 ++++++++++++++++++ .../backups-segments/cloud-backups/index.tsx | 150 ++++++++++++ .../panes/backups-segments/index.ts | 3 + .../panes/security-segments/index.ts | 1 - app/assets/javascripts/strings.ts | 3 + app/assets/javascripts/utils/index.ts | 9 + app/assets/stylesheets/_main.scss | 18 ++ app/assets/stylesheets/_sn.scss | 6 + app/assets/stylesheets/_ui.scss | 4 + package.json | 3 +- yarn.lock | 20 +- 18 files changed, 789 insertions(+), 101 deletions(-) create mode 100644 app/assets/javascripts/preferences/panes/Backups.tsx rename app/assets/javascripts/preferences/panes/{security-segments => backups-segments}/DataBackups.tsx (67%) create mode 100644 app/assets/javascripts/preferences/panes/backups-segments/EmailBackups.tsx create mode 100644 app/assets/javascripts/preferences/panes/backups-segments/cloud-backups/CloudBackupProvider.tsx create mode 100644 app/assets/javascripts/preferences/panes/backups-segments/cloud-backups/index.tsx create mode 100644 app/assets/javascripts/preferences/panes/backups-segments/index.ts diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/components/Dropdown.tsx index 5ec2e2932..17739b175 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/components/Dropdown.tsx @@ -9,7 +9,7 @@ import { import VisuallyHidden from '@reach/visually-hidden'; import { FunctionComponent } from 'preact'; import { IconType, Icon } from './Icon'; -import { useState } from 'preact/hooks'; +import { useEffect, useState } from 'preact/hooks'; export type DropdownItem = { icon?: IconType; @@ -64,6 +64,10 @@ export const Dropdown: FunctionComponent = ({ }) => { const [value, setValue] = useState(defaultValue); + useEffect(() => { + setValue(defaultValue); + }, [defaultValue]); + const labelId = `${id}-label`; const handleChange = (value: string) => { diff --git a/app/assets/javascripts/preferences/PreferencesMenu.ts b/app/assets/javascripts/preferences/PreferencesMenu.ts index 94cf73836..e52e81f0f 100644 --- a/app/assets/javascripts/preferences/PreferencesMenu.ts +++ b/app/assets/javascripts/preferences/PreferencesMenu.ts @@ -1,16 +1,20 @@ import { IconType } from '@/components/Icon'; import { action, makeAutoObservable, observable } from 'mobx'; import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments'; -import { ContentType, SNComponent } from '@standardnotes/snjs'; +import { + ComponentArea, + ContentType, + FeatureIdentifier, + SNComponent, +} from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; -import { FeatureIdentifier } from '@standardnotes/features'; -import { ComponentArea } from '@standardnotes/snjs'; const PREFERENCE_IDS = [ 'general', 'account', 'appearance', 'security', + 'backups', 'listed', 'shortcuts', 'accessibility', @@ -37,6 +41,7 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'general', label: 'General', icon: 'settings' }, { id: 'appearance', label: 'Appearance', icon: 'themes' }, { id: 'security', label: 'Security', icon: 'security' }, + { id: 'backups', label: 'Backups', icon: 'restore' }, { id: 'listed', label: 'Listed', icon: 'listed' }, { id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' }, { id: 'accessibility', label: 'Accessibility', icon: 'accessibility' }, @@ -48,6 +53,7 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'account', label: 'Account', icon: 'user' }, { id: 'general', label: 'General', icon: 'settings' }, { id: 'security', label: 'Security', icon: 'security' }, + { id: 'backups', label: 'Backups', icon: 'restore' }, { id: 'listed', label: 'Listed', icon: 'listed' }, { id: 'help-feedback', label: 'Help & feedback', icon: 'help' }, ]; @@ -101,6 +107,7 @@ export class PreferencesMenu { FeatureIdentifier.TwoFactorAuthManager, 'org.standardnotes.batch-manager', 'org.standardnotes.extensions-manager', + FeatureIdentifier.CloudLink, ]; this._extensionPanes = ( this.application.getItems([ diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index cd58f05a3..dd8173135 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -17,6 +17,7 @@ import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; import { useEffect, useMemo } from 'preact/hooks'; import { ExtensionPane } from './panes/ExtensionPane'; +import { Backups } from '@/preferences/panes/Backups'; interface PreferencesProps extends MfaProps { application: WebApplication; @@ -26,16 +27,54 @@ interface PreferencesProps extends MfaProps { const PaneSelector: FunctionComponent< PreferencesProps & { menu: PreferencesMenu } -> = observer( - ({ - menu, - appState, - application, - mfaProvider, - userProvider - }) => { - switch (menu.selectedPaneId) { - case 'general': +> = observer(({ menu, appState, application, mfaProvider, userProvider }) => { + switch (menu.selectedPaneId) { + case 'general': + return ( + + ); + case 'account': + return ( + + ); + case 'appearance': + return null; + case 'security': + return ( + + ); + case 'backups': + return ; + case 'listed': + return ; + case 'shortcuts': + return null; + case 'accessibility': + return null; + case 'get-free-month': + return null; + case 'help-feedback': + return ; + default: + if (menu.selectedExtension != undefined) { + return ( + + ); + } else { return ( ); - case 'account': - return ( - - ); - case 'appearance': - return null; - case 'security': - return ( - - ); - case 'listed': - return ; - case 'shortcuts': - return null; - case 'accessibility': - return null; - case 'get-free-month': - return null; - case 'help-feedback': - return ; - default: - if (menu.selectedExtension != undefined) { - return ( - - ); - } else { - return ( - - ); - } - } - }); + } + } +}); const PreferencesCanvas: FunctionComponent< PreferencesProps & { menu: PreferencesMenu } @@ -105,8 +98,13 @@ const PreferencesCanvas: FunctionComponent< export const PreferencesView: FunctionComponent = observer( (props) => { const menu = useMemo( - () => new PreferencesMenu(props.application, props.appState.enableUnfinishedFeatures), - [props.appState.enableUnfinishedFeatures, props.application]); + () => + new PreferencesMenu( + props.application, + props.appState.enableUnfinishedFeatures + ), + [props.appState.enableUnfinishedFeatures, props.application] + ); useEffect(() => { menu.selectPane(props.appState.preferences.currentPane); diff --git a/app/assets/javascripts/preferences/panes/Backups.tsx b/app/assets/javascripts/preferences/panes/Backups.tsx new file mode 100644 index 000000000..e4b5c03d6 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/Backups.tsx @@ -0,0 +1,23 @@ +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { FunctionComponent } from 'preact'; +import { PreferencesPane } from '../components'; +import { CloudLink, DataBackups, EmailBackups } from './backups-segments'; + +interface Props { + appState: AppState; + application: WebApplication; +} + +export const Backups: FunctionComponent = ({ + application, + appState, +}) => { + return ( + + + + + + ); +}; diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index 007dad57f..900940d7a 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; -import { Encryption, PasscodeLock, Protections, DataBackups } from './security-segments'; +import { Encryption, PasscodeLock, Protections } from './security-segments'; import { TwoFactorAuthWrapper } from './two-factor-auth'; import { MfaProps } from './two-factor-auth/MfaProps'; @@ -20,6 +20,5 @@ export const Security: FunctionComponent = (props) => ( userProvider={props.userProvider} /> - ); diff --git a/app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx b/app/assets/javascripts/preferences/panes/backups-segments/DataBackups.tsx similarity index 67% rename from app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx rename to app/assets/javascripts/preferences/panes/backups-segments/DataBackups.tsx index 9dec8bcbc..2cedea433 100644 --- a/app/assets/javascripts/preferences/panes/security-segments/DataBackups.tsx +++ b/app/assets/javascripts/preferences/panes/backups-segments/DataBackups.tsx @@ -5,32 +5,68 @@ import { STRING_INVALID_IMPORT_FILE, STRING_IMPORTING_ZIP_FILE, STRING_UNSUPPORTED_BACKUP_FILE_VERSION, - StringImportError + StringImportError, + STRING_E2E_ENABLED, + STRING_LOCAL_ENC_ENABLED, + STRING_ENC_NOT_ENABLED, } from '@/strings'; import { BackupFile } from '@standardnotes/snjs'; -import { useRef, useState } from 'preact/hooks'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { WebApplication } from '@/ui_models/application'; import { JSXInternal } from 'preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; -import { PreferencesGroup, PreferencesSegment, Title, Text, Subtitle } from '../../components'; +import { + PreferencesGroup, + PreferencesSegment, + Title, + Text, + Subtitle, +} from '../../components'; import { Button } from '@/components/Button'; type Props = { application: WebApplication; appState: AppState; -} - -export const DataBackups = observer(({ - application, - appState -}: Props) => { +}; +export const DataBackups = observer(({ application, appState }: Props) => { const fileInputRef = useRef(null); const [isImportDataLoading, setIsImportDataLoading] = useState(false); + const { + isBackupEncrypted, + isEncryptionEnabled, + setIsBackupEncrypted, + setIsEncryptionEnabled, + setEncryptionStatusString, + } = appState.accountMenu; - const { isBackupEncrypted, isEncryptionEnabled, setIsBackupEncrypted } = appState.accountMenu; + const refreshEncryptionStatus = useCallback(() => { + const hasUser = application.hasAccount(); + const hasPasscode = application.hasPasscode(); + + const encryptionEnabled = hasUser || hasPasscode; + + const encryptionStatusString = hasUser + ? STRING_E2E_ENABLED + : hasPasscode + ? STRING_LOCAL_ENC_ENABLED + : STRING_ENC_NOT_ENABLED; + + setEncryptionStatusString(encryptionStatusString); + setIsEncryptionEnabled(encryptionEnabled); + setIsBackupEncrypted(encryptionEnabled); + }, [ + application, + setEncryptionStatusString, + setIsBackupEncrypted, + setIsEncryptionEnabled, + ]); + + useEffect(() => { + refreshEncryptionStatus(); + }, [refreshEncryptionStatus]); const downloadDataArchive = () => { application.getArchiveService().downloadBackup(isBackupEncrypted); @@ -74,12 +110,14 @@ export const DataBackups = observer(({ statusText = StringImportError(result.errorCount); } void alertDialog({ - text: statusText + text: statusText, }); }; - const importFileSelected = async (event: TargetedEvent) => { - const { files } = (event.target as HTMLInputElement); + const importFileSelected = async ( + event: TargetedEvent + ) => { + const { files } = event.target as HTMLInputElement; if (!files) { return; @@ -90,15 +128,14 @@ export const DataBackups = observer(({ return; } - const version = data.version || data.keyParams?.version || data.auth_params?.version; + const version = + data.version || data.keyParams?.version || data.auth_params?.version; if (!version) { await performImport(data); return; } - if ( - application.protocolService.supportedVersions().includes(version) - ) { + if (application.protocolService.supportedVersions().includes(version)) { await performImport(data); } else { setIsImportDataLoading(false); @@ -107,7 +144,9 @@ export const DataBackups = observer(({ }; // Whenever "Import Backup" is either clicked or key-pressed, proceed the import - const handleImportFile = (event: TargetedEvent | KeyboardEvent) => { + const handleImportFile = ( + event: TargetedEvent | KeyboardEvent + ) => { if (event instanceof KeyboardEvent) { const { code } = event; @@ -161,26 +200,33 @@ export const DataBackups = observer(({ )} -