diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts index 37d3ef907..8d9e334ab 100644 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -1,5 +1,5 @@ import { WebDirective } from './../../types'; -import { isDesktopApplication, isNullOrUndefined, preventRefreshing } from '@/utils'; +import { isDesktopApplication, preventRefreshing } from '@/utils'; import template from '%/directives/account-menu.pug'; import { ProtectedAction, ContentType } from 'snjs'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; @@ -26,7 +26,6 @@ import { SyncOpStatus } from 'snjs/dist/@types/services/sync/sync_op_status'; import { PasswordWizardType } from '@/types'; import { BackupFile } from 'snjs/dist/@types/services/protocol_service'; import { confirmDialog, alertDialog } from '@/services/alertService'; -import { HttpResponse } from 'snjs/dist/@types/services/api/http_service'; const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request'; @@ -44,8 +43,6 @@ type FormData = { showPasscodeForm: boolean strictSignin?: boolean ephemeral: boolean - mfa: HttpResponse - userMfaCode?: string mergeLocal?: boolean url: string authenticating: boolean @@ -220,12 +217,10 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { formData.user_password!, formData.strictSignin, formData.ephemeral, - formData.mfa && formData.mfa.payload.mfa_key, - formData.userMfaCode, formData.mergeLocal ); - const hasError = !response || response.error; - if (!hasError) { + const error = response.error; + if (!error) { await this.setFormDataState({ authenticating: false, user_password: undefined @@ -233,29 +228,13 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { this.close(); return; } - const error = response - ? response.error! - : { - message: "An unknown error occured.", - tag: undefined, - status: 500 - } as HttpResponse; - if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') { - await this.setFormDataState({ - showLogin: false, - mfa: error, - status: undefined - }); - } else { - await this.setFormDataState({ - showLogin: true, - mfa: undefined, - status: undefined, - user_password: undefined - }); - if (error.message) { - this.application!.alertService!.alert(error.message); - } + await this.setFormDataState({ + showLogin: true, + status: undefined, + user_password: undefined + }); + if (error.message) { + this.application!.alertService!.alert(error.message); } await this.setFormDataState({ authenticating: false @@ -281,17 +260,11 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { this.getState().formData.ephemeral, this.getState().formData.mergeLocal ); - if (!response || response.error) { + const error = response.error; + if (error) { await this.setFormDataState({ status: undefined }); - const error = response - ? response.error! - : { - message: "An unknown error occured.", - tag: undefined, - status: 500 - } as HttpResponse; await this.setFormDataState({ authenticating: false }); diff --git a/app/assets/javascripts/directives/views/actionsMenu.ts b/app/assets/javascripts/directives/views/actionsMenu.ts index 80dec8457..42a7cb5fe 100644 --- a/app/assets/javascripts/directives/views/actionsMenu.ts +++ b/app/assets/javascripts/directives/views/actionsMenu.ts @@ -3,7 +3,7 @@ import { WebDirective } from './../../types'; import template from '%/directives/actions-menu.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { SNItem, Action, SNActionsExtension, UuidString } from 'snjs/dist/@types'; -import { ActionResponse } from 'snjs/dist/@types/services/actions_service'; +import { ActionResponse } from 'snjs'; import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension'; type ActionsMenuScope = { @@ -144,8 +144,8 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti } private async updateAction( - action: Action, - extension: SNActionsExtension, + action: Action, + extension: SNActionsExtension, params: UpdateActionParams ) { const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => { diff --git a/app/assets/javascripts/directives/views/passwordWizard.ts b/app/assets/javascripts/directives/views/passwordWizard.ts index 7f533da13..36ea4fa71 100644 --- a/app/assets/javascripts/directives/views/passwordWizard.ts +++ b/app/assets/javascripts/directives/views/passwordWizard.ts @@ -4,12 +4,35 @@ import template from '%/directives/password-wizard.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; const DEFAULT_CONTINUE_TITLE = "Continue"; -const Steps = { - PasswordStep: 1, - FinishStep: 2 +enum Steps { + PasswordStep = 1, + FinishStep = 2 }; -class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { +type FormData = { + currentPassword?: string, + newPassword?: string, + newPasswordConfirmation?: string, + status?: string +} + +type State = { + lockContinue: boolean + formData: FormData, + continueTitle: string, + step: Steps, + title: string, + showSpinner: boolean + processing: boolean +} + +type Props = { + type: PasswordWizardType, + changePassword: boolean, + securityUpdate: boolean +} + +class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { $element: JQLite application!: WebApplication type!: PasswordWizardType @@ -70,7 +93,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { } this.isContinuing = true; - this.setState({ + await this.setState({ showSpinner: true, continueTitle: "Generating Keys..." }); @@ -92,7 +115,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { }); } - async setFormDataState(formData: any) { + async setFormDataState(formData: Partial) { return this.setState({ formData: { ...this.state.formData, @@ -121,7 +144,9 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { this.application.alertService!.alert( "Your new password does not match its confirmation." ); - this.state.formData.status = null; + this.setFormDataState({ + status: undefined + }); return false; } } @@ -129,13 +154,15 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { this.application.alertService!.alert( "We don't have your email stored. Please log out then log back in to fix this issue." ); - this.state.formData.status = null; + this.setFormDataState({ + status: undefined + }); return false; } /** Validate current password */ const success = await this.application.validateAccountPassword( - this.state.formData.currentPassword + this.state.formData.currentPassword! ); if (!success) { this.application.alertService!.alert( @@ -146,37 +173,31 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { } async processPasswordChange() { - this.setState({ + await this.setState({ lockContinue: true, processing: true }); - this.setFormDataState({ + await this.setFormDataState({ status: "Processing encryption keys..." }); const newPassword = this.props.securityUpdate ? this.state.formData.currentPassword : this.state.formData.newPassword; const response = await this.application.changePassword( - this.state.formData.currentPassword, - newPassword + this.state.formData.currentPassword!, + newPassword! ); - const success = !response || !response.error; - this.setFormDataState({ - statusError: !success, - processing: success + const success = !response.error; + await this.setState({ + processing: false, + lockContinue: false, }); if (!success) { - this.application.alertService!.alert( - response?.error?.message - ? response.error.message - : "There was an error changing your password. Please try again." - ); this.setFormDataState({ status: "Unable to process your password. Please try again." }); } else { this.setState({ - lockContinue: false, formData: { ...this.state.formData, status: this.props.changePassword diff --git a/app/assets/javascripts/services/alertService.ts b/app/assets/javascripts/services/alertService.ts index 0e5ae1bec..38c0d457a 100644 --- a/app/assets/javascripts/services/alertService.ts +++ b/app/assets/javascripts/services/alertService.ts @@ -1,5 +1,5 @@ /* eslint-disable prefer-promise-reject-errors */ -import { SNAlertService, ButtonType, DismissBlockingDialog } from 'snjs'; +import { SNAlertService, ButtonType } from 'snjs'; import { SKAlert } from 'sn-stylekit'; /** @returns a promise resolving to true if the user confirmed, false if they canceled */ diff --git a/app/assets/javascripts/services/autolock_service.ts b/app/assets/javascripts/services/autolock_service.ts index 81b6f5db1..22e3d1d8f 100644 --- a/app/assets/javascripts/services/autolock_service.ts +++ b/app/assets/javascripts/services/autolock_service.ts @@ -1,4 +1,4 @@ -import { ApplicationGroup } from './../ui_models/application_group'; +import { ApplicationService } from 'snjs'; import { WebApplication } from '@/ui_models/application'; import { isDesktopApplication } from '@/utils'; import { AppStateEvent } from '@/ui_models/app_state'; @@ -13,26 +13,21 @@ const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND; const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey"; -export class AutolockService { +export class AutolockService extends ApplicationService { - private application: WebApplication private unsubState: any private pollFocusInterval: any private lastFocusState?: 'hidden' | 'visible' private lockAfterDate?: Date private lockTimeout?: any - constructor( - application: WebApplication - ) { - this.application = application; - setTimeout(() => { - this.observeVisibility(); - }, 0); + onAppLaunch() { + this.observeVisibility(); + return super.onAppLaunch(); } observeVisibility() { - this.unsubState = this.application.getAppState().addObserver( + this.unsubState = (this.application as WebApplication).getAppState().addObserver( async (eventName) => { if (eventName === AppStateEvent.WindowDidBlur) { this.documentVisibilityChanged(false); diff --git a/app/assets/javascripts/services/themeManager.ts b/app/assets/javascripts/services/themeManager.ts index d223beb01..487b4f7bb 100644 --- a/app/assets/javascripts/services/themeManager.ts +++ b/app/assets/javascripts/services/themeManager.ts @@ -1,5 +1,4 @@ import { WebApplication } from '@/ui_models/application'; -import _ from 'lodash'; import { StorageValueModes, EncryptionIntent, diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index 9bc8d7d7e..d80dd225d 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -1,5 +1,5 @@ /** @generic */ -export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."; +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) { @@ -46,12 +46,6 @@ export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a p 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_ENTER_ACCOUNT_PASSCODE = 'Enter your application passcode to unlock the application'; -export const STRING_ENTER_ACCOUNT_PASSWORD = 'Enter your account password'; -export const STRING_ENTER_PASSCODE_FOR_MIGRATION = 'Your application passcode is required to perform an upgrade of your local data storage structure.'; -export const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = 'Enter your application passcode before signing in or registering'; -export const STRING_STORAGE_UPDATE = 'Storage Update'; -export const STRING_AUTHENTICATION_REQUIRED = 'Authentication Required'; 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 */ @@ -71,8 +65,8 @@ export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL = export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available'; export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = - 'Encryption version 004 is available for your account and local data storage. ' + - 'This version strengthens the encryption algorithms for your account and ' + - 'disk use. To learn more about this upgrade, visit our ' + + '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'; diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index b6ecce1c9..57b3d0bdf 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -4,9 +4,7 @@ import { EditorGroup } from '@/ui_models/editor_group'; import { InputModalScope } from '@/directives/views/inputModal'; import { PasswordWizardType, PasswordWizardScope } from '@/types'; import { - Environment, SNApplication, - SNAlertService, platformFromString, Challenge, ProtectedAction @@ -167,21 +165,6 @@ export class WebApplication extends SNApplication { angular.element(document.body).append(el); } - async performProtocolUpgrade() { - const result = await this.upgradeProtocolVersion(); - if (result.success) { - this.alertService!.alert( - "Success! Your encryption version has been upgraded." + - " You'll be asked to enter your credentials again on other devices you're signed into." - ); - } else if (result.error) { - console.error(result.error); - this.alertService!.alert( - "Unable to upgrade encryption version. Please try again." - ); - } - } - async presentPrivilegesModal( action: ProtectedAction, onSuccess?: any, diff --git a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts index e73224cf4..81734b617 100644 --- a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts +++ b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts @@ -12,6 +12,11 @@ export class PureViewCtrl

{ private unsubApp: any private unsubState: any private stateTimeout?: ng.IPromise + /** + * Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that + * no Angular handlebars/syntax render in the UI before display data is ready. + */ + protected templateReady = false /* @ngInject */ constructor( @@ -28,6 +33,7 @@ export class PureViewCtrl

{ } this.addAppEventObserver(); this.addAppStateObserver(); + this.templateReady = true; } deinit() { @@ -114,7 +120,7 @@ export class PureViewCtrl

{ await this.onAppLaunch(); } else if (eventName === ApplicationEvent.CompletedIncrementalSync) { this.onAppIncrementalSync(); - } else if (eventName === ApplicationEvent.CompletedFullSync) { + } else if (eventName === ApplicationEvent.CompletedFullSync) { this.onAppFullSync(); } else if (eventName === ApplicationEvent.KeyStatusChanged) { this.onAppKeyChange(); diff --git a/app/assets/javascripts/views/application/application_view.ts b/app/assets/javascripts/views/application/application_view.ts index 4c6752f86..9a36f37eb 100644 --- a/app/assets/javascripts/views/application/application_view.ts +++ b/app/assets/javascripts/views/application/application_view.ts @@ -10,12 +10,10 @@ import { PANEL_NAME_TAGS } from '@/views/constants'; import { - STRING_SESSION_EXPIRED, STRING_DEFAULT_FILE_ERROR } from '@/strings'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PermissionDialog } from 'snjs/dist/@types/services/component_manager'; -import { alertDialog } from '@/services/alertService'; class ApplicationViewCtrl extends PureViewCtrl { private $compile?: ng.ICompileService @@ -29,7 +27,6 @@ class ApplicationViewCtrl extends PureViewCtrl { private showingDownloadStatus = false private uploadSyncStatus: any private lastAlertShownTimeStamp = 0; - private showingInvalidSessionAlert = false; /* @ngInject */ constructor( @@ -126,8 +123,6 @@ class ApplicationViewCtrl extends PureViewCtrl { this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus); this.completedInitialSync = true; } - } else if (eventName === ApplicationEvent.InvalidSyncSession) { - this.showInvalidSessionAlert(); } else if (eventName === ApplicationEvent.LocalDatabaseReadError) { this.application!.alertService!.alert( 'Unable to load local database. Please restart the app and try again.' @@ -258,24 +253,6 @@ class ApplicationViewCtrl extends PureViewCtrl { this.application!.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog; } - showInvalidSessionAlert() { - /** Don't show repeatedly; at most 30 seconds in between */ - const SHOW_INTERVAL = 30 * 1000; - if ( - !this.showingInvalidSessionAlert && - (Date.now() - this.lastAlertShownTimeStamp) > SHOW_INTERVAL - ) { - this.lastAlertShownTimeStamp = Date.now(); - this.showingInvalidSessionAlert = true; - setTimeout(async () => { - await alertDialog({ - text: STRING_SESSION_EXPIRED - }); - this.showingInvalidSessionAlert = false; - }, 500); - } - } - addDragDropHandlers() { /** * Disable dragging and dropping of files (but allow text) into main SN interface. diff --git a/app/assets/javascripts/views/challenge_modal/challenge-modal.pug b/app/assets/javascripts/views/challenge_modal/challenge-modal.pug index e8ad81429..a54e5d766 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge-modal.pug +++ b/app/assets/javascripts/views/challenge_modal/challenge-modal.pug @@ -1,26 +1,30 @@ .sk-modal-background(ng-click="ctrl.cancel()") -.challenge-modal.sk-modal-content +.challenge-modal.sk-modal-content(ng-if='ctrl.templateReady') .sn-component .sk-panel .sk-panel-header - .sk-panel-header-title {{ctrl.title}} + .sk-panel-header-title {{ctrl.challenge.modalTitle}} .sk-panel-content .sk-panel-section - div(ng-repeat="type in ctrl.state.types") - .sk-p.sk-panel-row.centered.prompt - strong {{ctrl.promptForChallenge(type)}} + .sk-p.sk-panel-row.centered.prompt + strong {{ctrl.challenge.heading}} + .sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.challenge.subheading') + | {{ctrl.challenge.subheading}} + .sk-panel-section + div(ng-repeat="prompt in ctrl.state.prompts track by prompt.id") .sk-panel-row input.sk-input.contrast( - ng-model="ctrl.state.values[type].value" + ng-model="ctrl.state.values[prompt.id].value" should-focus="$index == 0" sn-autofocus="true" sn-enter="ctrl.submit()" , - ng-change="ctrl.onTextValueChange(type)" - type="password" + ng-change="ctrl.onTextValueChange(prompt)" + ng-attr-type="{{prompt.secureTextEntry ? 'password' : 'text'}}", + ng-attr-placeholder="{{prompt.placeholder}}" ) .sk-panel-row.centered label.sk-label.danger( - ng-if="ctrl.state.values[type].invalid" + ng-if="ctrl.state.values[prompt.id].invalid" ) Invalid authentication. Please try again. .sk-panel-footer.extra-padding .sk-button.info.big.block.bold( diff --git a/app/assets/javascripts/views/challenge_modal/challenge_modal.ts b/app/assets/javascripts/views/challenge_modal/challenge_modal.ts index 63840079f..4356cfbcc 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge_modal.ts +++ b/app/assets/javascripts/views/challenge_modal/challenge_modal.ts @@ -1,43 +1,39 @@ import { WebApplication } from '@/ui_models/application'; import template from './challenge-modal.pug'; import { - ChallengeType, ChallengeValue, removeFromArray, Challenge, ChallengeReason, + ChallengePrompt } from 'snjs'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { WebDirective } from '@/types'; import { confirmDialog } from '@/services/alertService'; import { STRING_SIGN_OUT_CONFIRMATION, - STRING_ENTER_ACCOUNT_PASSCODE, - STRING_ENTER_ACCOUNT_PASSWORD, - STRING_ENTER_PASSCODE_FOR_MIGRATION, - STRING_STORAGE_UPDATE, - STRING_AUTHENTICATION_REQUIRED, - STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER, } from '@/strings'; type InputValue = { + prompt: ChallengePrompt value: string invalid: boolean } -type Values = Record +type Values = Record type ChallengeModalState = { - types: ChallengeType[] + prompts: ChallengePrompt[] values: Partial processing: boolean, forgotPasscode: boolean, showForgotPasscodeLink: boolean, + processingPrompts: ChallengePrompt[], + hasAccount: boolean, } -class ChallengeModalCtrl extends PureViewCtrl { +class ChallengeModalCtrl extends PureViewCtrl<{}, ChallengeModalState> { private $element: JQLite - private processingTypes: ChallengeType[] = [] application!: WebApplication challenge!: Challenge private cancelable = false @@ -58,14 +54,15 @@ class ChallengeModalCtrl extends PureViewCtrl { $onInit() { super.$onInit(); const values = {} as Values; - const types = this.challenge.types; - for (const type of types) { - values[type] = { + const prompts = this.challenge.prompts; + for (const prompt of prompts) { + values[prompt.id] = { + prompt, value: '', invalid: false }; } - let showForgotPasscodeLink: boolean; + let showForgotPasscodeLink = false; switch (this.challenge.reason) { case ChallengeReason.ApplicationUnlock: showForgotPasscodeLink = true; @@ -86,29 +83,40 @@ class ChallengeModalCtrl extends PureViewCtrl { } this.cancelable = !showForgotPasscodeLink this.setState({ - types, + prompts, values, processing: false, forgotPasscode: false, showForgotPasscodeLink, hasAccount: this.application.hasAccount(), + processingPrompts: [] }); - this.application.setChallengeCallbacks({ - challenge: this.challenge, - onValidValue: (value) => { - this.getState().values[value.type]!.invalid = false; - removeFromArray(this.processingTypes, value.type); - this.reloadProcessingStatus(); - }, - onInvalidValue: (value) => { - this.getState().values[value.type]!.invalid = true; - removeFromArray(this.processingTypes, value.type); - this.reloadProcessingStatus(); - }, - onComplete: () => { - this.dismiss(); - }, - }); + this.application.addChallengeObserver( + this.challenge, + { + onValidValue: (value) => { + this.getState().values[value.prompt.id]!.invalid = false; + removeFromArray(this.state.processingPrompts, value.prompt); + this.reloadProcessingStatus(); + }, + onInvalidValue: (value) => { + this.getState().values[value.prompt.id]!.invalid = true; + /** If custom validation, treat all values together and not individually */ + if (!value.prompt.validates) { + this.setState({ processingPrompts: [], processing: false }); + } else { + removeFromArray(this.state.processingPrompts, value.prompt); + this.reloadProcessingStatus(); + } + }, + onComplete: () => { + this.dismiss(); + }, + onCancel: () => { + this.dismiss(); + }, + } + ); } deinit() { @@ -118,34 +126,11 @@ class ChallengeModalCtrl extends PureViewCtrl { } reloadProcessingStatus() { - this.setState({ - processing: this.processingTypes.length > 0 + return this.setState({ + processing: this.state.processingPrompts.length > 0 }); } - get title(): string { - if (this.challenge.reason === ChallengeReason.Migration) { - return STRING_STORAGE_UPDATE; - } else { - return STRING_AUTHENTICATION_REQUIRED; - } - } - - promptForChallenge(challenge: ChallengeType): string { - if (challenge === ChallengeType.LocalPasscode) { - switch (this.challenge.reason) { - case ChallengeReason.Migration: - return STRING_ENTER_PASSCODE_FOR_MIGRATION; - case ChallengeReason.ResaveRootKey: - return STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER; - default: - return STRING_ENTER_ACCOUNT_PASSCODE; - } - } else { - return STRING_ENTER_ACCOUNT_PASSWORD; - } - } - async destroyLocalData() { if (await confirmDialog({ text: STRING_SIGN_OUT_CONFIRMATION, @@ -156,10 +141,10 @@ class ChallengeModalCtrl extends PureViewCtrl { }; } + /** @template */ cancel() { if (this.cancelable) { this.application!.cancelChallenge(this.challenge); - this.dismiss(); } } @@ -169,18 +154,18 @@ class ChallengeModalCtrl extends PureViewCtrl { }); } - onTextValueChange(challenge: ChallengeType) { + onTextValueChange(prompt: ChallengePrompt) { const values = this.getState().values; - values[challenge]!.invalid = false; + values[prompt.id]!.invalid = false; this.setState({ values }); } validate() { const failed = []; - for (const type of this.getState().types) { - const value = this.getState().values[type]; + for (const prompt of this.getState().prompts) { + const value = this.getState().values[prompt.id]; if (!value || value.value.length === 0) { - this.getState().values[type]!.invalid = true; + this.getState().values[prompt.id]!.invalid = true; } } return failed.length === 0; @@ -191,22 +176,29 @@ class ChallengeModalCtrl extends PureViewCtrl { return; } await this.setState({ processing: true }); - const values = []; - for (const key of Object.keys(this.getState().values)) { - const type = Number(key) as ChallengeType; - if (this.getState().values[type]!.invalid) { - continue; - } - const rawValue = this.getState().values[type]!.value; - const value = new ChallengeValue(type, rawValue); + const values: ChallengeValue[] = []; + for (const inputValue of Object.values(this.getState().values)) { + const rawValue = inputValue!!.value; + const value = new ChallengeValue(inputValue!.prompt, rawValue); values.push(value); } - this.processingTypes = values.map((v) => v.type); - if (values.length > 0) { - this.application.submitValuesForChallenge(this.challenge, values); - } else { - this.setState({ processing: false }); - } + const processingPrompts = values.map((v) => v.prompt); + await this.setState({ + processingPrompts: processingPrompts, + processing: processingPrompts.length > 0 + }) + /** + * Unfortunately neccessary to wait 50ms so that the above setState call completely + * updates the UI to change processing state, before we enter into UI blocking operation + * (crypto key generation) + */ + this.$timeout(() => { + if (values.length > 0) { + this.application.submitValuesForChallenge(this.challenge, values); + } else { + this.setState({ processing: false }); + } + }, 50) } dismiss() { diff --git a/app/assets/javascripts/views/footer/footer_view.ts b/app/assets/javascripts/views/footer/footer_view.ts index b42deab33..b6ff6ff21 100644 --- a/app/assets/javascripts/views/footer/footer_view.ts +++ b/app/assets/javascripts/views/footer/footer_view.ts @@ -347,7 +347,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, })) { preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => { - await this.application.performProtocolUpgrade(); + await this.application.upgradeProtocolVersion(); }); } } diff --git a/app/assets/javascripts/views/notes/note_utils.ts b/app/assets/javascripts/views/notes/note_utils.ts index 6d763ccef..2c7577b5f 100644 --- a/app/assets/javascripts/views/notes/note_utils.ts +++ b/app/assets/javascripts/views/notes/note_utils.ts @@ -1,4 +1,4 @@ -import { SNNote, SNTag } from 'snjs'; +import { SNNote } from 'snjs'; export enum NoteSortKey { CreatedAt = 'created_at', diff --git a/app/assets/stylesheets/_modals.scss b/app/assets/stylesheets/_modals.scss index 8220d1fdf..c83aefd04 100644 --- a/app/assets/stylesheets/_modals.scss +++ b/app/assets/stylesheets/_modals.scss @@ -13,8 +13,9 @@ .challenge-modal { max-width: 480px; + min-width: 400px !important; - .prompt { + .prompt, .subprompt { text-align: center; } .sk-panel .sk-panel-header { @@ -90,10 +91,6 @@ } } -#password-wizard { - font-size: 16px; -} - #item-preview-modal { > .sk-modal-content { width: 800px; diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug index 0898b91dc..bfd48cd3e 100644 --- a/app/assets/templates/directives/account-menu.pug +++ b/app/assets/templates/directives/account-menu.pug @@ -8,8 +8,7 @@ ng-if=` !self.state.user && !self.state.formData.showLogin && - !self.state.formData.showRegister && - !self.state.formData.mfa` + !self.state.formData.showRegister` ) .sk-panel-row .sk-h1 Sign in or register to enable sync and end-to-end encryption. @@ -131,35 +130,10 @@ type='checkbox' ) p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags) - .sk-panel-section(ng-if='self.state.formData.mfa') - form.sk-panel-form(ng-submit='self.submitMfaForm()') - .sk-p.sk-panel-row {{self.state.formData.mfa.message}} - .sk-panel-row - input.sk-input.contrast( - autofocus='true', - name='mfa', - ng-model='self.state.formData.userMfaCode', - placeholder='Enter Code', - required='', - should-focus='true', - sn-autofocus='true' - ) - .sk-button-group.stretch.sk-panel-row.form-submit( - ng-if='!self.state.formData.status' - ) - button.sk-button.info.featured(type='submit') - .sk-label Sign In - .sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status') - .sk-panel-row - .sk-panel-row - .sk-horizontal-group - .sk-spinner.small.neutral - .sk-label {{self.state.formData.status}} div( ng-if=` !self.state.formData.showLogin && - !self.state.formData.showRegister && - !self.state.formData.mfa` + !self.state.formData.showRegister` ) .sk-panel-section(ng-if='self.state.user') .sk-notification.danger(ng-if='self.syncStatus.error') diff --git a/app/assets/templates/directives/password-wizard.pug b/app/assets/templates/directives/password-wizard.pug index 65f56b69f..2fde1edc6 100644 --- a/app/assets/templates/directives/password-wizard.pug +++ b/app/assets/templates/directives/password-wizard.pug @@ -13,37 +13,36 @@ .sk-panel-column.stretch form.sk-panel-form input.sk-input.contrast( - ng-model='ctrl.state.formData.currentPassword', - placeholder='Current Password', - should-focus='true', - sn-autofocus='true', + ng-model='ctrl.state.formData.currentPassword', + placeholder='Current Password', + should-focus='true', + sn-autofocus='true', + type='password' + ) + .sk-panel-row + input.sk-input.contrast( + ng-if='ctrl.props.changePassword', + ng-model='ctrl.state.formData.newPassword', + placeholder='New Password', type='password' ) input.sk-input.contrast( - ng-if='ctrl.props.changePassword', - ng-model='ctrl.state.formData.newPassword', - placeholder='New Password', - type='password' - ) - input.sk-input.contrast( - ng-if='ctrl.props.changePassword', + ng-if='ctrl.props.changePassword', ng-model='ctrl.state.formData.newPasswordConfirmation', - placeholder='Confirm New Password', + placeholder='Confirm New Password', type='password' ) .sk-panel-section(ng-if='ctrl.state.step == 2') - div(ng-if='ctrl.props.changePassword') - p.sk-p.sk-panel-row.info-i Your password has been successfully changed. - div(ng-if='ctrl.props.securityUpdate') - p.sk-p.sk-panel-row.info-i - | The account update has been successfully applied to your account. - p.sk-p.sk-panel-row - | Please ensure you are running the latest version of Standard Notes + .sk-label.sk-bold.info(ng-if='ctrl.props.changePassword') + | Your password has been successfully changed. + p.sk-p.info-i(ng-if='ctrl.props.securityUpdate') + | The account update has been successfully applied to your account. + p.sk-p + | Please ensure you are running the latest version of Standard Notes | on all platforms to ensure maximum compatibility. .sk-panel-footer - .empty - a.sk-a.info.right( - ng-click='ctrl.nextStep()', - ng-disabled='ctrl.state.lockContinue') - .sk-spinner.small.inline.info.mr-5(ng-if='ctrl.state.showSpinner') - | {{ctrl.state.continueTitle}} + .sk-button.info( + ng-click='ctrl.nextStep()', + ng-disabled='ctrl.state.lockContinue' + ) + .sk-label {{ctrl.state.continueTitle}} diff --git a/dist/@types/app/assets/javascripts/services/autolock_service.d.ts b/dist/@types/app/assets/javascripts/services/autolock_service.d.ts index 7a0d92401..4ac99fcae 100644 --- a/dist/@types/app/assets/javascripts/services/autolock_service.d.ts +++ b/dist/@types/app/assets/javascripts/services/autolock_service.d.ts @@ -1,12 +1,11 @@ -import { WebApplication } from '@/ui_models/application'; -export declare class AutolockService { - private application; +import { ApplicationService } from 'snjs'; +export declare class AutolockService extends ApplicationService { private unsubState; private pollFocusInterval; private lastFocusState?; private lockAfterDate?; private lockTimeout?; - constructor(application: WebApplication); + onAppLaunch(): Promise; observeVisibility(): void; deinit(): void; private lockApplication; diff --git a/dist/@types/app/assets/javascripts/services/statusManager.d.ts b/dist/@types/app/assets/javascripts/services/statusManager.d.ts index d5b83f03a..a8269d412 100644 --- a/dist/@types/app/assets/javascripts/services/statusManager.d.ts +++ b/dist/@types/app/assets/javascripts/services/statusManager.d.ts @@ -3,15 +3,15 @@ declare type StatusCallback = (string: string) => void; export declare class StatusManager { private statuses; private observers; - statusFromString(string: string): { + replaceStatusWithString(status: FooterStatus, string: string): { + string: string; + }; + addStatusFromString(string: string): { string: string; }; - replaceStatusWithString(status: FooterStatus, string: string): FooterStatus; - addStatusFromString(string: string): FooterStatus; - addStatus(status: FooterStatus): FooterStatus; removeStatus(status: FooterStatus): undefined; - getStatusString(): string; - notifyObservers(): void; addStatusObserver(callback: StatusCallback): () => void; + private notifyObservers; + private getStatusString; } export {}; diff --git a/dist/@types/app/assets/javascripts/strings.d.ts b/dist/@types/app/assets/javascripts/strings.d.ts index a5ca2ad34..ffcada422 100644 --- a/dist/@types/app/assets/javascripts/strings.d.ts +++ b/dist/@types/app/assets/javascripts/strings.d.ts @@ -1,5 +1,5 @@ /** @generic */ -export declare const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."; +export declare const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign in to refresh your session."; export declare const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe."; export declare 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 declare function StringSyncException(data: any): string; @@ -32,15 +32,12 @@ export declare const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys..."; export declare const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys..."; export declare const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again."; export declare function StringImportError(errorCount: number): string; -export declare const STRING_ENTER_ACCOUNT_PASSCODE = "Enter your application passcode to unlock the application"; -export declare const STRING_ENTER_ACCOUNT_PASSWORD = "Enter your account password"; -export declare const STRING_ENTER_PASSCODE_FOR_MIGRATION = "Your application passcode is required to perform an upgrade of your local data storage structure."; -export declare const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = "Enter your application passcode before signing in or registering"; -export declare const STRING_STORAGE_UPDATE = "Storage Update"; -export declare const STRING_AUTHENTICATION_REQUIRED = "Authentication Required"; export declare 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 declare 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 declare const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE: string; export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE: string; export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL: string; +export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = "Encryption upgrade available"; +export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT: string; +export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = "Upgrade"; diff --git a/dist/@types/app/assets/javascripts/ui_models/application.d.ts b/dist/@types/app/assets/javascripts/ui_models/application.d.ts index 12223b172..264e854da 100644 --- a/dist/@types/app/assets/javascripts/ui_models/application.d.ts +++ b/dist/@types/app/assets/javascripts/ui_models/application.d.ts @@ -42,7 +42,6 @@ export declare class WebApplication extends SNApplication { checkForSecurityUpdate(): Promise; presentPasswordWizard(type: PasswordWizardType): void; promptForChallenge(challenge: Challenge): void; - performProtocolUpgrade(): Promise; presentPrivilegesModal(action: ProtectedAction, onSuccess?: any, onCancel?: any): Promise; presentPrivilegesManagementModal(): void; authenticationInProgress(): boolean; diff --git a/dist/@types/app/assets/javascripts/views/abstract/pure_view_ctrl.d.ts b/dist/@types/app/assets/javascripts/views/abstract/pure_view_ctrl.d.ts index 38387e19a..937a04176 100644 --- a/dist/@types/app/assets/javascripts/views/abstract/pure_view_ctrl.d.ts +++ b/dist/@types/app/assets/javascripts/views/abstract/pure_view_ctrl.d.ts @@ -12,6 +12,11 @@ export declare class PureViewCtrl

{ private unsubApp; private unsubState; private stateTimeout?; + /** + * Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that + * no Angular handlebars/syntax render in the UI before display data is ready. + */ + protected templateReady: boolean; constructor($timeout: ng.ITimeoutService, props?: P); $onInit(): void; deinit(): void; diff --git a/package-lock.json b/package-lock.json index e80160e25..265d100f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10956,8 +10956,8 @@ "from": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad" }, "snjs": { - "version": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790", - "from": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790" + "version": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9", + "from": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9" }, "sockjs": { "version": "0.3.20", diff --git a/package.json b/package.json index e758a2809..50cf53990 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,6 @@ }, "dependencies": { "sncrypto": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad", - "snjs": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790" + "snjs": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9" } }