Files
standardnotes-app-web/app/assets/javascripts/directives/views/passwordWizard.ts
Aman Harwara f1122f292e feat: New account menu and text input with icon & toggle (#665)
* feat: Add new icons

* Revert "feat: Add new icons"

This reverts commit 0acb403fe846dbb2e48fd22de35c3568c3cb4453.

* feat: Add new icons for account menu

* feat: Add new Icons

* feat: Add "currentPane" state to prefs view

* feat: Update account menu to new design

* feat: Add input component with icon & toggle

* fix: sync icon & function

* fix: Fix eye icon

* feat: Create re-usable checkbox

feat: Add "merge local" option

* feat: Allow using className on IconButton

* feat: Add disabled state on input

feat: Make toggle circle

* refactor: Move checkbox to components

* feat: Handle invalid email/password error

* feat: Implement new design for Create Account

* feat: Implement new account menu design

* feat: Add disabled option to IconButton

* feat: Set account menu pane from other component

* feat: Add 2fa account menu pane

feat: Add lock icon

* feat: Remove unnecessary 2FA menu pane

feat: Reset current menu pane on clickOutside

* feat: Change "Log in" to "Sign in"

* feat: Remove sync from footer

* feat: Change "Login" to "Sign in"

feat: Add spinner to "Syncing..."

refactor: Use then-catch-finally for sync

* feat: Use common enableCustomServer state

* feat: Animate account menu closing

* fix: Reset menu pane only after it's closed

* feat: Add keyDown handler to InputWithIcon

* feat: Handle Enter press in inputs

* Update app/assets/javascripts/components/InputWithIcon.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* Update app/assets/javascripts/components/InputWithIcon.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* refactor: Use server state from AccountMenuState

* Update app/assets/javascripts/components/AccountMenu/CreateAccount.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* Update app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* feat: Use common AdvancedOptions

* feat: Add "eye-off" icon and toggle state

* feat: Allow undefined values

* refactor: Remove enableCustomServer state

* feat: Persist server option state

* feat: Add bottom-100 and cursor-auto util classes

refactor: Use bottom-100 and cursor-auto classes

* refactor: Invert ternary operator

* refactor: Remove unused imports

* refactor: Use toggled as prop instead of state

* refactor: Change "Log in/out" to "Sign in/out"

* refactor: Change "Login" to "Sign in"

* refactor: Remove hardcoded width/height

* refactor: Use success class

* feat: Remove hardcoded width & height from svg

* fix: Fix chevron-down icon

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
2021-10-08 11:18:31 -05:00

245 lines
5.9 KiB
TypeScript

import { WebApplication } from '@/ui_models/application';
import {
PasswordWizardScope,
PasswordWizardType,
WebDirective,
} from './../../types';
import template from '%/directives/password-wizard.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
const DEFAULT_CONTINUE_TITLE = 'Continue';
enum Steps {
PasswordStep = 1,
FinishStep = 2,
}
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<Props, State>
implements PasswordWizardScope
{
$element: JQLite;
application!: WebApplication;
type!: PasswordWizardType;
isContinuing = false;
/* @ngInject */
constructor($element: JQLite, $timeout: ng.ITimeoutService) {
super($timeout);
this.$element = $element;
this.registerWindowUnloadStopper();
}
$onInit() {
super.$onInit();
this.initProps({
type: this.type,
changePassword: this.type === PasswordWizardType.ChangePassword,
securityUpdate: this.type === PasswordWizardType.AccountUpgrade,
});
this.setState({
formData: {},
continueTitle: DEFAULT_CONTINUE_TITLE,
step: Steps.PasswordStep,
title: this.props.changePassword ? 'Change Password' : 'Account Update',
});
}
$onDestroy() {
super.$onDestroy();
window.onbeforeunload = null;
}
/** Confirms with user before closing tab */
registerWindowUnloadStopper() {
window.onbeforeunload = () => {
return true;
};
}
resetContinueState() {
this.setState({
showSpinner: false,
continueTitle: DEFAULT_CONTINUE_TITLE,
});
this.isContinuing = false;
}
async nextStep() {
if (this.state.lockContinue || this.isContinuing) {
return;
}
if (this.state.step === Steps.FinishStep) {
this.dismiss();
return;
}
this.isContinuing = true;
await this.setState({
showSpinner: true,
continueTitle: 'Generating Keys...',
});
const valid = await this.validateCurrentPassword();
if (!valid) {
this.resetContinueState();
return;
}
const success = await this.processPasswordChange();
if (!success) {
this.resetContinueState();
return;
}
this.isContinuing = false;
this.setState({
showSpinner: false,
continueTitle: 'Finish',
step: Steps.FinishStep,
});
}
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.state.formData,
...formData,
},
});
}
async validateCurrentPassword() {
const currentPassword = this.state.formData.currentPassword;
const newPass = this.props.securityUpdate
? currentPassword
: this.state.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.application.alertService!.alert(
'Please enter your current password.'
);
return false;
}
if (this.props.changePassword) {
if (!newPass || newPass.length === 0) {
this.application.alertService!.alert('Please enter a new password.');
return false;
}
if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alertService!.alert(
'Your new password does not match its confirmation.'
);
this.setFormDataState({
status: undefined,
});
return false;
}
}
if (!this.application.getUser()?.email) {
this.application.alertService!.alert(
"We don't have your email stored. Please sign out then log back in to fix this issue."
);
this.setFormDataState({
status: undefined,
});
return false;
}
/** Validate current password */
const success = await this.application.validateAccountPassword(
this.state.formData.currentPassword!
);
if (!success) {
this.application.alertService!.alert(
'The current password you entered is not correct. Please try again.'
);
}
return success;
}
async processPasswordChange() {
await this.application.downloadBackup();
await this.setState({
lockContinue: true,
processing: true,
});
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!
);
const success = !response.error;
await this.setState({
processing: false,
lockContinue: false,
});
if (!success) {
this.setFormDataState({
status: 'Unable to process your password. Please try again.',
});
} else {
this.setState({
formData: {
...this.state.formData,
status: this.props.changePassword
? 'Successfully changed password.'
: 'Successfully performed account update.',
},
});
}
return success;
}
dismiss() {
if (this.state.lockContinue) {
this.application.alertService!.alert(
'Cannot close window until pending tasks are complete.'
);
} else {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
}
export class PasswordWizard extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PasswordWizardCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '=',
application: '=',
};
}
}