diff --git a/.eslintrc b/.eslintrc index b572d609c..21f273635 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,7 +11,7 @@ "parserOptions": { "project": "./app/assets/javascripts/tsconfig.json" }, - "ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__"], + "ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js"], "rules": { "standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals "no-throw-literal": 0, diff --git a/.gitignore b/.gitignore index 6db1cb540..baec9d89b 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,3 @@ yarn-error.log package-lock.json codeqldb - -coverage diff --git a/app/assets/javascripts/__mocks__/@standardnotes/snjs.js b/app/assets/javascripts/__mocks__/@standardnotes/snjs.js deleted file mode 100644 index a89d29416..000000000 --- a/app/assets/javascripts/__mocks__/@standardnotes/snjs.js +++ /dev/null @@ -1,11 +0,0 @@ -const { - ApplicationEvent, - ProtectionSessionDurations, - ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, -} = require('@standardnotes/snjs'); - -module.exports = { - ApplicationEvent: ApplicationEvent, - ProtectionSessionDurations: ProtectionSessionDurations, - ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, -}; diff --git a/app/assets/javascripts/components/AccountMenu/DataBackup.tsx b/app/assets/javascripts/components/AccountMenu/DataBackup.tsx new file mode 100644 index 000000000..808a27616 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/DataBackup.tsx @@ -0,0 +1,180 @@ +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 '@standardnotes/snjs'; +import { 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'; + +type Props = { + application: WebApplication; + appState: AppState; +} + +const DataBackup = observer(({ + application, + appState + }: Props) => { + + const fileInputRef = useRef(null); + const [isImportDataLoading, setIsImportDataLoading] = useState(false); + + const { isBackupEncrypted, isEncryptionEnabled, setIsBackupEncrypted } = appState.accountMenu; + + 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 }); + } + }; + + // Whenever "Import Backup" is either clicked or key-pressed, proceed the import + const handleImportFile = (event: TargetedEvent | KeyboardEvent) => { + if (event instanceof KeyboardEvent) { + const { code } = event; + + // Process only when "Enter" or "Space" keys are pressed + if (code !== 'Enter' && code !== 'Space') { + return; + } + // Don't proceed the event's default action + // (like scrolling in case the "space" key is pressed) + event.preventDefault(); + } + + (fileInputRef.current as HTMLInputElement).click(); + }; + + 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/Encryption.tsx b/app/assets/javascripts/components/AccountMenu/Encryption.tsx new file mode 100644 index 000000000..1a98f2404 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Encryption.tsx @@ -0,0 +1,33 @@ +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; + +type Props = { + appState: AppState; +} + +const Encryption = observer(({ appState }: Props) => { + const { isEncryptionEnabled, encryptionStatusString, notesAndTagsCount } = appState.accountMenu; + + 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/ErrorReporting.tsx b/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx new file mode 100644 index 000000000..92784557e --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/ErrorReporting.tsx @@ -0,0 +1,80 @@ +import { useState } from '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 {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 new file mode 100644 index 000000000..cdfbbc046 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -0,0 +1,272 @@ +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, + Strings +} from '@/strings'; +import { WebApplication } from '@/ui_models/application'; +import { preventRefreshing } from '@/utils'; +import { JSXInternal } from 'preact/src/jsx'; +import TargetedEvent = JSXInternal.TargetedEvent; +import { alertDialog } from '@Services/alertService'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { ApplicationEvent } from '@standardnotes/snjs'; +import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; +import { observer } from 'mobx-react-lite'; +import { AppState } from '@/ui_models/app_state'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +const PasscodeLock = observer(({ + application, + appState, + }: Props) => { + const keyStorageInfo = StringUtils.keyStorageInfo(application); + const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions(); + + const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } = appState.accountMenu; + + const passcodeInputRef = useRef(null); + + 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 = useCallback(() => { + const hasUser = application.hasAccount(); + const hasPasscode = application.hasPasscode(); + + setHasPasscode(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]); + + 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 || passcode.length === 0) { + await alertDialog({ + text: Strings.enterPasscode, + }); + } + + 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(() => { + const removeKeyStatusChangedObserver = application.addEventObserver( + async () => { + setCanAddPasscode(!application.isEphemeralSession()); + setHasPasscode(application.hasPasscode()); + setShowPasscodeForm(false); + }, + ApplicationEvent.KeyStatusChanged + ); + + return () => { + removeKeyStatusChangedObserver(); + }; + }, [application]); + + 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..8e7b1f229 --- /dev/null +++ b/app/assets/javascripts/components/AccountMenu/Protections.tsx @@ -0,0 +1,100 @@ +import { WebApplication } from '@/ui_models/application'; +import { FunctionalComponent } from 'preact'; +import { useCallback, useState } from 'preact/hooks'; +import { useEffect } from 'preact/hooks'; +import { ApplicationEvent } from '@standardnotes/snjs'; +import { isSameDay } from '@/utils'; + +type Props = { + application: WebApplication; +}; + +const Protections: FunctionalComponent = ({ application }) => { + const enableProtections = () => { + application.clearProtectionSession(); + }; + + const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources()); + + const getProtectionsDisabledUntil = useCallback((): 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; + }, [application]); + + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); + + useEffect(() => { + const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( + async () => { + setProtectionsDisabledUntil(getProtectionsDisabledUntil()); + }, + ApplicationEvent.ProtectionSessionExpiryDateChanged + ); + + const removeKeyStatusChangedObserver = application.addEventObserver( + async () => { + setHasProtections(application.hasProtectionSources()); + }, + ApplicationEvent.KeyStatusChanged + ); + + return () => { + removeProtectionSessionExpiryDateChangedObserver(); + removeKeyStatusChangedObserver(); + }; + }, [application, getProtectionsDisabledUntil]); + + if (!hasProtections) { + return null; + } + + 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/NoProtectionsNoteWarning.tsx b/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx index 243896f4f..e3e9ec291 100644 --- a/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx +++ b/app/assets/javascripts/components/NoProtectionsNoteWarning.tsx @@ -1,41 +1,27 @@ import { AppState } from '@/ui_models/app_state'; import { toDirective } from './utils'; -type Props = { - appState: AppState; - onViewNote: () => void; - requireAuthenticationForProtectedNote: boolean; -}; - -function NoProtectionsNoteWarning({ - appState, - onViewNote, - requireAuthenticationForProtectedNote, -}: Props) { - const instructionText = requireAuthenticationForProtectedNote - ? 'Authenticate to view this note.' - : 'Add a passcode or create an account to require authentication to view this note.'; +type Props = { appState: AppState; onViewNote: () => void }; +function NoProtectionsNoteWarning({ appState, onViewNote }: Props) { return (

This note is protected

-

{instructionText}

+

+ Add a passcode or create an account to require authentication to view + this note. +

- {!requireAuthenticationForProtectedNote && ( - - )} +
@@ -46,6 +32,5 @@ export const NoProtectionsdNoteWarningDirective = toDirective( NoProtectionsNoteWarning, { onViewNote: '&', - requireAuthenticationForProtectedNote: '=', } ); diff --git a/app/assets/javascripts/components/SearchOptions.tsx b/app/assets/javascripts/components/SearchOptions.tsx index a67dfcc60..b62989b7d 100644 --- a/app/assets/javascripts/components/SearchOptions.tsx +++ b/app/assets/javascripts/components/SearchOptions.tsx @@ -1,7 +1,7 @@ import { AppState } from '@/ui_models/app_state'; import { Icon } from './Icon'; import { toDirective, useCloseOnBlur } from './utils'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useRef, useState } from 'preact/hooks'; import { WebApplication } from '@/ui_models/application'; import VisuallyHidden from '@reach/visually-hidden'; import { @@ -11,6 +11,7 @@ import { } from '@reach/disclosure'; import { Switch } from './Switch'; import { observer } from 'mobx-react-lite'; +import { useEffect } from 'react'; type Props = { appState: AppState; diff --git a/app/assets/javascripts/components/utils.ts b/app/assets/javascripts/components/utils.ts index 3e4f9b631..652ee79e7 100644 --- a/app/assets/javascripts/components/utils.ts +++ b/app/assets/javascripts/components/utils.ts @@ -1,6 +1,7 @@ import { FunctionComponent, h, render } from 'preact'; import { unmountComponentAtNode } from 'preact/compat'; -import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks'; +import { StateUpdater, useCallback, useState } from 'preact/hooks'; +import { useEffect } from 'react'; /** * @returns a callback that will close a dropdown if none of its children has diff --git a/app/assets/javascripts/jest.config.js b/app/assets/javascripts/jest.config.js index b0733b204..e985f0c59 100644 --- a/app/assets/javascripts/jest.config.js +++ b/app/assets/javascripts/jest.config.js @@ -1,13 +1,10 @@ -const pathsToModuleNameMapper = - require('ts-jest/utils').pathsToModuleNameMapper; +const pathsToModuleNameMapper = require('ts-jest/utils').pathsToModuleNameMapper; const tsConfig = require('./tsconfig.json'); const pathsFromTsconfig = tsConfig.compilerOptions.paths; module.exports = { - restoreMocks: true, clearMocks: true, - resetMocks: true, moduleNameMapper: { ...pathsToModuleNameMapper(pathsFromTsconfig, { prefix: '', @@ -17,6 +14,7 @@ module.exports = { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, globals: { + window: {}, __VERSION__: '1.0.0', __DESKTOP__: false, __WEB__: true, diff --git a/app/assets/javascripts/preferences/panes/account/Sync.tsx b/app/assets/javascripts/preferences/panes/account/Sync.tsx index b7d6147bd..e8b465b39 100644 --- a/app/assets/javascripts/preferences/panes/account/Sync.tsx +++ b/app/assets/javascripts/preferences/panes/account/Sync.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/Button'; import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs'; import { STRING_GENERIC_SYNC_ERROR } from '@/strings'; import { useState } from '@node_modules/preact/hooks'; -import { observer } from 'mobx-react-lite'; +import { observer } from '@node_modules/mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; import { FunctionComponent } from 'preact'; diff --git a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx index f0e564274..2cf2caabb 100644 --- a/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx +++ b/app/assets/javascripts/preferences/panes/security-segments/Protections.tsx @@ -4,12 +4,7 @@ import { useCallback, useState } from 'preact/hooks'; import { useEffect } from 'preact/hooks'; import { ApplicationEvent } from '@standardnotes/snjs'; import { isSameDay } from '@/utils'; -import { - PreferencesGroup, - PreferencesSegment, - Title, - Text, -} from '@/preferences/components'; +import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/preferences/components'; import { Button } from '@/components/Button'; type Props = { @@ -21,9 +16,7 @@ export const Protections: FunctionalComponent = ({ application }) => { application.clearProtectionSession(); }; - const [hasProtections, setHasProtections] = useState(() => - application.hasProtectionSources() - ); + const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources()); const getProtectionsDisabledUntil = useCallback((): string | null => { const protectionExpiry = application.getProtectionSessionExpiryDate(); @@ -33,7 +26,7 @@ export const Protections: FunctionalComponent = ({ application }) => { if (isSameDay(protectionExpiry, now)) { f = new Intl.DateTimeFormat(undefined, { hour: 'numeric', - minute: 'numeric', + minute: 'numeric' }); } else { f = new Intl.DateTimeFormat(undefined, { @@ -41,7 +34,7 @@ export const Protections: FunctionalComponent = ({ application }) => { day: 'numeric', month: 'short', hour: 'numeric', - minute: 'numeric', + minute: 'numeric' }); } @@ -50,23 +43,14 @@ export const Protections: FunctionalComponent = ({ application }) => { return null; }, [application]); - const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState( - getProtectionsDisabledUntil() - ); + const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil()); useEffect(() => { - const removeUnprotectedSessionBeginObserver = application.addEventObserver( + const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver( async () => { setProtectionsDisabledUntil(getProtectionsDisabledUntil()); }, - ApplicationEvent.UnprotectedSessionBegan - ); - - const removeUnprotectedSessionEndObserver = application.addEventObserver( - async () => { - setProtectionsDisabledUntil(getProtectionsDisabledUntil()); - }, - ApplicationEvent.UnprotectedSessionExpired + ApplicationEvent.ProtectionSessionExpiryDateChanged ); const removeKeyStatusChangedObserver = application.addEventObserver( @@ -77,8 +61,7 @@ export const Protections: FunctionalComponent = ({ application }) => { ); return () => { - removeUnprotectedSessionBeginObserver(); - removeUnprotectedSessionEndObserver(); + removeProtectionSessionExpiryDateChangedObserver(); removeKeyStatusChangedObserver(); }; }, [application, getProtectionsDisabledUntil]); @@ -91,28 +74,19 @@ export const Protections: FunctionalComponent = ({ application }) => { Protections - {protectionsDisabledUntil ? ( - - Unprotected access expires at {protectionsDisabledUntil}. - - ) : ( - Protections are enabled. - )} + {protectionsDisabledUntil + ? Protections are disabled until {protectionsDisabledUntil}. + : Protections are enabled. + } - Actions like viewing or searching protected notes, exporting decrypted - backups, or revoking an active session require additional - authentication such as entering your account password or application - passcode. + 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 && ( -