From d42518fc61cb4edd185da406798583a99e076b2f Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 22 Sep 2020 00:10:43 -0500 Subject: [PATCH] feat: custom challenges --- app/assets/javascripts/strings.ts | 6 +- .../views/application/application_view.ts | 6 +- .../views/challenge_modal/challenge-modal.pug | 20 ++- .../views/challenge_modal/challenge_modal.ts | 139 ++++++++++-------- .../javascripts/services/statusManager.d.ts | 12 +- .../app/assets/javascripts/strings.d.ts | 6 +- package-lock.json | 4 +- package.json | 2 +- 8 files changed, 100 insertions(+), 95 deletions(-) diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index 8351b22fd..b368b1a0e 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,10 +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.'; diff --git a/app/assets/javascripts/views/application/application_view.ts b/app/assets/javascripts/views/application/application_view.ts index 4c6752f86..375a26a8c 100644 --- a/app/assets/javascripts/views/application/application_view.ts +++ b/app/assets/javascripts/views/application/application_view.ts @@ -268,9 +268,9 @@ class ApplicationViewCtrl extends PureViewCtrl { this.lastAlertShownTimeStamp = Date.now(); this.showingInvalidSessionAlert = true; setTimeout(async () => { - await alertDialog({ - text: STRING_SESSION_EXPIRED - }); + // await alertDialog({ + // text: STRING_SESSION_EXPIRED + // }); this.showingInvalidSessionAlert = false; }, 500); } diff --git a/app/assets/javascripts/views/challenge_modal/challenge-modal.pug b/app/assets/javascripts/views/challenge_modal/challenge-modal.pug index e8ad81429..b7b2595ed 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge-modal.pug +++ b/app/assets/javascripts/views/challenge_modal/challenge-modal.pug @@ -3,24 +3,28 @@ .sn-component .sk-panel .sk-panel-header - .sk-panel-header-title {{ctrl.title}} + .sk-panel-header-title {{ctrl.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.state.title}} + .sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.state.subtitle') + | {{ctrl.state.subtitle}} + .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..969f7f4b3 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge_modal.ts +++ b/app/assets/javascripts/views/challenge_modal/challenge_modal.ts @@ -1,43 +1,43 @@ 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, + title: string, + subtitle: string } -class ChallengeModalCtrl extends PureViewCtrl { +class ChallengeModalCtrl extends PureViewCtrl<{}, ChallengeModalState> { private $element: JQLite - private processingTypes: ChallengeType[] = [] application!: WebApplication challenge!: Challenge private cancelable = false @@ -58,14 +58,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 +87,42 @@ class ChallengeModalCtrl extends PureViewCtrl { } this.cancelable = !showForgotPasscodeLink this.setState({ - types, + prompts, values, processing: false, forgotPasscode: false, showForgotPasscodeLink, hasAccount: this.application.hasAccount(), + title: this.challenge.title, + subtitle: this.challenge.subtitle, + 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: [] }); + } else { + removeFromArray(this.state.processingPrompts, value.prompt); + } + this.reloadProcessingStatus(); + }, + onComplete: () => { + this.dismiss(); + }, + onCancel: () => { + this.dismiss(); + }, + } + ); } deinit() { @@ -118,12 +132,12 @@ class ChallengeModalCtrl extends PureViewCtrl { } reloadProcessingStatus() { - this.setState({ - processing: this.processingTypes.length > 0 + return this.setState({ + processing: this.state.processingPrompts.length > 0 }); } - get title(): string { + get modalTitle(): string { if (this.challenge.reason === ChallengeReason.Migration) { return STRING_STORAGE_UPDATE; } else { @@ -131,21 +145,6 @@ class ChallengeModalCtrl extends PureViewCtrl { } } - 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, @@ -169,18 +168,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 +190,32 @@ 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) { + const values: ChallengeValue[] = []; + for (const inputValue of Object.values(this.getState().values)) { + if (inputValue!.invalid) { continue; } - const rawValue = this.getState().values[type]!.value; - const value = new ChallengeValue(type, rawValue); + 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/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..be22b76a0 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,10 +32,6 @@ 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."; diff --git a/package-lock.json b/package-lock.json index e80160e25..1a2377da0 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#6a0e03eaca106ffda7bce7d87761a29fd8f8420d", + "from": "github:standardnotes/snjs#6a0e03eaca106ffda7bce7d87761a29fd8f8420d" }, "sockjs": { "version": "0.3.20", diff --git a/package.json b/package.json index e758a2809..eff192948 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#6a0e03eaca106ffda7bce7d87761a29fd8f8420d" } }