Skip to content

Commit 05262ee

Browse files
atepemmohandast52Tanya-atatakai
authored
feat [OPE-893]: FE - Implement accessing seed phrase from within Pearl (#1338)
* style: layout implement * feat: get phrase from password * feat: remove alerts after first copy * style: fix alert order on side panel * feat: enhance Backup Seed Phrase alert and Recovery Modal UI * feat: improve RecoveryModal functionality with error handling and message feedback * feat: implement recovery phrase retrieval and enhance RecoveryModal functionality * refactor: simplify conditionals and parameter names in BackupSeedPhraseAlert and RecoveryModal * feat: enhance RecoveryPhraseStep with copy handling and unmount backup logic * feat: add hooks for recovery seed phrase and password validation with error handling * feat: integrate recovery seed phrase retrieval with improved error handling and UI feedback * feat: remove recoveryphrase alert and section if no mnemonic * chore: merge main * chore: address pr comments and fix for initial signup --------- Co-authored-by: mohandast52 <mohandast52@gmail.com> Co-authored-by: Atatakai <densatsu@mail.ru>
1 parent a8f9787 commit 05262ee

19 files changed

Lines changed: 492 additions & 41 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Button, Flex, Typography } from 'antd';
2+
import { TbShieldHalfFilled } from 'react-icons/tb';
3+
4+
import { Pages } from '@/enums';
5+
import {
6+
useMnemonicExists,
7+
usePageState,
8+
useRecoveryPhraseBackup,
9+
} from '@/hooks';
10+
11+
import { Alert } from '../ui';
12+
13+
const { Text } = Typography;
14+
15+
export const BackupSeedPhraseAlert = () => {
16+
const { goto: gotoPage } = usePageState();
17+
const { isBackedUp } = useRecoveryPhraseBackup();
18+
const { mnemonicExists } = useMnemonicExists();
19+
20+
// Don't show alert if mnemonic doesn't exist or if already backed up
21+
if (!mnemonicExists || isBackedUp) return null;
22+
23+
return (
24+
<Alert
25+
type="warning"
26+
className="mt-auto mb-16"
27+
message={
28+
<Flex vertical gap={10}>
29+
<TbShieldHalfFilled fontSize={20} />
30+
<Text className="text-sm">
31+
Back up your Secret Recovery Phrase to never lose access.
32+
</Text>
33+
<Button
34+
type="default"
35+
size="small"
36+
className="w-fit"
37+
onClick={() => gotoPage(Pages.Settings)}
38+
>
39+
Back Up Recovery Phrase
40+
</Button>
41+
</Flex>
42+
}
43+
/>
44+
);
45+
};

frontend/components/MainPageV1/Sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from '@/hooks';
4141
import { AgentConfig } from '@/types/Agent';
4242

43+
import { BackupSeedPhraseAlert } from './BackupSeedPhraseAlert';
4344
import { UpdateAvailableAlert } from './UpdateAvailableAlert/UpdateAvailableAlert';
4445
import { UpdateAvailableModal } from './UpdateAvailableAlert/UpdateAvailableModal';
4546

@@ -275,6 +276,7 @@ export const Sidebar = () => {
275276
</div>
276277

277278
<div>
279+
<BackupSeedPhraseAlert />
278280
<UpdateAvailableAlert />
279281
<UpdateAvailableModal />
280282

frontend/components/MainPageV1/UpdateAvailableAlert/UpdateAvailableAlert.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const UpdateAvailableAlert = () => {
2222
return (
2323
<Alert
2424
type="info"
25-
className="mt-auto mb-16"
25+
className="mt-auto mb-16 text-sm"
2626
message={
2727
<Flex vertical gap={2}>
2828
<TbDownload

frontend/components/PearlWallet/Withdraw/EnterWithdrawalAddress/EnterPasswordBeforeWithdrawal.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { useMutation } from '@tanstack/react-query';
21
import { Button, Flex, Input, Typography } from 'antd';
32
import { useCallback } from 'react';
43

54
import { Alert } from '@/components/ui';
6-
import { AccountService } from '@/service/Account';
7-
import { getErrorMessage } from '@/utils/error';
5+
import { useValidatePassword } from '@/hooks';
86

97
const { Text } = Typography;
108

@@ -17,28 +15,6 @@ const PasswordLabel = () => (
1715
</Text>
1816
);
1917

20-
const useValidatePassword = () => {
21-
const { isPending, isSuccess, isError, mutateAsync } = useMutation({
22-
mutationFn: async (password: string) =>
23-
await AccountService.loginAccount(password),
24-
});
25-
26-
const validatePassword = useCallback(
27-
async (password: string) => {
28-
try {
29-
await mutateAsync(password);
30-
return true;
31-
} catch (e) {
32-
console.error(getErrorMessage(e));
33-
return false;
34-
}
35-
},
36-
[mutateAsync],
37-
);
38-
39-
return { isLoading: isPending, isSuccess, isError, validatePassword };
40-
};
41-
4218
type EnterPasswordBeforeWithdrawalProps = {
4319
password: string;
4420
onPasswordChange: (value: string) => void;
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { Alert, Button, Flex, Form, Input, Typography } from 'antd';
2+
import { useCallback, useState } from 'react';
3+
import { TbCopy, TbCopyCheck } from 'react-icons/tb';
4+
import styled from 'styled-components';
5+
import { useUnmount } from 'usehooks-ts';
6+
7+
import { COLOR } from '@/constants';
8+
import { useMessageApi } from '@/context/MessageProvider';
9+
import { useRecoveryPhraseBackup } from '@/hooks';
10+
import { ValueOf } from '@/types';
11+
import { copyToClipboard } from '@/utils';
12+
13+
import { Modal } from '../ui';
14+
import { useRecoverySeedPhrase } from './useRecoverySeedPhrase';
15+
16+
const { Paragraph } = Typography;
17+
18+
const RecoveryWordContainer = styled.div`
19+
padding: 4px 8px;
20+
background-color: ${COLOR.GRAY_1};
21+
border-radius: 8px;
22+
`;
23+
24+
const initialDescription =
25+
'The Secret Recovery Phrase provides full access to your Pearl wallet and agent funds.';
26+
const recoveryPhraseDescription =
27+
"Store it in a safe place that only you can access and won't forget.";
28+
29+
type RecoveryPhraseStepProps = {
30+
phrase: string[];
31+
isCopied: boolean;
32+
onCopy: (isCopied: boolean) => void;
33+
};
34+
const RecoveryPhraseStep = ({
35+
phrase,
36+
isCopied,
37+
onCopy,
38+
}: RecoveryPhraseStepProps) => {
39+
const message = useMessageApi();
40+
41+
const handleCopy = useCallback(async () => {
42+
try {
43+
await copyToClipboard(phrase.join(' '));
44+
message.success('Recovery phrase copied!');
45+
onCopy(true);
46+
} catch (e) {
47+
message.error('Failed to copy recovery phrase.');
48+
console.error(e);
49+
}
50+
}, [phrase, message, onCopy]);
51+
52+
return (
53+
<Flex vertical gap={12} className="mt-12">
54+
<Flex wrap gap={10}>
55+
{phrase.map((word: string, index: number) => (
56+
<RecoveryWordContainer key={`recovery-word-${index}`}>
57+
{word}
58+
</RecoveryWordContainer>
59+
))}
60+
</Flex>
61+
<Button
62+
icon={isCopied ? <TbCopyCheck size={16} /> : <TbCopy size={16} />}
63+
onClick={handleCopy}
64+
type="primary"
65+
className="w-full text-sm mt-12"
66+
>
67+
{isCopied ? 'Copied' : 'Copy to Clipboard'}
68+
</Button>
69+
</Flex>
70+
);
71+
};
72+
73+
const STEPS = {
74+
PASSWORD: 'password',
75+
RECOVERY_PHRASE: 'recoveryPhrase',
76+
} as const;
77+
78+
type RecoveryModalProps = {
79+
open: boolean;
80+
onClose: () => void;
81+
};
82+
83+
export const RecoveryModal = ({ open, onClose }: RecoveryModalProps) => {
84+
const [form] = Form.useForm();
85+
const { isLoading, getRecoverySeedPhrase, errorMessage } =
86+
useRecoverySeedPhrase();
87+
const { markAsBackedUp } = useRecoveryPhraseBackup();
88+
89+
const [recoveryPhrase, setRecoveryPhrase] = useState<string[]>([]);
90+
const [step, setStep] = useState<ValueOf<typeof STEPS>>('password');
91+
const [isCopied, setIsCopied] = useState(false);
92+
93+
const handleReveal = useCallback(
94+
async (values: { password: string }) => {
95+
try {
96+
const data = await getRecoverySeedPhrase(values.password);
97+
setRecoveryPhrase(data.mnemonic);
98+
setStep(STEPS.RECOVERY_PHRASE);
99+
} catch (e: unknown) {
100+
console.error(e);
101+
}
102+
},
103+
[setRecoveryPhrase, setStep, getRecoverySeedPhrase],
104+
);
105+
106+
const onCopy = useCallback((copied: boolean) => {
107+
setIsCopied(copied);
108+
}, []);
109+
110+
// Mark recovery phrase as backed up on unmount if it was copied to clipboard
111+
// and the recovery phrase exists.
112+
useUnmount(() => {
113+
if (recoveryPhrase && isCopied) {
114+
markAsBackedUp();
115+
}
116+
});
117+
118+
const StepsAction =
119+
step === STEPS.PASSWORD ? (
120+
<>
121+
<Paragraph className="mt-8 mb-0 text-neutral-secondary">
122+
Enter your Pearl password to continue.
123+
</Paragraph>
124+
{errorMessage && (
125+
<Alert
126+
message={errorMessage}
127+
type="error"
128+
showIcon
129+
className="mt-12 w-full"
130+
/>
131+
)}
132+
<Form
133+
form={form}
134+
onFinish={handleReveal}
135+
layout="vertical"
136+
className="w-full mt-12"
137+
>
138+
<Form.Item
139+
label="Password"
140+
name="password"
141+
rules={[{ required: true, message: 'Please enter your password' }]}
142+
>
143+
<Input.Password disabled={isLoading} />
144+
</Form.Item>
145+
146+
<Flex justify="flex-end" gap={12} className="w-full">
147+
<Button onClick={onClose}>Cancel</Button>
148+
<Form.Item className="mb-0">
149+
<Button
150+
type="primary"
151+
htmlType="submit"
152+
className="w-full"
153+
loading={isLoading}
154+
>
155+
Reveal Recovery Phrase
156+
</Button>
157+
</Form.Item>
158+
</Flex>
159+
</Form>
160+
</>
161+
) : (
162+
<RecoveryPhraseStep
163+
phrase={recoveryPhrase}
164+
isCopied={isCopied}
165+
onCopy={onCopy}
166+
/>
167+
);
168+
169+
return (
170+
<Modal
171+
open={open}
172+
onCancel={onClose}
173+
title="Secret Recovery Phrase"
174+
description={
175+
step === 'password' ? initialDescription : recoveryPhraseDescription
176+
}
177+
action={StepsAction}
178+
size="small"
179+
destroyOnHidden
180+
closable
181+
/>
182+
);
183+
};

0 commit comments

Comments
 (0)