From fab9ca2ad212d39cf5aa38a0e568595077c236f3 Mon Sep 17 00:00:00 2001 From: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com> Date: Wed, 3 Feb 2021 11:53:52 +0100 Subject: [PATCH] fix: hide account warning after login + improve key storage wording --- .../components/NoAccountWarning.tsx | 11 +- .../directives/views/accountMenu.ts | 33 +++-- app/assets/javascripts/strings.ts | 128 ++++++++++++------ app/assets/javascripts/ui_models/app_state.ts | 41 +++++- .../javascripts/ui_models/application.ts | 4 +- app/assets/javascripts/utils.ts | 6 + app/assets/stylesheets/_main.scss | 2 + .../templates/directives/account-menu.pug | 2 + 8 files changed, 156 insertions(+), 71 deletions(-) diff --git a/app/assets/javascripts/components/NoAccountWarning.tsx b/app/assets/javascripts/components/NoAccountWarning.tsx index aeb26dd3d..3e5eeabc2 100644 --- a/app/assets/javascripts/components/NoAccountWarning.tsx +++ b/app/assets/javascripts/components/NoAccountWarning.tsx @@ -1,17 +1,10 @@ -import { WebApplication } from '@/ui_models/application'; import { toDirective, useAutorunValue } from './utils'; import Close from '../../icons/ic_close.svg'; import { AppState } from '@/ui_models/app_state'; -function NoAccountWarning({ - application, - appState, -}: { - application: WebApplication; - appState: AppState; -}) { +function NoAccountWarning({ appState }: { appState: AppState }) { const canShow = useAutorunValue(() => appState.noAccountWarning.show); - if (!canShow || application.hasAccount()) { + if (!canShow) { return null; } return ( diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts index 2f53b42ac..944a317a0 100644 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -17,10 +17,11 @@ import { StringImportError, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, - STRING_UNSUPPORTED_BACKUP_FILE_VERSION + STRING_UNSUPPORTED_BACKUP_FILE_VERSION, + Strings } from '@/strings'; import { PasswordWizardType } from '@/types'; -import { BackupFile, ContentType } from '@standardnotes/snjs'; +import { BackupFile, ContentType, Platform } from '@standardnotes/snjs'; import { confirmDialog, alertDialog } from '@/services/alertService'; import { autorun, IReactionDisposer } from 'mobx'; import { storage, StorageKey } from '@/services/localStorage'; @@ -30,8 +31,6 @@ import { errorReportingId } from '@/services/errorReporting'; -const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request'; - const ELEMENT_NAME_AUTH_EMAIL = 'email'; const ELEMENT_NAME_AUTH_PASSWORD = 'password'; const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf'; @@ -62,16 +61,17 @@ type AccountMenuState = { user: any; mutable: any; importData: any; - encryptionStatusString: string; - server: string; - encryptionEnabled: boolean; - selectedAutoLockInterval: any; + encryptionStatusString?: string; + server?: string; + encryptionEnabled?: boolean; + selectedAutoLockInterval?: unknown; showBetaWarning: boolean; errorReportingEnabled: boolean; syncInProgress: boolean; - syncError: string; + syncError?: string; showSessions: boolean; errorReportingId: string | null; + keyStorageInfo: string | null; } class AccountMenuCtrl extends PureViewCtrl { @@ -95,8 +95,8 @@ class AccountMenuCtrl extends PureViewCtrl { getInitialState() { return { appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion), - passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(), - user: this.application!.getUser(), + passcodeAutoLockOptions: this.application.getAutolockService().getAutoLockIntervalOptions(), + user: this.application.getUser(), formData: { mergeLocal: true, ephemeral: false, @@ -106,7 +106,10 @@ class AccountMenuCtrl extends PureViewCtrl { errorReportingEnabled: storage.get(StorageKey.DisableErrorReporting) === false, showSessions: false, errorReportingId: errorReportingId(), - } as AccountMenuState; + keyStorageInfo: Strings.keyStorageInfo(this.application), + importData: null, + syncInProgress: false, + }; } getState() { @@ -128,9 +131,9 @@ class AccountMenuCtrl extends PureViewCtrl { refreshedCredentialState() { return { - user: this.application!.getUser(), - canAddPasscode: !this.application!.isEphemeralSession(), - hasPasscode: this.application!.hasPasscode(), + user: this.application.getUser(), + canAddPasscode: !this.application.isEphemeralSession(), + hasPasscode: this.application.hasPasscode(), showPasscodeForm: false }; } diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index cf2c79458..c8782ae29 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -1,27 +1,45 @@ +import { Platform, SNApplication } from '@standardnotes/snjs'; +import { getPlatform, isDesktopApplication } from './utils'; + /** @generic */ -export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign in to refresh your session."; -export const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe."; -export const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in."; +export const STRING_SESSION_EXPIRED = + 'Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.'; +export const STRING_DEFAULT_FILE_ERROR = + 'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.'; +export const STRING_GENERIC_SYNC_ERROR = + 'There was an error syncing. Please try again. If all else fails, try signing out and signing back in.'; export function StringSyncException(data: any) { - return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify(data)}.`; + return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify( + data + )}.`; } /** @footer */ -export const STRING_NEW_UPDATE_READY = "A new update is ready to install. Please use the top-level 'Updates' menu to manage installation."; +export const STRING_NEW_UPDATE_READY = + "A new update is ready to install. Please use the top-level 'Updates' menu to manage installation."; /** @tags */ -export const STRING_DELETE_TAG = "Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes."; +export const STRING_DELETE_TAG = + 'Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.'; /** @editor */ -export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN = 'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'; -export const STRING_DELETED_NOTE = "The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded."; -export const STRING_INVALID_NOTE = "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."; -export const STRING_ELLIPSES = "..."; -export const STRING_GENERIC_SAVE_ERROR = "There was an error saving your note. Please try again."; -export const STRING_DELETE_PLACEHOLDER_ATTEMPT = "This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note."; -export const STRING_ARCHIVE_LOCKED_ATTEMPT = "This note is locked. If you'd like to archive it, unlock it, and try again."; -export const STRING_UNARCHIVE_LOCKED_ATTEMPT = "This note is locked. If you'd like to archive it, unlock it, and try again."; -export const STRING_DELETE_LOCKED_ATTEMPT = "This note is locked. If you'd like to delete it, unlock it, and try again."; +export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN = + 'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'; +export const STRING_DELETED_NOTE = + 'The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded.'; +export const STRING_INVALID_NOTE = + "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."; +export const STRING_ELLIPSES = '...'; +export const STRING_GENERIC_SAVE_ERROR = + 'There was an error saving your note. Please try again.'; +export const STRING_DELETE_PLACEHOLDER_ATTEMPT = + 'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.'; +export const STRING_ARCHIVE_LOCKED_ATTEMPT = + "This note is locked. If you'd like to archive it, unlock it, and try again."; +export const STRING_UNARCHIVE_LOCKED_ATTEMPT = + "This note is locked. If you'd like to archive it, unlock it, and try again."; +export const STRING_DELETE_LOCKED_ATTEMPT = + "This note is locked. If you'd like to delete it, unlock it, and try again."; export function StringDeleteNote(title: string, permanently: boolean) { return permanently ? `Are you sure you want to permanently delete ${title}?` @@ -32,44 +50,78 @@ export function StringEmptyTrash(count: number) { } /** @account */ -export const STRING_ACCOUNT_MENU_UNCHECK_MERGE = "Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?"; -export const STRING_SIGN_OUT_CONFIRMATION = "Are you sure you want to end your session? This will delete all local items and extensions."; -export const STRING_ERROR_DECRYPTING_IMPORT = "There was an error decrypting your items. Make sure the password you entered is correct and try again."; -export const STRING_E2E_ENABLED = "End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud."; -export const STRING_LOCAL_ENC_ENABLED = "Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage."; -export const STRING_ENC_NOT_ENABLED = "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption."; -export const STRING_IMPORT_SUCCESS = "Your data has been successfully imported."; -export const STRING_REMOVE_PASSCODE_CONFIRMATION = "Are you sure you want to remove your application passcode?"; -export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = " This will remove encryption from your local data."; -export const STRING_NON_MATCHING_PASSCODES = "The two passcodes you entered do not match. Please try again."; -export const STRING_NON_MATCHING_PASSWORDS = "The two passwords you entered do not match. Please try again."; -export const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys..."; -export const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys..."; -export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again."; +export const STRING_ACCOUNT_MENU_UNCHECK_MERGE = + 'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?'; +export const STRING_SIGN_OUT_CONFIRMATION = + 'Are you sure you want to end your session? This will delete all local items and extensions.'; +export const STRING_ERROR_DECRYPTING_IMPORT = + 'There was an error decrypting your items. Make sure the password you entered is correct and try again.'; +export const STRING_E2E_ENABLED = + 'End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud.'; +export const STRING_LOCAL_ENC_ENABLED = + 'Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.'; +export const STRING_ENC_NOT_ENABLED = + 'Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.'; +export const STRING_IMPORT_SUCCESS = + 'Your data has been successfully imported.'; +export const STRING_REMOVE_PASSCODE_CONFIRMATION = + 'Are you sure you want to remove your application passcode?'; +export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = + ' This will remove encryption from your local data.'; +export const STRING_NON_MATCHING_PASSCODES = + 'The two passcodes you entered do not match. Please try again.'; +export const STRING_NON_MATCHING_PASSWORDS = + 'The two passwords you entered do not match. Please try again.'; +export const STRING_GENERATING_LOGIN_KEYS = 'Generating Login Keys...'; +export const STRING_GENERATING_REGISTER_KEYS = 'Generating Account Keys...'; +export const STRING_INVALID_IMPORT_FILE = + 'Unable to open file. Ensure it is a proper JSON file and try again.'; export function StringImportError(errorCount: number) { return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`; } -export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.'; +export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = + 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.'; /** @password_change */ -export const STRING_FAILED_PASSWORD_CHANGE = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup."; +export const STRING_FAILED_PASSWORD_CHANGE = + 'There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.'; export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE = - "The encryption upgrade is in progress. You may lose data if you quit the app. " + - "Are you sure you want to quit?"; + 'The encryption upgrade is in progress. You may lose data if you quit the app. ' + + 'Are you sure you want to quit?'; export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE = - "A passcode change is in progress. You may lose data if you quit the app. " + - "Are you sure you want to quit?"; + 'A passcode change is in progress. You may lose data if you quit the app. ' + + 'Are you sure you want to quit?'; export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL = - "A passcode removal is in progress. You may lose data if you quit the app. " + - "Are you sure you want to quit?"; + 'A passcode removal is in progress. You may lose data if you quit the app. ' + + 'Are you sure you want to quit?'; -export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available'; +export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = + 'Encryption upgrade available'; export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = 'Encryption version 004 is available. ' + 'This version strengthens the encryption algorithms your account and ' + 'local storage use. To learn more about this upgrade, visit our ' + 'Security Upgrade page.'; export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade'; + +export const Strings = { + keyStorageInfo(application: SNApplication): string | null { + if (!isDesktopApplication()) { + return null; + } + if (!application.hasAccount()) { + return null; + } + const platform = getPlatform(); + const keychainName = + platform === Platform.WindowsDesktop + ? 'credential manager' + : platform === Platform.MacDesktop + ? 'keychain' + : 'password manager'; + return `Your keys are currently stored in your operating system's ${keychainName}. Adding a passcode prevents even your operating system from reading them.`; + }, +}; diff --git a/app/assets/javascripts/ui_models/app_state.ts b/app/assets/javascripts/ui_models/app_state.ts index 8ab3b443b..8748bb593 100644 --- a/app/assets/javascripts/ui_models/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state.ts @@ -10,10 +10,11 @@ import { UuidString, SyncOpStatus, PrefKey, + SNApplication, } from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { Editor } from '@/ui_models/editor'; -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, runInAction } from 'mobx'; import { Bridge } from '@/services/bridge'; import { storage, StorageKey } from '@/services/localStorage'; @@ -47,7 +48,7 @@ class ActionsMenuState { makeObservable(this, { hiddenExtensions: observable, toggleExtensionVisibility: action, - deinit: action, + reset: action, }); } @@ -55,7 +56,7 @@ class ActionsMenuState { this.hiddenExtensions[uuid] = !this.hiddenExtensions[uuid]; } - deinit() { + reset() { this.hiddenExtensions = {}; } } @@ -113,8 +114,26 @@ class AccountMenuState { class NoAccountWarningState { show: boolean; - constructor() { - this.show = storage.get(StorageKey.ShowNoAccountWarning) ?? true; + constructor(application: SNApplication, appObservers: (() => void)[]) { + this.show = application.hasAccount() + ? false + : storage.get(StorageKey.ShowNoAccountWarning) ?? true; + + appObservers.push( + application.addEventObserver(async () => { + runInAction(() => { + this.show = false; + }); + }, ApplicationEvent.SignedIn), + application.addEventObserver(async () => { + if (application.hasAccount()) { + runInAction(() => { + this.show = false; + }); + } + }, ApplicationEvent.Started) + ); + makeObservable(this, { show: observable, hide: action, @@ -146,10 +165,12 @@ export class AppState { showBetaWarning: boolean; readonly accountMenu = new AccountMenuState(); readonly actionsMenu = new ActionsMenuState(); - readonly noAccountWarning = new NoAccountWarningState(); + readonly noAccountWarning: NoAccountWarningState; readonly sync = new SyncState(); isSessionsModalVisible = false; + private appEventObserverRemovers: (() => void)[] = []; + /* @ngInject */ constructor( $rootScope: ng.IRootScopeService, @@ -160,6 +181,10 @@ export class AppState { this.$timeout = $timeout; this.$rootScope = $rootScope; this.application = application; + this.noAccountWarning = new NoAccountWarningState( + application, + this.appEventObserverRemovers + ); this.addAppEventObserver(); this.streamNotesAndTags(); this.onVisibilityChange = () => { @@ -193,10 +218,12 @@ export class AppState { storage.remove(StorageKey.ShowBetaWarning); this.noAccountWarning.reset(); } - this.actionsMenu.deinit(); + this.actionsMenu.reset(); this.unsubApp(); this.unsubApp = undefined; this.observers.length = 0; + this.appEventObserverRemovers.forEach((remover) => remover()); + this.appEventObserverRemovers.length = 0; if (this.rootScopeCleanup1) { this.rootScopeCleanup1(); this.rootScopeCleanup2(); diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index b18f6dc3c..2bc8b2c66 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -12,7 +12,7 @@ import { DeinitSource, } from '@standardnotes/snjs'; import angular from 'angular'; -import { getPlatformString } from '@/utils'; +import { getPlatform, getPlatformString } from '@/utils'; import { AlertService } from '@/services/alertService'; import { WebDeviceInterface } from '@/web_device_interface'; import { @@ -58,7 +58,7 @@ export class WebApplication extends SNApplication { ) { super( bridge.environment, - platformFromString(getPlatformString()), + getPlatform(), deviceInterface, WebCrypto, new AlertService(), diff --git a/app/assets/javascripts/utils.ts b/app/assets/javascripts/utils.ts index 8c94fe42e..06b08284b 100644 --- a/app/assets/javascripts/utils.ts +++ b/app/assets/javascripts/utils.ts @@ -1,3 +1,5 @@ +import { Platform, platformFromString } from "@standardnotes/snjs"; + declare const process : { env: { NODE_ENV: string | null | undefined @@ -26,6 +28,10 @@ export function getPlatformString() { } } +export function getPlatform(): Platform { + return platformFromString(getPlatformString()); +} + let sharedDateFormatter: Intl.DateTimeFormat; export function dateToLocalizedString(date: Date) { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { diff --git a/app/assets/stylesheets/_main.scss b/app/assets/stylesheets/_main.scss index c0db8f3b5..914c0bd12 100644 --- a/app/assets/stylesheets/_main.scss +++ b/app/assets/stylesheets/_main.scss @@ -94,6 +94,8 @@ a { p { overflow: auto; + color: var(--sn-stylekit-paragraph-text-color); + margin: 0; } .main-ui-view { diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug index cc043c76c..5461ce92c 100644 --- a/app/assets/templates/directives/account-menu.pug +++ b/app/assets/templates/directives/account-menu.pug @@ -176,6 +176,8 @@ p.sk-p | Add a passcode to lock the application and | encrypt on-device key storage. + p(ng-if='self.state.keyStorageInfo') + | {{self.state.keyStorageInfo}} div(ng-if='!self.state.canAddPasscode') p.sk-p | Adding a passcode is not supported in temporary sessions. Please sign