feat: swapped out seed modal for settings category page

still work on category component but password protection works nicely
pull/3083/head
William Grant 1 year ago
parent 01bdc6e1cf
commit fd722d1f2f

@ -1,4 +1,3 @@
import { Dispatch, SetStateAction } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
@ -18,9 +17,9 @@ const StyledModalContainer = styled.div`
export type EnterPasswordModalProps = { export type EnterPasswordModalProps = {
passwordHash: string; passwordHash: string;
passwordValid: boolean; passwordValid: boolean;
setPasswordValid: Dispatch<SetStateAction<boolean>>; setPasswordValid: (value: boolean) => void;
onClickOk: () => any; onClickOk?: () => any;
onClickClose: () => any; onClickClose?: () => any;
title?: string; title?: string;
}; };
@ -29,7 +28,9 @@ export const EnterPasswordModal = (props: EnterPasswordModalProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const onClose = () => { const onClose = () => {
onClickClose(); if (onClickClose) {
onClickClose();
}
dispatch(updateEnterPasswordModal(null)); dispatch(updateEnterPasswordModal(null));
}; };
@ -52,7 +53,9 @@ export const EnterPasswordModal = (props: EnterPasswordModalProps) => {
window.removeEventListener('keyup', onEnter); window.removeEventListener('keyup', onEnter);
void onClickOk(); if (onClickOk) {
void onClickOk();
}
}; };
const onEnter = (event: any) => { const onEnter = (event: any) => {

@ -2,7 +2,7 @@ import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { resetConversationExternal } from '../../state/ducks/conversations'; import { resetConversationExternal } from '../../state/ducks/conversations';
import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog'; import { updateDeleteAccountModal } from '../../state/ducks/modalDialog';
import { import {
SectionType, SectionType,
setLeftOverlayMode, setLeftOverlayMode,
@ -107,9 +107,6 @@ const LeftPaneSettingsCategoryRow = (props: {
dispatch(setLeftOverlayMode('message-requests')); dispatch(setLeftOverlayMode('message-requests'));
dispatch(resetConversationExternal()); dispatch(resetConversationExternal());
break; break;
case 'recoveryPassword':
dispatch(recoveryPhraseModal({}));
break;
case 'clearData': case 'clearData':
dispatch(updateDeleteAccountModal({})); dispatch(updateDeleteAccountModal({}));
break; break;

@ -19,6 +19,7 @@ import { CategoryConversations } from './section/CategoryConversations';
import { SettingsCategoryHelp } from './section/CategoryHelp'; import { SettingsCategoryHelp } from './section/CategoryHelp';
import { SettingsCategoryPermissions } from './section/CategoryPermissions'; import { SettingsCategoryPermissions } from './section/CategoryPermissions';
import { SettingsCategoryPrivacy } from './section/CategoryPrivacy'; import { SettingsCategoryPrivacy } from './section/CategoryPrivacy';
import { SettingsCategoryRecoveryPassword } from './section/CategoryRecoveryPassword';
export function displayPasswordModal( export function displayPasswordModal(
passwordAction: PasswordAction, passwordAction: PasswordAction,
@ -115,11 +116,12 @@ const SettingInCategory = (props: {
return <SettingsCategoryHelp />; return <SettingsCategoryHelp />;
case 'permissions': case 'permissions':
return <SettingsCategoryPermissions />; return <SettingsCategoryPermissions />;
case 'recoveryPassword':
return <SettingsCategoryRecoveryPassword />;
// these three down there have no options, they are just a button // these are just buttons and don't have screens
case 'clearData': case 'clearData':
case 'messageRequests': case 'messageRequests':
case 'recoveryPassword':
default: default:
return null; return null;
} }

@ -43,9 +43,11 @@ export const SettingsHeader = (props: Props) => {
case 'privacy': case 'privacy':
categoryTitle = window.i18n('privacySettingsTitle'); categoryTitle = window.i18n('privacySettingsTitle');
break; break;
case 'recoveryPassword':
categoryTitle = window.i18n('sessionRecoveryPassword');
break;
case 'clearData': case 'clearData':
case 'messageRequests': case 'messageRequests':
case 'recoveryPassword':
throw new Error(`no header for should be tried to be rendered for "${category}"`); throw new Error(`no header for should be tried to be rendered for "${category}"`);
default: default:

@ -0,0 +1,94 @@
import { isEmpty } from 'lodash';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import { usePasswordModal } from '../../../hooks/usePasswordModal';
import { mnDecode } from '../../../session/crypto/mnemonic';
import { ToastUtils } from '../../../session/utils';
import { showSettingsSection } from '../../../state/ducks/section';
import { getTheme } from '../../../state/selectors/theme';
import { getThemeValue } from '../../../themes/globals';
import { getCurrentRecoveryPhrase } from '../../../util/storage';
import { SessionQRCode } from '../../SessionQRCode';
import { SessionSettingsItemWrapper } from '../SessionSettingListItem';
export const SettingsCategoryRecoveryPassword = () => {
const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [hexEncodedSeed, setHexEncodedSeed] = useState('');
const dispatch = useDispatch();
const { hasPassword, passwordValid } = usePasswordModal({
title: window.i18n('sessionRecoveryPassword'),
onClose: () => {
dispatch(showSettingsSection('privacy'));
},
});
const theme = useSelector(getTheme);
const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => {
window.clipboard.writeText(recoveryPhraseToCopy);
ToastUtils.pushCopiedToClipBoard();
};
const fetchRecoverPhrase = () => {
const newRecoveryPhrase = getCurrentRecoveryPhrase();
setRecoveryPhrase(newRecoveryPhrase);
if (!isEmpty(newRecoveryPhrase)) {
setHexEncodedSeed(mnDecode(newRecoveryPhrase, 'english'));
}
setLoadingSeed(false);
};
useMount(() => {
if (!hasPassword || (hasPassword && passwordValid)) {
fetchRecoverPhrase();
}
});
if ((hasPassword && !passwordValid) || loadingSeed) {
return null;
}
return (
<>
<SessionSettingsItemWrapper
// TODO need to support escaping the HTML or passing through a ReactNode
title={window.i18n('sessionRecoveryPassword')}
description={window.i18n('recoveryPasswordDescription')}
inline={false}
>
<i
onClick={() => {
if (isEmpty(recoveryPhrase)) {
return;
}
copyRecoveryPhrase(recoveryPhrase);
}}
className="session-modal__text-highlight"
data-testid="recovery-phrase-seed-modal"
>
{recoveryPhrase}
</i>
<SessionQRCode
id={'session-recovery-passwod'}
value={hexEncodedSeed}
size={240}
backgroundColor={getThemeValue(
theme.includes('dark') ? '--text-primary-color' : '--background-primary-color'
)}
foregroundColor={getThemeValue(
theme.includes('dark') ? '--background-primary-color' : '--text-primary-color'
)}
logoImage={'./images/session/qr/shield.svg'}
logoWidth={56}
logoHeight={56}
logoIsSVG={true}
/>
{/* TODO Toggling between QR and recovery password */}
{/* TODO Permenantly hide button */}
</SessionSettingsItemWrapper>
</>
);
};

@ -1,19 +1,27 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useState } from 'react'; import { useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useMount } from 'react-use'; import useMount from 'react-use/lib/useMount';
import { Data } from '../data/data'; import { Data } from '../data/data';
import { updateEnterPasswordModal } from '../state/ducks/modalDialog'; import { updateEnterPasswordModal } from '../state/ducks/modalDialog';
/**
* Password protection for a component if a password has been set
* @param title - Title of the password modal
* @param onSuccess - Callback when password is correct
* @param onClose - Callback when modal is cancelled or closed. Definitely use this if your component returns null until a password is entered
* @returns An object with two properties - hasPassword which is true if a password has been set, passwordValid which is true if the password entered is correct
*/
export function usePasswordModal({ export function usePasswordModal({
title,
onSuccess, onSuccess,
onClose, onClose,
title,
}: { }: {
onSuccess: () => void;
onClose: () => void;
title?: string; title?: string;
onSuccess?: () => void;
onClose?: () => void;
}) { }) {
const [hasPassword, setHasPassword] = useState(false);
const [passwordHash, setPasswordHash] = useState(''); const [passwordHash, setPasswordHash] = useState('');
const [passwordValid, setPasswordValid] = useState(false); const [passwordValid, setPasswordValid] = useState(false);
@ -25,6 +33,8 @@ export function usePasswordModal({
} }
const hash = await Data.getPasswordHash(); const hash = await Data.getPasswordHash();
setHasPassword(!!hash);
if (hash && !isEmpty(hash)) { if (hash && !isEmpty(hash)) {
setPasswordHash(hash); setPasswordHash(hash);
dispatch( dispatch(
@ -33,12 +43,16 @@ export function usePasswordModal({
passwordValid, passwordValid,
setPasswordValid, setPasswordValid,
onClickOk: () => { onClickOk: () => {
onSuccess(); if (onSuccess) {
onSuccess();
}
setPasswordHash(''); setPasswordHash('');
dispatch(updateEnterPasswordModal(null)); dispatch(updateEnterPasswordModal(null));
}, },
onClickClose: () => { onClickClose: () => {
onClose(); if (onClose) {
onClose();
}
setPasswordHash(''); setPasswordHash('');
dispatch(updateEnterPasswordModal(null)); dispatch(updateEnterPasswordModal(null));
}, },
@ -51,4 +65,6 @@ export function usePasswordModal({
useMount(() => { useMount(() => {
void validateAccess(); void validateAccess();
}); });
return { hasPassword, passwordValid };
} }

Loading…
Cancel
Save