refactor: separate DataBackup and ErrorReporting components, remove the original (large) AccountMenu component

This commit is contained in:
VardanHakobyan
2021-06-14 18:40:07 +04:00
parent 41a21e6ca7
commit 42ea6a1ea9
7 changed files with 263 additions and 1271 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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<string | undefined>;
server: string | undefined;
setServer: StateUpdater<string | undefined>;
closeAccountMenu: () => void;
@@ -33,10 +28,7 @@ type Props = {
}
const Authentication: FC<Props> = ({
// const Authentication = observer(({
application,
// url,
// setUrl,
server,
setServer,
closeAccountMenu,
@@ -52,7 +44,6 @@ const Authentication: FC<Props> = ({
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// const [passwordConfirmation, setPasswordConfirmation] = useState<string | undefined>(undefined);
const [passwordConfirmation, setPasswordConfirmation] = useState<string>('');
const [status, setStatus] = useState<string | undefined>(undefined);
const [isEmailFocused, setIsEmailFocused] = useState(false);
@@ -61,7 +52,6 @@ const Authentication: FC<Props> = ({
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<Props> = ({
useEffect(() => {
if (!showLogin && !showRegister) {
setPassword('');
// setPasswordConfirmation(undefined); // TODO: Vardan: maybe change this to empty string as for password?
setPasswordConfirmation('');
}
}, [showLogin, showRegister]);

View File

@@ -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<boolean>;
}
const DataBackup: FC<Props> = ({
application,
isBackupEncrypted,
isEncryptionEnabled,
setIsBackupEncrypted
}) => {
const [isImportDataLoading, setIsImportDataLoading] = useState(false);
const downloadDataArchive = () => {
application.getArchiveService().downloadBackup(isBackupEncrypted);
};
const readFile = async (file: File): Promise<any> => {
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<HTMLInputElement, Event>) => {
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 ? (
<div className="sk-spinner small info" />
) : (
<div className="sk-panel-section">
<div className="sk-panel-section-title">Data Backups</div>
<div className="sk-p">Download a backup of all your data.</div>
{isEncryptionEnabled && (
<form className="sk-panel-form sk-panel-row">
<div className="sk-input-group">
<label className="sk-horizontal-group tight">
<input
type="radio"
onChange={() => setIsBackupEncrypted(true)}
checked={isBackupEncrypted}
/>
<p className="sk-p">Encrypted</p>
</label>
<label className="sk-horizontal-group tight">
<input
type="radio"
onChange={() => setIsBackupEncrypted(false)}
checked={!isBackupEncrypted}
/>
<p className="sk-p">Decrypted</p>
</label>
</div>
</form>
)}
<div className="sk-panel-row" />
<div className="flex">
<button className="sn-button small info" onClick={downloadDataArchive}>Download Backup</button>
<label className="sn-button small flex items-center info ml-2">
<input
type="file"
onChange={importFileSelected}
style={{ display: 'none' }}
/>
Import Backup
</label>
</div>
{isDesktopApplication() && (
<p className="mt-5">
Backups are automatically created on desktop and can be managed
via the "Backups" top-level menu.
</p>
)}
<div className="sk-panel-row" />
</div>
)}
</>
);
};
export default DataBackup;

View File

@@ -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 <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
<br><br>
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 (
<div className="sk-panel-section">
<div className="sk-panel-section-title">Error Reporting</div>
<div className="sk-panel-section-subtitle info">
Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'}
</div>
<p className="sk-p">
Help us improve Standard Notes by automatically submitting
anonymized error reports.
</p>
{errorReportingIdValue && (
<>
<p className="sk-p selectable">
Your random identifier is
strong {errorReportingIdValue}
</p>
<p className="sk-p">
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.
</p>
</>
)}
<div className="sk-panel-row">
<button className="sn-button small info" onClick={toggleErrorReportingEnabled}>
{isErrorReportingEnabled ? 'Disable' : 'Enable'} Error Reporting
</button>
</div>
<div className="sk-panel-row">
<a className="sk-a" onClick={openErrorReportingDialog}>What data is being sent?</a>
</div>
</div>
);
});
export default ErrorReporting;

View File

@@ -26,7 +26,7 @@ const PasscodeLock: FC<Props> = ({
application,
setEncryptionStatusString,
setIsEncryptionEnabled,
setIsBackupEncrypted,
setIsBackupEncrypted
}) => {
const keyStorageInfo = StringUtils.keyStorageInfo(application);
const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions();
@@ -56,7 +56,7 @@ const PasscodeLock: FC<Props> = ({
setSelectedAutoLockInterval(interval);
}, [application]);
const refreshEncryptionStatus = () => {
const refreshEncryptionStatus = useCallback(() => {
const hasUser = application.hasAccount();
const hasPasscode = application.hasPasscode();
@@ -64,16 +64,16 @@ const PasscodeLock: FC<Props> = ({
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<Props> = ({
// 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<Props> = ({
return () => {
removeKeyStatusChangedObserver();
}
})
};
});
return (
<div className="sk-panel-section">
@@ -264,6 +263,6 @@ const PasscodeLock: FC<Props> = ({
)}
</div>
);
}
};
export default PasscodeLock;

View File

@@ -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<string | undefined>(undefined);
const [encryptionStatusString, setEncryptionStatusString] = useState<string | undefined>(undefined);
const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false);
const [server, setServer] = useState<string | undefined>(application.getHost());
// const [url, setUrl] = useState<string | undefined>(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<any> => {
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<HTMLInputElement, Event>) => {
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 <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
<br><br>
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 (
<div className="sn-component">
@@ -244,8 +95,6 @@ const AccountMenu = observer(({ application, appState }: Props) => {
<div className="sk-panel-content">
<Authentication
application={application}
// url={url}
// setUrl={setUrl}
server={server}
setServer={setServer}
closeAccountMenu={closeAccountMenu}
@@ -284,86 +133,13 @@ const AccountMenu = observer(({ application, appState }: Props) => {
setIsEncryptionEnabled={setIsEncryptionEnabled}
setIsBackupEncrypted={setIsBackupEncrypted}
/>
{isImportDataLoading ? (
<div className="sk-spinner small info" />
) : (
<div className="sk-panel-section">
<div className="sk-panel-section-title">Data Backups</div>
<div className="sk-p">Download a backup of all your data.</div>
{isEncryptionEnabled && (
<form className="sk-panel-form sk-panel-row">
<div className="sk-input-group">
<label className="sk-horizontal-group tight">
<input
type="radio"
onChange={() => setIsBackupEncrypted(true)}
checked={isBackupEncrypted}
/>
<p className="sk-p">Encrypted</p>
</label>
<label className="sk-horizontal-group tight">
<input
type="radio"
onChange={() => setIsBackupEncrypted(false)}
checked={!isBackupEncrypted}
/>
<p className="sk-p">Decrypted</p>
</label>
</div>
</form>
)}
<div className="sk-panel-row" />
<div className="flex">
<button className="sn-button small info" onClick={downloadDataArchive}>Download Backup</button>
<label className="sn-button small flex items-center info ml-2">
<input
type="file"
onChange={importFileSelected}
style={{ display: 'none' }}
/>
Import Backup
</label>
</div>
{isDesktopApplication() && (
<p className="mt-5">
Backups are automatically created on desktop and can be managed
via the "Backups" top-level menu.
</p>
)}
<div className="sk-panel-row" />
</div>
)}
<div className="sk-panel-section">
<div className="sk-panel-section-title">Error Reporting</div>
<div className="sk-panel-section-subtitle info">
Automatic error reporting is {isErrorReportingEnabled ? 'enabled' : 'disabled'}
</div>
<p className="sk-p">
Help us improve Standard Notes by automatically submitting
anonymized error reports.
</p>
{errorReportingIdValue && (
<>
<p className="sk-p selectable">
Your random identifier is
strong {errorReportingIdValue}
</p>
<p className="sk-p">
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.
</p>
</>
)}
<div className="sk-panel-row">
<button className="sn-button small info" onClick={toggleErrorReportingEnabled}>
{isErrorReportingEnabled ? 'Disable' : 'Enable'} Error Reporting
</button>
</div>
<div className="sk-panel-row">
<a className="sk-a" onClick={openErrorReportingDialog}>What data is being sent?</a>
</div>
</div>
<DataBackup
application={application}
isBackupEncrypted={isBackupEncrypted}
isEncryptionEnabled={isEncryptionEnabled}
setIsBackupEncrypted={setIsBackupEncrypted}
/>
<ErrorReporting appState={appState} />
</div>
)}
</div>