Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove app setting password protection #2954

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
145 changes: 92 additions & 53 deletions ts/components/dialog/DeleteAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useMount } from 'react-use';
import { ed25519Str } from '../../session/onions/onionPath';
import { SnodeAPI } from '../../session/apis/snode_api/SNodeAPI';
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/sync/syncUtils';
Expand All @@ -12,6 +13,7 @@ import { SessionWrapperModal } from '../SessionWrapperModal';
import { Data } from '../../data/data';
import { deleteAllLogs } from '../../node/logs';
import { SessionRadioGroup } from '../basic/SessionRadioGroup';
import { Password, loadPassword, newVerificationState } from './SessionPasswordVerification';

const deleteDbLocally = async () => {
window?.log?.info('last message sent successfully. Deleting everything');
Expand Down Expand Up @@ -172,11 +174,71 @@ const DescriptionWhenAskingConfirmation = (props: { deleteMode: DeleteModes }) =
};

export const DeleteAccountModal = () => {
const [isLoading, setIsLoading] = useState(false);
const [askingConfirmation, setAskingConfirmation] = useState(false);
const [deleteMode, setDeleteMode] = useState<DeleteModes>(DEVICE_ONLY);

const [askingConfirmation, setAskingConfirmation] = useState(false);
const [verificationState, setVerificationState] = useState(newVerificationState());
const dispatch = useDispatch();
const { loadingPassword, passwordValid, hasPassword } = verificationState;

/**
* Performs specified on close action then removes the modal.
*/
const onClickCancelHandler = useCallback(() => {
dispatch(updateDeleteAccountModal(null));
}, [dispatch]);

useMount(() => {
void loadPassword(verificationState, setVerificationState);
});

return (
<>
{!loadingPassword && (
<SessionWrapperModal
title={window.i18n('clearAllData')}
onClose={onClickCancelHandler}
showExitIcon={true}
>
{hasPassword && !passwordValid ? (
<Password
verificationState={verificationState}
setVerificationState={setVerificationState}
onClose={onClickCancelHandler}
/>
) : (
<>
{askingConfirmation ? (
<DescriptionWhenAskingConfirmation deleteMode={deleteMode} />
) : (
<DescriptionBeforeAskingConfirmation
deleteMode={deleteMode}
setDeleteMode={setDeleteMode}
/>
)}
<DeleteAccount
onClose={onClickCancelHandler}
deleteMode={deleteMode}
askingConfirmation={askingConfirmation}
setAskingConfirmation={setAskingConfirmation}
/>
</>
)}
</SessionWrapperModal>
)}
</>
);
};

interface DeleteAccountProps {
deleteMode: DeleteModes;
onClose: () => any;
askingConfirmation: boolean;
setAskingConfirmation: (val: boolean) => any;
}

const DeleteAccount = (props: DeleteAccountProps) => {
const { deleteMode, onClose, askingConfirmation, setAskingConfirmation } = props;
const [isLoading, setIsLoading] = useState(false);

const onDeleteEverythingLocallyOnly = async () => {
if (!isLoading) {
Expand Down Expand Up @@ -206,59 +268,36 @@ export const DeleteAccountModal = () => {
}
};

/**
* Performs specified on close action then removes the modal.
*/
const onClickCancelHandler = useCallback(() => {
dispatch(updateDeleteAccountModal(null));
}, [dispatch]);

return (
<SessionWrapperModal
title={window.i18n('clearAllData')}
onClose={onClickCancelHandler}
showExitIcon={true}
>
{askingConfirmation ? (
<DescriptionWhenAskingConfirmation deleteMode={deleteMode} />
) : (
<DescriptionBeforeAskingConfirmation
deleteMode={deleteMode}
setDeleteMode={setDeleteMode}
<div className="session-modal__centered">
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('clear')}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={() => {
if (!askingConfirmation) {
setAskingConfirmation(true);
return;
}
if (deleteMode === 'device_only') {
void onDeleteEverythingLocallyOnly();
} else if (deleteMode === 'device_and_network') {
void onDeleteEverythingAndNetworkData();
}
}}
disabled={isLoading}
/>
)}
<div className="session-modal__centered">
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('clear')}
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={() => {
if (!askingConfirmation) {
setAskingConfirmation(true);
return;
}
if (deleteMode === 'device_only') {
void onDeleteEverythingLocallyOnly();
} else if (deleteMode === 'device_and_network') {
void onDeleteEverythingAndNetworkData();
}
}}
disabled={isLoading}
/>

<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
onClick={() => {
dispatch(updateDeleteAccountModal(null));
}}
disabled={isLoading}
/>
</div>
<SpacerLG />
<SessionSpinner loading={isLoading} />
<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
onClick={onClose}
disabled={isLoading}
/>
</div>
</SessionWrapperModal>
<SpacerLG />
<SessionSpinner loading={isLoading} />
</div>
);
};
123 changes: 123 additions & 0 deletions ts/components/dialog/SessionPasswordVerification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import { useMount } from 'react-use';
import { ToastUtils } from '../../session/utils';
import { matchesHash } from '../../util/passwordUtils';
import { SpacerSM } from '../basic/Text';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { Data } from '../../data/data';

interface PasswordVerificationState {
loadingPassword: boolean;
hasPassword: boolean | null;
passwordHash: string;
passwordValid: boolean;
}

export function newVerificationState(): PasswordVerificationState {
return {
loadingPassword: true,
hasPassword: null,
passwordHash: '',
passwordValid: false,
};
}

export async function loadPassword(
state: PasswordVerificationState,
setState: (val: PasswordVerificationState) => any
) {
if (!state.loadingPassword) {
return;
}

const hash = await Data.getPasswordHash();

setState({
...state,
hasPassword: !!hash,
passwordHash: hash || '',
loadingPassword: false,
});
}

export interface PasswordProps {
verificationState: PasswordVerificationState;
setVerificationState: (val: PasswordVerificationState) => any;
onClose: () => any;
}

export const Password = (props: PasswordProps) => {
const { verificationState, setVerificationState, onClose } = props;
const { passwordHash } = verificationState;
const i18n = window.i18n;

useMount(() => {
setTimeout(() => (document.getElementById('input-password') as any)?.focus(), 100);
});

const setPasswordValid = (passwordValid: boolean) => {
setVerificationState({
...verificationState,
passwordValid,
});
};

const confirmPassword = () => {
const passwordValue = (document.getElementById('input-password') as any)?.value;
const isPasswordValid = matchesHash(passwordValue as string, passwordHash);

if (!passwordValue) {
ToastUtils.pushToastError('enterPasswordErrorToast', i18n('noGivenPassword'));

return false;
}

if (passwordHash && !isPasswordValid) {
ToastUtils.pushToastError('enterPasswordErrorToast', i18n('invalidPassword'));
return false;
}

setPasswordValid(true);

window.removeEventListener('keyup', onEnter);
return true;
};

const onEnter = (event: any) => {
if (event.key === 'Enter') {
confirmPassword();
}
};

return (
<>
<div className="session-modal__input-group">
<input
type="password"
id="input-password"
placeholder={i18n('enterPassword')}
onKeyUp={onEnter}
/>
</div>

<SpacerSM />

<div
className="session-modal__button-group"
style={{ justifyContent: 'center', width: '100%' }}
>
<SessionButton
text={i18n('done')}
buttonType={SessionButtonType.Simple}
onClick={confirmPassword}
/>
<SessionButton
text={i18n('cancel')}
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={onClose}
/>
</div>
</>
);
};