Skip to content

Commit 434cffa

Browse files
authored
A-1211619233334096: seed phrase validation (#250)
* style: error and textarea styles improvements * feat: improve seed pharase validation * refactoring: restore account page * test: add new unit tests * bump version
1 parent 5401cf1 commit 434cffa

File tree

15 files changed

+271
-58
lines changed

15 files changed

+271
-58
lines changed

package-lock.json

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browser-extension",
3-
"version": "1.5.2",
3+
"version": "1.5.3",
44
"private": true,
55
"dependencies": {
66
"@bitcoinerlab/secp256k1": "^1.2.0",
@@ -19,7 +19,6 @@
1919
"date-fns": "^4.1.0",
2020
"decimal.js": "^10.6.0",
2121
"ecpair": "3.0.0",
22-
"jest-webgl-canvas-mock": "^2.5.3",
2322
"konva": "^10.2.0",
2423
"node-forge": "^1.3.3",
2524
"pbkdf2": "^3.1.5",
@@ -88,6 +87,7 @@
8887
"jest": "^30.2.0",
8988
"jest-canvas-mock": "^2.5.2",
9089
"jest-environment-jsdom": "^30.2.0",
90+
"jest-webgl-canvas-mock": "^2.5.3",
9191
"prettier": "^3.8.1",
9292
"pretty-quick": "^4.2.2",
9393
"style-loader": "^4.0.0",

public/manifestDefault.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Mojito - A Mintlayer Wallet",
4-
"version": "1.5.2",
4+
"version": "1.5.3",
55
"short_name": "Mojito",
66
"description": "Mojito is a non-custodial decentralized crypto wallet that lets you send and receive BTC and ML from any other address.",
77
"homepage_url": "https://www.mintlayer.org/",

public/manifestFirefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Mojito - A Mintlayer Wallet",
4-
"version": "1.5.2",
4+
"version": "1.5.3",
55
"description": "Mojito is a non-custodial decentralized crypto wallet that lets you send and receive BTC and ML from any other address.",
66
"homepage_url": "https://www.mintlayer.org/",
77
"icons": {

src/components/basic/Error/Error.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
.errorMessage * {
77
color: rgb(var(--color-red));
88
text-align: center;
9-
font-weight: bold;
9+
font-weight: 600;
1010
}

src/components/basic/Textarea/Textarea.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.textarea {
22
width: 100%;
3-
padding: 15px;
3+
padding: 15px 15px 30px;
44
resize: none;
55
border: 1px solid rgb(var(--color-light-gray));
66
border-radius: var(--round-size);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.seed-field-wrapper {
2+
position: relative;
3+
}
4+
5+
.seed-word-counter {
6+
position: absolute;
7+
bottom: 1rem;
8+
right: 1rem;
9+
font-size: 0.75rem;
10+
opacity: 0.5;
11+
pointer-events: none;
12+
}
13+
14+
.seed-word-counter.counter-valid {
15+
color: var(--success-color, #43a047);
16+
opacity: 1;
17+
}

src/components/composed/RestoreSeedField/RestoreSeedField.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { useState } from 'react'
22
import { Textarea } from '@BasicComponents'
33

4+
import './RestoreSeedField.css'
5+
46
const RestoreSeedField = ({ setFields, accountWordsValid }) => {
57
const [textareaValue, setTextareaValue] = useState('')
8+
const [wordCount, setWordCount] = useState(0)
69

710
const onChangeHandler = ({ target }) => {
811
setTextareaValue(target.value)
9-
const words = target.value.trim().split(' ')
12+
const words = target.value.trim().split(/\s+/).filter(Boolean)
13+
setWordCount(words.length)
1014
setFields(words)
1115
}
1216

@@ -15,14 +19,33 @@ const RestoreSeedField = ({ setFields, accountWordsValid }) => {
1519
rows: 14,
1620
}
1721

22+
const getCounterLabel = () => {
23+
if (wordCount === 0) return null
24+
if (wordCount <= 12) return `${wordCount} / 12`
25+
if (wordCount <= 24) return `${wordCount} / 24`
26+
return `${wordCount} / 24`
27+
}
28+
29+
const counterLabel = getCounterLabel()
30+
1831
return (
19-
<Textarea
20-
value={textareaValue}
21-
onChange={onChangeHandler}
22-
id="restore-seed-textarea"
23-
size={textariaSize}
24-
validity={accountWordsValid}
25-
/>
32+
<div className="seed-field-wrapper">
33+
<Textarea
34+
value={textareaValue}
35+
onChange={onChangeHandler}
36+
id="restore-seed-textarea"
37+
size={textariaSize}
38+
validity={accountWordsValid}
39+
/>
40+
{counterLabel && (
41+
<p
42+
className={`seed-word-counter${accountWordsValid ? ' counter-valid' : ''}`}
43+
data-testid="seed-word-counter"
44+
>
45+
{counterLabel}
46+
</p>
47+
)}
48+
</div>
2649
)
2750
}
2851

src/components/composed/RestoreSeedField/RestoreSeedField.test.js

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,194 @@ describe('RestoreSeedField', () => {
123123
target: { value: 'word1 word2 word3' },
124124
})
125125

126-
// Note: split(' ') will create empty strings for multiple spaces
126+
expect(mockSetFields).toHaveBeenCalledWith(['word1', 'word2', 'word3'])
127+
})
128+
129+
it('handles newlines between words (copy-paste from password manager)', () => {
130+
render(
131+
<RestoreSeedField
132+
setFields={mockSetFields}
133+
accountWordsValid={true}
134+
/>,
135+
)
136+
137+
const textarea = screen.getByRole('textbox')
138+
fireEvent.change(textarea, {
139+
target: { value: 'word1\nword2\nword3' },
140+
})
141+
142+
expect(mockSetFields).toHaveBeenCalledWith(['word1', 'word2', 'word3'])
143+
})
144+
145+
it('handles tabs between words', () => {
146+
render(
147+
<RestoreSeedField
148+
setFields={mockSetFields}
149+
accountWordsValid={true}
150+
/>,
151+
)
152+
153+
const textarea = screen.getByRole('textbox')
154+
fireEvent.change(textarea, {
155+
target: { value: 'word1\tword2\tword3' },
156+
})
157+
158+
expect(mockSetFields).toHaveBeenCalledWith(['word1', 'word2', 'word3'])
159+
})
160+
161+
it('handles trailing space without adding empty word', () => {
162+
render(
163+
<RestoreSeedField
164+
setFields={mockSetFields}
165+
accountWordsValid={true}
166+
/>,
167+
)
168+
169+
const textarea = screen.getByRole('textbox')
170+
fireEvent.change(textarea, {
171+
target: { value: 'word1 word2 word3 ' },
172+
})
173+
174+
expect(mockSetFields).toHaveBeenCalledWith(['word1', 'word2', 'word3'])
175+
})
176+
177+
it('handles mixed whitespace (spaces, newlines, tabs)', () => {
178+
render(
179+
<RestoreSeedField
180+
setFields={mockSetFields}
181+
accountWordsValid={true}
182+
/>,
183+
)
184+
185+
const textarea = screen.getByRole('textbox')
186+
fireEvent.change(textarea, {
187+
target: { value: 'word1 word2\nword3\t word4' },
188+
})
189+
127190
expect(mockSetFields).toHaveBeenCalledWith([
128191
'word1',
129-
'',
130-
'',
131192
'word2',
132-
'',
133-
'',
134-
'',
135193
'word3',
194+
'word4',
136195
])
137196
})
197+
198+
it('returns empty array for blank input', () => {
199+
render(
200+
<RestoreSeedField
201+
setFields={mockSetFields}
202+
accountWordsValid={false}
203+
/>,
204+
)
205+
206+
const textarea = screen.getByRole('textbox')
207+
fireEvent.change(textarea, {
208+
target: { value: ' ' },
209+
})
210+
211+
expect(mockSetFields).toHaveBeenCalledWith([])
212+
})
213+
214+
it('does not show counter when input is empty', () => {
215+
render(
216+
<RestoreSeedField
217+
setFields={mockSetFields}
218+
accountWordsValid={false}
219+
/>,
220+
)
221+
222+
expect(screen.queryByTestId('seed-word-counter')).not.toBeInTheDocument()
223+
})
224+
225+
it('shows counter as N/12 when word count is 12 or less', () => {
226+
render(
227+
<RestoreSeedField
228+
setFields={mockSetFields}
229+
accountWordsValid={false}
230+
/>,
231+
)
232+
233+
const textarea = screen.getByRole('textbox')
234+
fireEvent.change(textarea, { target: { value: 'word1 word2 word3' } })
235+
236+
expect(screen.getByTestId('seed-word-counter')).toHaveTextContent('3 / 12')
237+
})
238+
239+
it('shows counter as 12/12 when exactly 12 words', () => {
240+
render(
241+
<RestoreSeedField
242+
setFields={mockSetFields}
243+
accountWordsValid={false}
244+
/>,
245+
)
246+
247+
const textarea = screen.getByRole('textbox')
248+
fireEvent.change(textarea, {
249+
target: { value: 'a b c d e f g h i j k l' },
250+
})
251+
252+
expect(screen.getByTestId('seed-word-counter')).toHaveTextContent('12 / 12')
253+
})
254+
255+
it('shows counter as N/24 when word count is between 13 and 24', () => {
256+
render(
257+
<RestoreSeedField
258+
setFields={mockSetFields}
259+
accountWordsValid={false}
260+
/>,
261+
)
262+
263+
const textarea = screen.getByRole('textbox')
264+
fireEvent.change(textarea, {
265+
target: { value: 'a b c d e f g h i j k l m' },
266+
})
267+
268+
expect(screen.getByTestId('seed-word-counter')).toHaveTextContent('13 / 24')
269+
})
270+
271+
it('shows counter as 24/24 when exactly 24 words', () => {
272+
render(
273+
<RestoreSeedField
274+
setFields={mockSetFields}
275+
accountWordsValid={false}
276+
/>,
277+
)
278+
279+
const textarea = screen.getByRole('textbox')
280+
fireEvent.change(textarea, {
281+
target: { value: 'a b c d e f g h i j k l m n o p q r s t u v w x' },
282+
})
283+
284+
expect(screen.getByTestId('seed-word-counter')).toHaveTextContent('24 / 24')
285+
})
286+
287+
it('counter has counter-valid class when accountWordsValid is true', () => {
288+
render(
289+
<RestoreSeedField
290+
setFields={mockSetFields}
291+
accountWordsValid={true}
292+
/>,
293+
)
294+
295+
const textarea = screen.getByRole('textbox')
296+
fireEvent.change(textarea, { target: { value: 'word1 word2' } })
297+
298+
expect(screen.getByTestId('seed-word-counter')).toHaveClass('counter-valid')
299+
})
300+
301+
it('counter does not have counter-valid class when accountWordsValid is false', () => {
302+
render(
303+
<RestoreSeedField
304+
setFields={mockSetFields}
305+
accountWordsValid={false}
306+
/>,
307+
)
308+
309+
const textarea = screen.getByRole('textbox')
310+
fireEvent.change(textarea, { target: { value: 'word1 word2' } })
311+
312+
expect(screen.getByTestId('seed-word-counter')).not.toHaveClass(
313+
'counter-valid',
314+
)
315+
})
138316
})

0 commit comments

Comments
 (0)