feat: updated settings password modal

pull/2522/head
William Grant 3 years ago
parent d95bd47438
commit 0d10667d6e

@ -536,29 +536,6 @@ label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--background-secondary-color); background-color: var(--background-secondary-color);
&__password-lock {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
&-box {
padding: 45px 60px;
display: flex;
flex-direction: column;
align-items: center;
max-width: 90%;
min-width: 400px;
background: var(--background-primary-color);
color: var(--text-primary-color);
border: 1px solid var(--border-color);
border-radius: 5px;
}
}
} }
#qr svg, #qr svg,

@ -42,5 +42,3 @@
@import 'session_slider'; @import 'session_slider';
@import 'session_conversation'; @import 'session_conversation';
@import '_session_password';

@ -3,7 +3,7 @@ import React from 'react';
import { missingCaseError } from '../../util'; import { missingCaseError } from '../../util';
import { ToastUtils } from '../../session/utils'; import { ToastUtils } from '../../session/utils';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { SpacerLG, SpacerSM } from '../basic/Text'; import { SpacerSM } from '../basic/Text';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { sessionPassword } from '../../state/ducks/modalDialog'; import { sessionPassword } from '../../state/ducks/modalDialog';
import { LocalizerKeys } from '../../types/LocalizerKeys'; import { LocalizerKeys } from '../../types/LocalizerKeys';
@ -11,7 +11,7 @@ import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/S
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionWrapperModal } from '../SessionWrapperModal';
import { matchesHash, validatePassword } from '../../util/passwordUtils'; import { matchesHash, validatePassword } from '../../util/passwordUtils';
export type PasswordAction = 'set' | 'change' | 'remove'; export type PasswordAction = 'set' | 'change' | 'remove' | 'enter';
interface Props { interface Props {
passwordAction: PasswordAction; passwordAction: PasswordAction;
@ -62,6 +62,9 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
case 'remove': case 'remove':
placeholders = [window.i18n('enterPassword')]; placeholders = [window.i18n('enterPassword')];
break; break;
case 'enter':
placeholders = [window.i18n('enterPassword')];
break;
default: default:
placeholders = [window.i18n('createPassword'), window.i18n('confirmPassword')]; placeholders = [window.i18n('createPassword'), window.i18n('confirmPassword')];
} }
@ -72,6 +75,8 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
? 'changePassword' ? 'changePassword'
: passwordAction === 'remove' : passwordAction === 'remove'
? 'removePassword' ? 'removePassword'
: passwordAction === 'enter'
? 'passwordViewTitle'
: 'setPassword'; : 'setPassword';
return ( return (
@ -89,7 +94,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
onKeyUp={this.onPasswordInput} onKeyUp={this.onPasswordInput}
data-testid="password-input" data-testid="password-input"
/> />
{passwordAction !== 'remove' && ( {passwordAction !== 'enter' && passwordAction !== 'remove' && (
<input <input
type="password" type="password"
id="password-modal-input-confirm" id="password-modal-input-confirm"
@ -110,7 +115,6 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
</div> </div>
<SpacerSM /> <SpacerSM />
{this.showError()}
<div className="session-modal__button-group"> <div className="session-modal__button-group">
<SessionButton <SessionButton
@ -119,12 +123,14 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
buttonType={SessionButtonType.Simple} buttonType={SessionButtonType.Simple}
onClick={this.setPassword} onClick={this.setPassword}
/> />
<SessionButton {passwordAction !== 'enter' && (
text={window.i18n('cancel')} <SessionButton
buttonColor={passwordAction !== 'remove' ? SessionButtonColor.Danger : undefined} text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple} buttonColor={passwordAction !== 'remove' ? SessionButtonColor.Danger : undefined}
onClick={this.closeDialog} buttonType={SessionButtonType.Simple}
/> onClick={this.closeDialog}
/>
)}
</div> </div>
</SessionWrapperModal> </SessionWrapperModal>
); );
@ -141,18 +147,9 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
} }
private showError() { private showError() {
const message = this.state.error; if (this.state.error) {
ToastUtils.pushToastError('enterPasswordErrorToast', this.state.error);
return ( }
<>
{message && (
<>
<div className="session-label danger">{message}</div>
<SpacerLG />
</>
)}
</>
);
} }
/** /**
@ -166,6 +163,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.setState({ this.setState({
error: errorFirstInput, error: errorFirstInput,
}); });
this.showError();
return false; return false;
} }
return true; return true;
@ -182,6 +180,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.setState({ this.setState({
error: window.i18n('setPasswordInvalid'), error: window.i18n('setPasswordInvalid'),
}); });
this.showError();
return; return;
} }
await window.setPassword(enteredPassword, null); await window.setPassword(enteredPassword, null);
@ -211,6 +210,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.setState({ this.setState({
error: window.i18n('passwordsDoNotMatch'), error: window.i18n('passwordsDoNotMatch'),
}); });
this.showError();
return; return;
} }
@ -219,6 +219,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.setState({ this.setState({
error: window.i18n('changePasswordInvalid'), error: window.i18n('changePasswordInvalid'),
}); });
this.showError();
return; return;
} }
await window.setPassword(newPassword, oldPassword); await window.setPassword(newPassword, oldPassword);
@ -240,6 +241,7 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.setState({ this.setState({
error: window.i18n('removePasswordInvalid'), error: window.i18n('removePasswordInvalid'),
}); });
this.showError();
return; return;
} }
await window.setPassword(null, oldPassword); await window.setPassword(null, oldPassword);
@ -254,6 +256,25 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
this.closeDialog(); this.closeDialog();
} }
private async handleActionEnter(enteredPassword: string) {
// be sure the password is valid
if (!this.validatePassword(enteredPassword)) {
return;
}
const isValidWithStoredInDB = Boolean(await this.validatePasswordHash(enteredPassword));
if (!isValidWithStoredInDB) {
this.setState({
error: window.i18n('invalidPassword'),
});
this.showError();
return;
}
this.props.onOk();
this.closeDialog();
}
// tslint:disable-next-line: cyclomatic-complexity // tslint:disable-next-line: cyclomatic-complexity
private async setPassword() { private async setPassword() {
const { passwordAction } = this.props; const { passwordAction } = this.props;
@ -285,6 +306,10 @@ export class SessionPasswordDialog extends React.Component<Props, State> {
await this.handleActionRemove(firstPasswordEntered); await this.handleActionRemove(firstPasswordEntered);
return; return;
} }
case 'enter': {
await this.handleActionEnter(firstPasswordEntered);
return;
}
default: default:
throw missingCaseError(passwordAction); throw missingCaseError(passwordAction);
} }

@ -9,13 +9,26 @@ import { SessionNotificationGroupSettings } from './SessionNotificationGroupSett
import { CategoryConversations } from './section/CategoryConversations'; import { CategoryConversations } from './section/CategoryConversations';
import { SettingsCategoryPrivacy } from './section/CategoryPrivacy'; import { SettingsCategoryPrivacy } from './section/CategoryPrivacy';
import { SettingsCategoryAppearance } from './section/CategoryAppearance'; import { SettingsCategoryAppearance } from './section/CategoryAppearance';
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
import { Data } from '../../data/data'; import { Data } from '../../data/data';
import { matchesHash } from '../../util/passwordUtils';
import { SettingsCategoryPermissions } from './section/CategoryPermissions'; import { SettingsCategoryPermissions } from './section/CategoryPermissions';
import { SettingsCategoryHelp } from './section/CategoryHelp'; import { SettingsCategoryHelp } from './section/CategoryHelp';
import styled from 'styled-components'; import styled from 'styled-components';
import { ToastUtils } from '../../session/utils'; import { sessionPassword } from '../../state/ducks/modalDialog';
import { PasswordAction } from '../dialog/SessionPasswordDialog';
export function displayPasswordModal(
passwordAction: PasswordAction,
onPasswordUpdated: (action: string) => void
) {
window.inboxStore?.dispatch(
sessionPassword({
passwordAction,
onOk: () => {
onPasswordUpdated(passwordAction);
},
})
);
}
export function getMediaPermissionsSettings() { export function getMediaPermissionsSettings() {
return window.getSettingValue('media-permissions'); return window.getSettingValue('media-permissions');
@ -92,58 +105,6 @@ const SessionInfo = () => {
); );
}; };
const StyledPasswordInput = styled.input`
width: 100%;
background: var(--text-box-background-color);
color: var(--text-box-text-user-color);
padding: var(--margins-xs) var(--margins-md);
margin-bottom: var(--margins-lg);
outline: none;
border: 1px solid var(--border-color);
border-radius: 7px;
text-align: center;
font-size: 16px;
font-family: var(--font-default);
::placeholder {
color: var(--text-box-text-control-color);
}
`;
const StyledH3 = styled.h3`
padding: 0px;
margin-bottom: var(--margins-lg);
`;
const PasswordLock = ({
validatePasswordLock,
}: {
validatePasswordLock: () => Promise<boolean>;
}) => {
return (
<div className="session-settings__password-lock">
<div className="session-settings__password-lock-box">
<StyledH3>{window.i18n('passwordViewTitle')}</StyledH3>
<StyledPasswordInput
type="password"
id="password-lock-input"
defaultValue=""
placeholder={window.i18n('enterPassword')}
data-testid="password-lock-input"
autoFocus={true}
/>
<SessionButton
buttonType={SessionButtonType.Simple}
text={window.i18n('done')}
onClick={validatePasswordLock}
/>
</div>
</div>
);
};
const SettingInCategory = (props: { const SettingInCategory = (props: {
category: SessionSettingCategory; category: SessionSettingCategory;
hasPassword: boolean; hasPassword: boolean;
@ -216,38 +177,23 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
}); });
} }
public componentDidMount() { public componentDidUpdate(_: SettingsViewProps, _prevState: State) {
window.addEventListener('keyup', this.onKeyUp); const oldShouldRenderPasswordLock = _prevState.shouldLockSettings && _prevState.hasPassword;
} const newShouldRenderPasswordLock = this.state.shouldLockSettings && this.state.hasPassword;
public componentWillUnmount() { if (
window.removeEventListener('keyup', this.onKeyUp); newShouldRenderPasswordLock &&
} newShouldRenderPasswordLock !== oldShouldRenderPasswordLock
) {
public async validatePasswordLock() { displayPasswordModal('enter', action => {
const enteredPassword = String( if (action === 'enter') {
(document.getElementById('password-lock-input') as HTMLInputElement)?.value // Unlocked settings
); this.setState({
shouldLockSettings: false,
if (!enteredPassword) { });
ToastUtils.pushToastError('validatePassword', window.i18n('noGivenPassword')); }
});
return false;
}
// Check if the password matches the hash we have stored
const hash = await Data.getPasswordHash();
if (hash && !matchesHash(enteredPassword, hash)) {
ToastUtils.pushToastError('validatePassword', window.i18n('invalidPassword'));
return false;
} }
// Unlocked settings
this.setState({
shouldLockSettings: false,
});
return true;
} }
public render() { public render() {
@ -256,22 +202,23 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
return ( return (
<div className="session-settings"> <div className="session-settings">
<SettingsHeader category={category} /> {shouldRenderPasswordLock ? (
<></>
<StyledSettingsView> ) : (
{shouldRenderPasswordLock ? ( <>
<PasswordLock validatePasswordLock={this.validatePasswordLock} /> <SettingsHeader category={category} />
) : ( <StyledSettingsView>
<StyledSettingsList ref={this.settingsViewRef}> <StyledSettingsList ref={this.settingsViewRef}>
<SettingInCategory <SettingInCategory
category={category} category={category}
onPasswordUpdated={this.onPasswordUpdated} onPasswordUpdated={this.onPasswordUpdated}
hasPassword={Boolean(this.state.hasPassword)} hasPassword={Boolean(this.state.hasPassword)}
/> />
</StyledSettingsList> </StyledSettingsList>
)} <SessionInfo />
<SessionInfo /> </StyledSettingsView>
</StyledSettingsView> </>
)}
</div> </div>
); );
} }
@ -290,14 +237,4 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
}); });
} }
} }
private async onKeyUp(event: any) {
const lockPasswordVisible = Boolean(document.getElementById('password-lock-input'));
if (event.key === 'Enter' && lockPasswordVisible) {
await this.validatePasswordLock();
}
event.preventDefault();
}
} }

@ -4,27 +4,13 @@ import useUpdate from 'react-use/lib/useUpdate';
import { Data, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data'; import { Data, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data';
import { SettingsKey } from '../../../data/settings-key'; import { SettingsKey } from '../../../data/settings-key';
import { ConversationTypeEnum } from '../../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../../models/conversationAttributes';
import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { SessionButtonColor } from '../../basic/SessionButton'; import { SessionButtonColor } from '../../basic/SessionButton';
import { SpacerLG } from '../../basic/Text'; import { SpacerLG } from '../../basic/Text';
import { TypingBubble } from '../../conversation/TypingBubble'; import { TypingBubble } from '../../conversation/TypingBubble';
import { PasswordAction } from '../../dialog/SessionPasswordDialog';
import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem'; import { SessionSettingButtonItem, SessionToggleWithDescription } from '../SessionSettingListItem';
import { displayPasswordModal } from '../SessionSettings';
function displayPasswordModal(
passwordAction: PasswordAction,
onPasswordUpdated: (action: string) => void
) {
window.inboxStore?.dispatch(
sessionPassword({
passwordAction,
onOk: () => {
onPasswordUpdated(passwordAction);
},
})
);
}
async function toggleLinkPreviews() { async function toggleLinkPreviews() {
const newValue = !window.getSettingValue(SettingsKey.settingsLinkPreview); const newValue = !window.getSettingValue(SettingsKey.settingsLinkPreview);

@ -51,7 +51,7 @@ test.describe('Password checks', () => {
); );
// Type password into input field // Type password into input field
await typeIntoInput(window, 'password-lock-input', testPassword); await typeIntoInput(window, 'password-input', testPassword);
// Click OK // Click OK
await clickOnMatchingText(window, 'OK'); await clickOnMatchingText(window, 'OK');
// Change password // Change password
@ -99,7 +99,7 @@ test.describe('Password checks', () => {
// Click OK // Click OK
await window.keyboard.press('Enter'); await window.keyboard.press('Enter');
// Type password into input field // Type password into input field
await typeIntoInput(window, 'password-lock-input', testPassword); await typeIntoInput(window, 'password-input', testPassword);
await window.keyboard.press('Delete'); await window.keyboard.press('Delete');
// Click OK // Click OK
await clickOnMatchingText(window, 'OK'); await clickOnMatchingText(window, 'OK');
@ -108,7 +108,7 @@ test.describe('Password checks', () => {
// // Click on settings tab // // Click on settings tab
await clickOnTestIdWithText(window, 'settings-section'); await clickOnTestIdWithText(window, 'settings-section');
// // Try with incorrect password // // Try with incorrect password
await typeIntoInput(window, 'password-lock-input', '0000'); await typeIntoInput(window, 'password-input', '0000');
await window.keyboard.press('Delete'); await window.keyboard.press('Delete');
// Confirm // Confirm
await clickOnMatchingText(window, 'OK'); await clickOnMatchingText(window, 'OK');

Loading…
Cancel
Save