diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 039bcc770..e8a659dc8 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -420,6 +420,7 @@
"readReceiptSettingDescription": "Send read receipts in one-to-one chats.",
"readReceiptSettingTitle": "Read Receipts",
"received": "Received",
+ "recoveryPasswordDescription": "Use your recovery password to load your account on new devices.
Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.",
"recoveryPasswordEnter": "Enter your recovery password",
"recoveryPasswordErrorMessageGeneric": "Please check your recovery password and try again.",
"recoveryPasswordErrorMessageIncorrect": "Some of the words in your Recovery Password are incorrect. Please check and try again.",
diff --git a/ts/components/dialog/EnterPasswordModal.tsx b/ts/components/dialog/EnterPasswordModal.tsx
new file mode 100644
index 000000000..0eaabd6f9
--- /dev/null
+++ b/ts/components/dialog/EnterPasswordModal.tsx
@@ -0,0 +1,106 @@
+import { Dispatch, SetStateAction } from 'react';
+import { useDispatch } from 'react-redux';
+import styled from 'styled-components';
+
+import { ToastUtils } from '../../session/utils';
+import { matchesHash } from '../../util/passwordUtils';
+
+import { updateEnterPasswordModal } from '../../state/ducks/modalDialog';
+import { SpacerSM } from '../basic/Text';
+
+import { SessionWrapperModal } from '../SessionWrapperModal';
+import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
+
+const StyledModalContainer = styled.div`
+ margin: var(--margins-md) var(--margins-sm);
+`;
+
+export type EnterPasswordModalProps = {
+ passwordHash: string;
+ passwordValid: boolean;
+ setPasswordValid: Dispatch>;
+ onClickOk: () => any;
+ onClickClose: () => any;
+ title?: string;
+};
+
+export const EnterPasswordModal = (props: EnterPasswordModalProps) => {
+ const { passwordHash, setPasswordValid, onClickOk, onClickClose, title } = props;
+ const dispatch = useDispatch();
+
+ const onClose = () => {
+ onClickClose();
+ dispatch(updateEnterPasswordModal(null));
+ };
+
+ const confirmPassword = () => {
+ const passwordValue = (document.getElementById('seed-input-password') as any)?.value;
+ const isPasswordValid = matchesHash(passwordValue as string, passwordHash);
+
+ if (!passwordValue) {
+ ToastUtils.pushToastError('enterPasswordErrorToast', window.i18n('noGivenPassword'));
+
+ return;
+ }
+
+ if (passwordHash && !isPasswordValid) {
+ ToastUtils.pushToastError('enterPasswordErrorToast', window.i18n('invalidPassword'));
+ return;
+ }
+
+ setPasswordValid(true);
+
+ window.removeEventListener('keyup', onEnter);
+
+ void onClickOk();
+ };
+
+ const onEnter = (event: any) => {
+ if (event.key === 'Enter') {
+ confirmPassword();
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/ts/components/dialog/ModalContainer.tsx b/ts/components/dialog/ModalContainer.tsx
index 7092ffd90..8d61f51fa 100644
--- a/ts/components/dialog/ModalContainer.tsx
+++ b/ts/components/dialog/ModalContainer.tsx
@@ -7,6 +7,7 @@ import {
getDeleteAccountModalState,
getEditProfileDialog,
getEditProfilePictureModalState,
+ getEnterPasswordModalState,
getInviteContactModal,
getOnionPathDialog,
getReactClearAllDialog,
@@ -22,6 +23,7 @@ import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog';
import { DeleteAccountModal } from './DeleteAccountModal';
import { EditProfileDialog } from './EditProfileDialog';
import { EditProfilePictureModal } from './EditProfilePictureModal';
+import { EnterPasswordModal } from './EnterPasswordModal';
import { InviteContactsDialog } from './InviteContactsDialog';
import { AddModeratorsDialog } from './ModeratorsAddDialog';
import { RemoveModeratorsDialog } from './ModeratorsRemoveDialog';
@@ -30,8 +32,8 @@ import { ReactClearAllModal } from './ReactClearAllModal';
import { ReactListModal } from './ReactListModal';
import { SessionConfirm } from './SessionConfirm';
import { SessionNicknameDialog } from './SessionNicknameDialog';
-import { SessionPasswordDialog } from './SessionPasswordDialog';
import { SessionSeedModal } from './SessionSeedModal';
+import { SessionSetPasswordDialog } from './SessionSetPasswordDialog';
import { UpdateGroupMembersDialog } from './UpdateGroupMembersDialog';
import { UpdateGroupNameDialog } from './UpdateGroupNameDialog';
import { UserDetailsDialog } from './UserDetailsDialog';
@@ -48,6 +50,7 @@ export const ModalContainer = () => {
const editProfileModalState = useSelector(getEditProfileDialog);
const onionPathModalState = useSelector(getOnionPathDialog);
const recoveryPhraseModalState = useSelector(getRecoveryPhraseDialog);
+ const enterPasswordModalState = useSelector(getEnterPasswordModalState);
const sessionPasswordModalState = useSelector(getSessionPasswordDialog);
const deleteAccountModalState = useSelector(getDeleteAccountModalState);
const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState);
@@ -70,7 +73,8 @@ export const ModalContainer = () => {
{editProfileModalState && }
{onionPathModalState && }
{recoveryPhraseModalState && }
- {sessionPasswordModalState && }
+ {enterPasswordModalState && }
+ {sessionPasswordModalState && }
{deleteAccountModalState && }
{confirmModalState && }
{reactListModalState && }
diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx
index 3def16ac3..91ceaff1d 100644
--- a/ts/components/dialog/SessionSeedModal.tsx
+++ b/ts/components/dialog/SessionSeedModal.tsx
@@ -1,97 +1,20 @@
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import useMount from 'react-use/lib/useMount';
import styled from 'styled-components';
-import { Data } from '../../data/data';
import { ToastUtils } from '../../session/utils';
-import { matchesHash } from '../../util/passwordUtils';
import { mnDecode } from '../../session/crypto/mnemonic';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
import { SpacerSM } from '../basic/Text';
+import { usePasswordModal } from '../../hooks/usePasswordModal';
import { getTheme } from '../../state/selectors/theme';
import { getThemeValue } from '../../themes/globals';
import { getCurrentRecoveryPhrase } from '../../util/storage';
import { SessionQRCode } from '../SessionQRCode';
import { SessionWrapperModal } from '../SessionWrapperModal';
-import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
-
-interface PasswordProps {
- setPasswordValid: (val: boolean) => any;
- passwordHash: string;
-}
-
-const Password = (props: PasswordProps) => {
- const { setPasswordValid, passwordHash } = props;
- const i18n = window.i18n;
- const dispatch = useDispatch();
-
- const onClose = () => dispatch(recoveryPhraseModal(null));
-
- const confirmPassword = () => {
- const passwordValue = (document.getElementById('seed-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 (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
+import { SessionButton, SessionButtonType } from '../basic/SessionButton';
interface SeedProps {
recoveryPhrase: string;
@@ -102,7 +25,6 @@ const StyledRecoveryPhrase = styled.i``;
const Seed = (props: SeedProps) => {
const { recoveryPhrase, onClickCopy } = props;
- const i18n = window.i18n;
const dispatch = useDispatch();
const theme = useSelector(getTheme);
@@ -129,7 +51,7 @@ const Seed = (props: SeedProps) => {
maxWidth: '600px',
}}
>
- {i18n('recoveryPhraseSavePromptMain')}
+ {window.i18n('recoveryPhraseSavePromptMain')}
{
style={{ justifyContent: 'center', width: '100%' }}
>
{
copyRecoveryPhrase(recoveryPhrase);
@@ -184,52 +106,35 @@ const SessionSeedModalInner = (props: ModalInnerProps) => {
const { onClickOk } = props;
const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState('');
- const [hasPassword, setHasPassword] = useState(null);
- const [passwordValid, setPasswordValid] = useState(false);
- const [passwordHash, setPasswordHash] = useState('');
const dispatch = useDispatch();
- useMount(() => {
- async function validateAccess() {
- if (passwordHash || recoveryPhrase) {
- return;
- }
-
- const hash = await Data.getPasswordHash();
- setHasPassword(!!hash);
- setPasswordHash(hash || '');
+ const onClose = () => dispatch(recoveryPhraseModal(null));
+ usePasswordModal({
+ onSuccess: () => {
const newRecoveryPhrase = getCurrentRecoveryPhrase();
setRecoveryPhrase(newRecoveryPhrase);
setLoadingSeed(false);
- }
-
- setTimeout(() => (document.getElementById('seed-input-password') as any)?.focus(), 100);
- void validateAccess();
+ },
+ onClose,
+ title: window.window.i18n('sessionRecoveryPassword'),
});
- const onClose = () => dispatch(recoveryPhraseModal(null));
+ if (loadingSeed) {
+ return null;
+ }
return (
- <>
- {!loadingSeed && (
-
-
-
-
- {hasPassword && !passwordValid ? (
-
- ) : (
-
- )}
-
-
- )}
- >
+
+
+
+
+
+
);
};
diff --git a/ts/components/dialog/SessionPasswordDialog.tsx b/ts/components/dialog/SessionSetPasswordDialog.tsx
similarity index 99%
rename from ts/components/dialog/SessionPasswordDialog.tsx
rename to ts/components/dialog/SessionSetPasswordDialog.tsx
index 1f492e48d..f9d88c551 100644
--- a/ts/components/dialog/SessionPasswordDialog.tsx
+++ b/ts/components/dialog/SessionSetPasswordDialog.tsx
@@ -25,7 +25,7 @@ interface State {
currentPasswordRetypeEntered: string | null;
}
-export class SessionPasswordDialog extends Component {
+export class SessionSetPasswordDialog extends Component {
private passportInput: HTMLInputElement | null = null;
constructor(props: any) {
diff --git a/ts/hooks/usePasswordModal.ts b/ts/hooks/usePasswordModal.ts
new file mode 100644
index 000000000..b4f4c3087
--- /dev/null
+++ b/ts/hooks/usePasswordModal.ts
@@ -0,0 +1,54 @@
+import { isEmpty } from 'lodash';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { useMount } from 'react-use';
+import { Data } from '../data/data';
+import { updateEnterPasswordModal } from '../state/ducks/modalDialog';
+
+export function usePasswordModal({
+ onSuccess,
+ onClose,
+ title,
+}: {
+ onSuccess: () => void;
+ onClose: () => void;
+ title?: string;
+}) {
+ const [passwordHash, setPasswordHash] = useState('');
+ const [passwordValid, setPasswordValid] = useState(false);
+
+ const dispatch = useDispatch();
+
+ const validateAccess = async () => {
+ if (!isEmpty(passwordHash)) {
+ return;
+ }
+
+ const hash = await Data.getPasswordHash();
+ if (hash && !isEmpty(hash)) {
+ setPasswordHash(hash);
+ dispatch(
+ updateEnterPasswordModal({
+ passwordHash,
+ passwordValid,
+ setPasswordValid,
+ onClickOk: () => {
+ onSuccess();
+ setPasswordHash('');
+ dispatch(updateEnterPasswordModal(null));
+ },
+ onClickClose: () => {
+ onClose();
+ setPasswordHash('');
+ dispatch(updateEnterPasswordModal(null));
+ },
+ title,
+ })
+ );
+ }
+ };
+
+ useMount(() => {
+ void validateAccess();
+ });
+}
diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx
index 715c8c5c0..d72cbe257 100644
--- a/ts/state/ducks/modalDialog.tsx
+++ b/ts/state/ducks/modalDialog.tsx
@@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { EnterPasswordModalProps } from '../../components/dialog/EnterPasswordModal';
import { SessionConfirmDialogProps } from '../../components/dialog/SessionConfirm';
import type { EditProfilePictureModalProps, PasswordAction } from '../../types/ReduxTypes';
@@ -19,6 +20,7 @@ export type ChangeNickNameModalState = InviteContactModalState;
export type EditProfileModalState = object | null;
export type OnionPathModalState = EditProfileModalState;
export type RecoveryPhraseModalState = EditProfileModalState;
+export type EnterPasswordModalState = EnterPasswordModalProps | null;
export type DeleteAccountModalState = EditProfileModalState;
export type SessionPasswordModalState = { passwordAction: PasswordAction; onOk: () => void } | null;
@@ -49,6 +51,7 @@ export type ModalState = {
editProfileModal: EditProfileModalState;
onionPathModal: OnionPathModalState;
recoveryPhraseModal: RecoveryPhraseModalState;
+ enterPasswordModal: EnterPasswordModalState;
sessionPasswordModal: SessionPasswordModalState;
deleteAccountModal: DeleteAccountModalState;
reactListModalState: ReactModalsState;
@@ -69,6 +72,7 @@ export const initialModalState: ModalState = {
editProfileModal: null,
onionPathModal: null,
recoveryPhraseModal: null,
+ enterPasswordModal: null,
sessionPasswordModal: null,
deleteAccountModal: null,
reactListModalState: null,
@@ -116,6 +120,9 @@ const ModalSlice = createSlice({
recoveryPhraseModal(state, action: PayloadAction) {
return { ...state, recoveryPhraseModal: action.payload };
},
+ updateEnterPasswordModal(state, action: PayloadAction) {
+ return { ...state, enterPasswordModal: action.payload };
+ },
sessionPassword(state, action: PayloadAction) {
return { ...state, sessionPasswordModal: action.payload };
},
@@ -147,6 +154,7 @@ export const {
editProfileModal,
onionPathModal,
recoveryPhraseModal,
+ updateEnterPasswordModal,
sessionPassword,
updateDeleteAccountModal,
updateBanOrUnbanUserModal,
diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts
index beaa1ce41..d682048fb 100644
--- a/ts/state/selectors/modal.ts
+++ b/ts/state/selectors/modal.ts
@@ -8,6 +8,7 @@ import {
DeleteAccountModalState,
EditProfileModalState,
EditProfilePictureModalState,
+ EnterPasswordModalState,
InviteContactModalState,
ModalState,
OnionPathModalState,
@@ -85,6 +86,11 @@ export const getRecoveryPhraseDialog = createSelector(
(state: ModalState): RecoveryPhraseModalState => state.recoveryPhraseModal
);
+export const getEnterPasswordModalState = createSelector(
+ getModal,
+ (state: ModalState): EnterPasswordModalState => state.enterPasswordModal
+);
+
export const getSessionPasswordDialog = createSelector(
getModal,
(state: ModalState): SessionPasswordModalState => state.sessionPasswordModal
diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts
index 4f4d2ff8e..8b04bd121 100644
--- a/ts/types/LocalizerKeys.ts
+++ b/ts/types/LocalizerKeys.ts
@@ -420,6 +420,7 @@ export type LocalizerKeys =
| 'readReceiptSettingDescription'
| 'readReceiptSettingTitle'
| 'received'
+ | 'recoveryPasswordDescription'
| 'recoveryPasswordEnter'
| 'recoveryPasswordErrorMessageGeneric'
| 'recoveryPasswordErrorMessageIncorrect'