From fd722d1f2fe588fb2da007f6024629c9c00c4478 Mon Sep 17 00:00:00 2001 From: William Grant Date: Wed, 8 May 2024 17:06:34 +1000 Subject: [PATCH] feat: swapped out seed modal for settings category page still work on category component but password protection works nicely --- ts/components/dialog/EnterPasswordModal.tsx | 15 +-- .../leftpane/LeftPaneSettingSection.tsx | 5 +- ts/components/settings/SessionSettings.tsx | 6 +- .../settings/SessionSettingsHeader.tsx | 4 +- .../section/CategoryRecoveryPassword.tsx | 94 +++++++++++++++++++ ts/hooks/usePasswordModal.ts | 28 ++++-- 6 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 ts/components/settings/section/CategoryRecoveryPassword.tsx diff --git a/ts/components/dialog/EnterPasswordModal.tsx b/ts/components/dialog/EnterPasswordModal.tsx index 0eaabd6f9..4c352ea69 100644 --- a/ts/components/dialog/EnterPasswordModal.tsx +++ b/ts/components/dialog/EnterPasswordModal.tsx @@ -1,4 +1,3 @@ -import { Dispatch, SetStateAction } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -18,9 +17,9 @@ const StyledModalContainer = styled.div` export type EnterPasswordModalProps = { passwordHash: string; passwordValid: boolean; - setPasswordValid: Dispatch>; - onClickOk: () => any; - onClickClose: () => any; + setPasswordValid: (value: boolean) => void; + onClickOk?: () => any; + onClickClose?: () => any; title?: string; }; @@ -29,7 +28,9 @@ export const EnterPasswordModal = (props: EnterPasswordModalProps) => { const dispatch = useDispatch(); const onClose = () => { - onClickClose(); + if (onClickClose) { + onClickClose(); + } dispatch(updateEnterPasswordModal(null)); }; @@ -52,7 +53,9 @@ export const EnterPasswordModal = (props: EnterPasswordModalProps) => { window.removeEventListener('keyup', onEnter); - void onClickOk(); + if (onClickOk) { + void onClickOk(); + } }; const onEnter = (event: any) => { diff --git a/ts/components/leftpane/LeftPaneSettingSection.tsx b/ts/components/leftpane/LeftPaneSettingSection.tsx index 4a002b515..2423b96bd 100644 --- a/ts/components/leftpane/LeftPaneSettingSection.tsx +++ b/ts/components/leftpane/LeftPaneSettingSection.tsx @@ -2,7 +2,7 @@ import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { resetConversationExternal } from '../../state/ducks/conversations'; -import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog'; +import { updateDeleteAccountModal } from '../../state/ducks/modalDialog'; import { SectionType, setLeftOverlayMode, @@ -107,9 +107,6 @@ const LeftPaneSettingsCategoryRow = (props: { dispatch(setLeftOverlayMode('message-requests')); dispatch(resetConversationExternal()); break; - case 'recoveryPassword': - dispatch(recoveryPhraseModal({})); - break; case 'clearData': dispatch(updateDeleteAccountModal({})); break; diff --git a/ts/components/settings/SessionSettings.tsx b/ts/components/settings/SessionSettings.tsx index 9899e88db..90829c57a 100644 --- a/ts/components/settings/SessionSettings.tsx +++ b/ts/components/settings/SessionSettings.tsx @@ -19,6 +19,7 @@ import { CategoryConversations } from './section/CategoryConversations'; import { SettingsCategoryHelp } from './section/CategoryHelp'; import { SettingsCategoryPermissions } from './section/CategoryPermissions'; import { SettingsCategoryPrivacy } from './section/CategoryPrivacy'; +import { SettingsCategoryRecoveryPassword } from './section/CategoryRecoveryPassword'; export function displayPasswordModal( passwordAction: PasswordAction, @@ -115,11 +116,12 @@ const SettingInCategory = (props: { return ; case 'permissions': return ; + case 'recoveryPassword': + return ; - // these three down there have no options, they are just a button + // these are just buttons and don't have screens case 'clearData': case 'messageRequests': - case 'recoveryPassword': default: return null; } diff --git a/ts/components/settings/SessionSettingsHeader.tsx b/ts/components/settings/SessionSettingsHeader.tsx index 471c14693..21a4511c0 100644 --- a/ts/components/settings/SessionSettingsHeader.tsx +++ b/ts/components/settings/SessionSettingsHeader.tsx @@ -43,9 +43,11 @@ export const SettingsHeader = (props: Props) => { case 'privacy': categoryTitle = window.i18n('privacySettingsTitle'); break; + case 'recoveryPassword': + categoryTitle = window.i18n('sessionRecoveryPassword'); + break; case 'clearData': case 'messageRequests': - case 'recoveryPassword': throw new Error(`no header for should be tried to be rendered for "${category}"`); default: diff --git a/ts/components/settings/section/CategoryRecoveryPassword.tsx b/ts/components/settings/section/CategoryRecoveryPassword.tsx new file mode 100644 index 000000000..fd3e99f72 --- /dev/null +++ b/ts/components/settings/section/CategoryRecoveryPassword.tsx @@ -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 ( + <> + + { + if (isEmpty(recoveryPhrase)) { + return; + } + copyRecoveryPhrase(recoveryPhrase); + }} + className="session-modal__text-highlight" + data-testid="recovery-phrase-seed-modal" + > + {recoveryPhrase} + + + {/* TODO Toggling between QR and recovery password */} + {/* TODO Permenantly hide button */} + + + ); +}; diff --git a/ts/hooks/usePasswordModal.ts b/ts/hooks/usePasswordModal.ts index b4f4c3087..afe85e4c9 100644 --- a/ts/hooks/usePasswordModal.ts +++ b/ts/hooks/usePasswordModal.ts @@ -1,19 +1,27 @@ import { isEmpty } from 'lodash'; import { useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useMount } from 'react-use'; +import useMount from 'react-use/lib/useMount'; import { Data } from '../data/data'; 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({ + title, onSuccess, onClose, - title, }: { - onSuccess: () => void; - onClose: () => void; title?: string; + onSuccess?: () => void; + onClose?: () => void; }) { + const [hasPassword, setHasPassword] = useState(false); const [passwordHash, setPasswordHash] = useState(''); const [passwordValid, setPasswordValid] = useState(false); @@ -25,6 +33,8 @@ export function usePasswordModal({ } const hash = await Data.getPasswordHash(); + setHasPassword(!!hash); + if (hash && !isEmpty(hash)) { setPasswordHash(hash); dispatch( @@ -33,12 +43,16 @@ export function usePasswordModal({ passwordValid, setPasswordValid, onClickOk: () => { - onSuccess(); + if (onSuccess) { + onSuccess(); + } setPasswordHash(''); dispatch(updateEnterPasswordModal(null)); }, onClickClose: () => { - onClose(); + if (onClose) { + onClose(); + } setPasswordHash(''); dispatch(updateEnterPasswordModal(null)); }, @@ -51,4 +65,6 @@ export function usePasswordModal({ useMount(() => { void validateAccess(); }); + + return { hasPassword, passwordValid }; }