From 5411580777ca09eef272c65d58a167dd107d09f9 Mon Sep 17 00:00:00 2001 From: Zachary Johnson Date: Wed, 24 Jan 2024 22:11:27 -0500 Subject: [PATCH 1/5] automatically jump to the next input box when confirming the seed phrase backup --- src/components/MnemonicPhraseInput.tsx | 19 +++++++++++++++++++ src/components/MnemonicWordInput.tsx | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/MnemonicPhraseInput.tsx b/src/components/MnemonicPhraseInput.tsx index 7f6ff21dc..cc53a56fe 100644 --- a/src/components/MnemonicPhraseInput.tsx +++ b/src/components/MnemonicPhraseInput.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react' import MnemonicWordInput from './MnemonicWordInput' interface MnemonicPhraseInputProps { @@ -15,6 +16,20 @@ export default function MnemonicPhraseInput({ isValid, onChange, }: MnemonicPhraseInputProps) { + const [activeIndex, setActiveIndex] = useState(0) + const inputRefs = useRef([]) + + useEffect(() => { + if (activeIndex < mnemonicPhrase.length && isValid && isValid(activeIndex)) { + const nextIndex = activeIndex + 1 + setActiveIndex(nextIndex) + + if (inputRefs.current[nextIndex]) { + inputRefs.current[nextIndex].focus() + } + } + }, [mnemonicPhrase, activeIndex, isValid]) + return (
{mnemonicPhrase.map((_, outerIndex) => { @@ -26,9 +41,11 @@ export default function MnemonicPhraseInput({
{wordGroup.map((givenWord, innerIndex) => { const wordIndex = outerIndex + innerIndex + const isCurrentActive = wordIndex === activeIndex return (
(inputRefs.current[wordIndex] = el)} index={wordIndex} value={givenWord} setValue={(value, i) => { @@ -37,6 +54,8 @@ export default function MnemonicPhraseInput({ }} isValid={isValid ? isValid(wordIndex) : undefined} disabled={isDisabled ? isDisabled(wordIndex) : undefined} + onFocus={() => setActiveIndex(wordIndex)} + autoFocus={isCurrentActive} />
) diff --git a/src/components/MnemonicWordInput.tsx b/src/components/MnemonicWordInput.tsx index eb61abc8b..94039cd12 100644 --- a/src/components/MnemonicWordInput.tsx +++ b/src/components/MnemonicWordInput.tsx @@ -3,19 +3,32 @@ import { useTranslation } from 'react-i18next' import styles from './MnemonicWordInput.module.css' interface MnemonicWordInputProps { + forwardRef: (el: HTMLInputElement) => void index: number value: string setValue: (value: string, index: number) => void isValid?: boolean disabled?: boolean + onFocus?: () => void + autoFocus?: boolean } -const MnemonicWordInput = ({ index, value, setValue, isValid, disabled }: MnemonicWordInputProps) => { +const MnemonicWordInput = ({ + forwardRef, + index, + value, + setValue, + isValid, + disabled, + onFocus, + autoFocus, +}: MnemonicWordInputProps) => { const { t } = useTranslation() return ( {index + 1}. 0} isValid={isValid === true} + onFocus={onFocus} + autoFocus={autoFocus} required /> From 4096c20d4f731fbf0e4fcae2dfa3e4bb4e151319 Mon Sep 17 00:00:00 2001 From: Zachary Johnson Date: Wed, 7 Feb 2024 21:28:15 -0500 Subject: [PATCH 2/5] add duplicate wallet validation on create-wallet page --- src/components/CreateWallet.test.tsx | 2 ++ src/components/WalletCreationForm.tsx | 21 +++++++++++++++++++-- src/i18n/locales/en/translation.json | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/CreateWallet.test.tsx b/src/components/CreateWallet.test.tsx index 967d68880..331440734 100644 --- a/src/components/CreateWallet.test.tsx +++ b/src/components/CreateWallet.test.tsx @@ -13,6 +13,7 @@ jest.mock('../libs/JmWalletApi', () => ({ getGetinfo: jest.fn(), getSession: jest.fn(), postWalletCreate: jest.fn(), + getWalletAll: jest.fn(), })) const NOOP = () => {} @@ -38,6 +39,7 @@ describe('', () => { const neverResolvingPromise = new Promise(() => {}) ;(apiMock.getGetinfo as jest.Mock).mockReturnValue(neverResolvingPromise) ;(apiMock.getSession as jest.Mock).mockReturnValue(neverResolvingPromise) + ;(apiMock.getWalletAll as jest.Mock).mockReturnValue(neverResolvingPromise) }) it('should display alert when rescanning is active', async () => { diff --git a/src/components/WalletCreationForm.tsx b/src/components/WalletCreationForm.tsx index db7ba6e50..bc5d62bd4 100644 --- a/src/components/WalletCreationForm.tsx +++ b/src/components/WalletCreationForm.tsx @@ -1,10 +1,12 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useState } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { Formik, FormikErrors } from 'formik' import Sprite from './Sprite' import { JM_WALLET_FILE_EXTENSION, sanitizeWalletName } from '../utils' import styles from './WalletCreationForm.module.css' +import * as Api from '../libs/JmWalletApi' +import { useServiceInfo } from '../context/ServiceInfoContext' export interface CreateWalletFormValues { walletName: string @@ -36,14 +38,29 @@ const WalletCreationForm = ({ onCancel, onSubmit, }: WalletCreationFormProps) => { + const [walletList, setWalletList] = useState(null) const { t, i18n } = useTranslation() + useEffect(() => { + const abortCtrl = new AbortController() + + Api.getWalletAll({ signal: abortCtrl.signal }) + .then((res) => (res.ok ? res.json() : Api.Helper.throwError(res, t('wallets.error_loading_failed')))) + .then((data) => { + if (abortCtrl.signal.aborted) return + setWalletList(data.wallets) + }) + }, [t]) + const validate = useCallback( (values: CreateWalletFormValues) => { const errors = {} as FormikErrors if (!values.walletName || !validateWalletName(values.walletName)) { errors.walletName = t('create_wallet.feedback_invalid_wallet_name') } + if (walletList && walletList.includes(`${values.walletName}.jmdat`)) { + errors.walletName = t('create_wallet.feedback_wallet_name_already_exists') + } if (!values.password) { errors.password = t('create_wallet.feedback_invalid_password') } @@ -52,7 +69,7 @@ const WalletCreationForm = ({ } return errors }, - [t], + [t, walletList], ) return ( diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 4965e2c48..861aa5ff0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -131,6 +131,7 @@ "label_wallet_name": "Wallet name", "placeholder_wallet_name": "Your Wallet...", "feedback_invalid_wallet_name": "Please choose a valid wallet name: Use only letters, numbers, underscores or hyphens.", + "feedback_wallet_name_already_exists": "Please choose another wallet name. This one is already in use.", "label_password": "Password to unlock the wallet", "placeholder_password": "Choose a secure password...", "feedback_invalid_password": "Please set a password.", From 9ddadf5c0077a74bb9ffea20fbee570c5edfe537 Mon Sep 17 00:00:00 2001 From: Zachary Johnson Date: Wed, 7 Feb 2024 22:32:51 -0500 Subject: [PATCH 3/5] add duplicate wallet name error message test --- src/components/CreateWallet.test.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/CreateWallet.test.tsx b/src/components/CreateWallet.test.tsx index 331440734..96f9906b5 100644 --- a/src/components/CreateWallet.test.tsx +++ b/src/components/CreateWallet.test.tsx @@ -89,6 +89,26 @@ describe('', () => { expect(await screen.findByText('create_wallet.feedback_invalid_password_confirm')).toBeVisible() }) + it('should show validation message to user if duplicate wallet name', async () => { + ;(apiMock.getWalletAll as jest.Mock).mockReturnValue( + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ wallets: [`${testWalletName}.jmdat`] }), + }), + ) + setup({}) + + expect(await screen.queryByText('create_wallet.feedback_wallet_name_already_exists')).not.toBeInTheDocument() + + await user.type(screen.getByPlaceholderText('create_wallet.placeholder_wallet_name'), testWalletName) + await user.type(screen.getByPlaceholderText('create_wallet.placeholder_password'), testWalletPassword) + await user.type(screen.getByPlaceholderText('create_wallet.placeholder_password_confirm'), testWalletPassword) + + await user.click(screen.getByText('create_wallet.button_create')) + + expect(await screen.findByText('create_wallet.feedback_wallet_name_already_exists')).toBeVisible() + }) + it('should not submit form if wallet name contains invalid characters', async () => { setup({}) From 9bc2b88bb119ff5715aa6cba42d5d25400ffddbc Mon Sep 17 00:00:00 2001 From: Zachary Johnson Date: Wed, 7 Feb 2024 22:48:54 -0500 Subject: [PATCH 4/5] fix linting issue --- src/components/WalletCreationForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/WalletCreationForm.tsx b/src/components/WalletCreationForm.tsx index bc5d62bd4..687f3d6a1 100644 --- a/src/components/WalletCreationForm.tsx +++ b/src/components/WalletCreationForm.tsx @@ -6,7 +6,6 @@ import Sprite from './Sprite' import { JM_WALLET_FILE_EXTENSION, sanitizeWalletName } from '../utils' import styles from './WalletCreationForm.module.css' import * as Api from '../libs/JmWalletApi' -import { useServiceInfo } from '../context/ServiceInfoContext' export interface CreateWalletFormValues { walletName: string From 420da64a2db222c9c925c9fed1b91d6bae87ade9 Mon Sep 17 00:00:00 2001 From: Zachary Johnson Date: Thu, 8 Feb 2024 20:05:18 -0500 Subject: [PATCH 5/5] add in PR feedback --- src/components/WalletCreationForm.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/WalletCreationForm.tsx b/src/components/WalletCreationForm.tsx index 687f3d6a1..413c50052 100644 --- a/src/components/WalletCreationForm.tsx +++ b/src/components/WalletCreationForm.tsx @@ -49,6 +49,11 @@ const WalletCreationForm = ({ if (abortCtrl.signal.aborted) return setWalletList(data.wallets) }) + .catch(() => { + // do nothing on purpose + }) + + return () => abortCtrl.abort() }, [t]) const validate = useCallback(