Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Same wallet import optimization #3001

Merged
merged 21 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.content {
width: 648px;
.detail {
margin: 0;
color: var(--secondary-text-color);
}
.groupWrap {
margin-top: 16px;
height: 177px;
overflow-y: scroll;
border: 1px solid var(--divide-line-color);
border-radius: 8px;
padding-left: 16px;
> div {
border-bottom: 1px solid var(--divide-line-color);
padding-top: 16px;
&:last-child {
border: none;
}
}
.radioItem {
padding: 0 0 16px;
&:hover,
&:focus {
background: transparent;
button {
visibility: visible;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useMemo, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Dialog from 'widgets/Dialog'
import RadioGroup from 'widgets/RadioGroup'
import { useState as useGlobalState, useDispatch, AppActions } from 'states'
import { requestPassword } from 'services/remote'
import styles from './detectDuplicateWalletDialog.module.scss'

const DetectDuplicateWalletDialog = ({ onClose }: { onClose: () => void }) => {
const {
wallet: { id: currentID = '' },
settings: { wallets = [] },
} = useGlobalState()
const dispatch = useDispatch()
const [duplicatedWallets, setDuplicatedWallets] = useState<string[]>([])
const [t] = useTranslation()

const groups = useMemo(() => {
const obj: {
[key: string]: State.WalletIdentity[]
} = {}
wallets.forEach(item => {
if (item.extendedKey in obj) {
obj[item.extendedKey].push(item)
} else {
obj[item.extendedKey] = [item]
}
})

return Object.values(obj).filter(list => list.length > 1)
}, [wallets])

const handleGroupChange = useCallback(
(checked: string) => {
const [extendedKey, id] = checked.split('_')

const list = wallets
.filter(item => {
if (item.extendedKey === extendedKey) {
return item.id !== id
}
return duplicatedWallets.includes(item.id)
})
.map(item => item.id)

setDuplicatedWallets(list)
},
[wallets, duplicatedWallets, setDuplicatedWallets]
)

const onConfirm = useCallback(async () => {
const getRequest = (id: string) => {
if (wallets.find(item => (item.id === id && item.device) || item.isWatchOnly)) {
return requestPassword({ walletID: id, action: 'delete-wallet' })
}
return new Promise(resolve => {
dispatch({
type: AppActions.RequestPassword,
payload: {
walletID: id,
actionType: 'delete',
onSuccess: async () => {
await resolve(id)
},
},
})
})
}

const requestToDeleteIds = duplicatedWallets
.filter(item => item !== currentID)
.concat(duplicatedWallets.includes(currentID) ? [currentID] : [])

// eslint-disable-next-line no-restricted-syntax
for (const id of requestToDeleteIds) {
// eslint-disable-next-line no-await-in-loop
await getRequest(id)
}
onClose()
}, [wallets, duplicatedWallets, requestPassword, onClose, dispatch])

return (
<Dialog
show
title={t('settings.wallet-manager.detected-duplicate.title')}
onCancel={onClose}
onConfirm={onConfirm}
disabled={duplicatedWallets.length === 0}
>
<div className={styles.content}>
<p className={styles.detail}>{t('settings.wallet-manager.detected-duplicate.detail')}</p>
<div className={styles.groupWrap}>
{groups.map(group => (
<RadioGroup
inputIdPrefix="detect-duplicate-wallet"
key={group[0].extendedKey}
defaultValue=""
onChange={handleGroupChange}
itemClassName={styles.radioItem}
options={group.map(wallet => ({
value: `${wallet.extendedKey}_${wallet.id}`,
label: <span className={styles.walletName}>{wallet.name}</span>,
}))}
/>
))}
</div>
</div>
</Dialog>
)
}

DetectDuplicateWalletDialog.displayName = 'DetectDuplicateWalletDialog'

export default DetectDuplicateWalletDialog
67 changes: 39 additions & 28 deletions packages/neuron-ui/src/components/ImportHardware/name-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useTranslation } from 'react-i18next'
import Button from 'widgets/Button'
import TextField from 'widgets/TextField'
import { createHardwareWallet } from 'services/remote'
import { CONSTANTS, isSuccessResponse, useDialogWrapper } from 'utils'
import { CONSTANTS, isSuccessResponse, useDialogWrapper, ErrorCode } from 'utils'
import Alert from 'widgets/Alert'
import { FinishCreateLoading, getAlertStatus } from 'components/WalletWizard'
import { importedWalletDialogShown } from 'services/localCache'
import ReplaceDuplicateWalletDialog, { useReplaceDuplicateWallet } from 'components/ReplaceDuplicateWalletDialog'
import { ImportStep, ActionType, ImportHardwareState } from './common'

import styles from './findDevice.module.scss'
Expand All @@ -26,6 +27,7 @@ const NameWallet = ({
const [walletName, setWalletName] = useState(`${model?.manufacturer} ${model?.product}`)
const [errorMsg, setErrorMsg] = useState('')
const { dialogRef, openDialog, closeDialog } = useDialogWrapper()
const { onImportingExitingWalletError, dialogProps } = useReplaceDuplicateWallet()

const onBack = useCallback(() => {
dispatch({ step: ImportStep.ImportHardware })
Expand All @@ -46,6 +48,11 @@ const NameWallet = ({
importedWalletDialogShown.setStatus(res.result.id, true)
}
} else {
if (res.status === ErrorCode.DuplicateImportWallet) {
onImportingExitingWalletError(res.message)
return
}

setErrorMsg(typeof res.message === 'string' ? res.message : res.message!.content!)
}
})
Expand All @@ -62,33 +69,37 @@ const NameWallet = ({
}, [])

return (
<form className={styles.container}>
<header className={styles.title}>{t('import-hardware.title.name-wallet')}</header>
<section className={styles.name}>
<TextField
required
autoFocus
placeholder={t('wizard.set-wallet-name')}
onChange={onInput}
field="wallet-name"
value={walletName}
maxLength={MAX_WALLET_NAME_LENGTH}
/>
</section>
<Alert status={getAlertStatus(!!walletName, !errorMsg)} className={styles.alert}>
<span>{errorMsg || t('wizard.new-name')}</span>
</Alert>
<footer className={styles.footer}>
<Button
type="submit"
label={t('import-hardware.actions.finish')}
onClick={onNext}
disabled={!walletName || !!errorMsg}
/>
<Button type="text" label={t('import-hardware.actions.back')} onClick={onBack} />
</footer>
<FinishCreateLoading dialogRef={dialogRef} />
</form>
<>
<form className={styles.container}>
<header className={styles.title}>{t('import-hardware.title.name-wallet')}</header>
<section className={styles.name}>
<TextField
required
autoFocus
placeholder={t('wizard.set-wallet-name')}
onChange={onInput}
field="wallet-name"
value={walletName}
maxLength={MAX_WALLET_NAME_LENGTH}
/>
</section>
<Alert status={getAlertStatus(!!walletName, !errorMsg)} className={styles.alert}>
<span>{errorMsg || t('wizard.new-name')}</span>
</Alert>
<footer className={styles.footer}>
<Button
type="submit"
label={t('import-hardware.actions.finish')}
onClick={onNext}
disabled={!walletName || !!errorMsg}
/>
<Button type="text" label={t('import-hardware.actions.back')} onClick={onBack} />
</footer>
<FinishCreateLoading dialogRef={dialogRef} />
</form>

<ReplaceDuplicateWalletDialog {...dialogProps} />
</>
)
}

Expand Down
101 changes: 56 additions & 45 deletions packages/neuron-ui/src/components/ImportKeystore/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
useDialogWrapper,
} from 'utils'

import ReplaceDuplicateWalletDialog, { useReplaceDuplicateWallet } from 'components/ReplaceDuplicateWalletDialog'
import { FinishCreateLoading, CreateFirstWalletNav } from 'components/WalletWizard'
import TextField from 'widgets/TextField'
import { importedWalletDialogShown } from 'services/localCache'
Expand Down Expand Up @@ -48,6 +49,7 @@ const ImportKeystore = () => {
const navigate = useNavigate()
const [fields, setFields] = useState(defaultFields)
const [openingFile, setOpeningFile] = useState(false)
const { onImportingExitingWalletError, dialogProps } = useReplaceDuplicateWallet()
const goBack = useGoBack()

const disabled = !!(
Expand Down Expand Up @@ -115,6 +117,11 @@ const ImportKeystore = () => {
throw new PasswordIncorrectException()
}

if (res.status === ErrorCode.DuplicateImportWallet) {
onImportingExitingWalletError(res.message)
return
}

if (res.message) {
const msg = typeof res.message === 'string' ? res.message : res.message.content || ''
if (msg) {
Expand Down Expand Up @@ -193,51 +200,55 @@ const ImportKeystore = () => {
)

return (
<form className={styles.container} onSubmit={handleSubmit}>
<div className={styles.title}>{t('import-keystore.title')}</div>
<CreateFirstWalletNav />
{Object.entries(fields)
.filter(([key]) => !key.endsWith('Error'))
.map(([key, value]) => {
return (
<TextField
className={styles.field}
key={key}
field={key}
onClick={key === 'path' ? handleFileClick : undefined}
placeholder={t(`import-keystore.placeholder.${key}`)}
type={key === 'password' ? 'password' : 'text'}
readOnly={key === 'path'}
disabled={key === 'path' && openingFile}
value={value}
error={fields[`${key}Error` as keyof KeystoreFields]}
errorWithIcon
onChange={handleChange}
suffix={
key === 'path' ? (
<span
onClick={handleFileClick}
className={styles.chooseFileSuffix}
onKeyDown={() => {}}
role="button"
tabIndex={-1}
>
{t('import-keystore.select-file')}
</span>
) : null
}
required
/>
)
})}
<div className={styles.actions}>
<Button type="submit" label={t('import-keystore.button.submit')} disabled={disabled}>
{t('import-keystore.button.submit') as string}
</Button>
<Button type="text" onClick={goBack} label={t('import-keystore.button.back')} />
</div>
<FinishCreateLoading dialogRef={dialogRef} />
</form>
<>
<form className={styles.container} onSubmit={handleSubmit}>
<div className={styles.title}>{t('import-keystore.title')}</div>
<CreateFirstWalletNav />
{Object.entries(fields)
.filter(([key]) => !key.endsWith('Error'))
.map(([key, value]) => {
return (
<TextField
className={styles.field}
key={key}
field={key}
onClick={key === 'path' ? handleFileClick : undefined}
placeholder={t(`import-keystore.placeholder.${key}`)}
type={key === 'password' ? 'password' : 'text'}
readOnly={key === 'path'}
disabled={key === 'path' && openingFile}
value={value}
error={fields[`${key}Error` as keyof KeystoreFields]}
errorWithIcon
onChange={handleChange}
suffix={
key === 'path' ? (
<span
onClick={handleFileClick}
className={styles.chooseFileSuffix}
onKeyDown={() => {}}
role="button"
tabIndex={-1}
>
{t('import-keystore.select-file')}
</span>
) : null
}
required
/>
)
})}
<div className={styles.actions}>
<Button type="submit" label={t('import-keystore.button.submit')} disabled={disabled}>
{t('import-keystore.button.submit') as string}
</Button>
<Button type="text" onClick={goBack} label={t('import-keystore.button.back')} />
</div>
<FinishCreateLoading dialogRef={dialogRef} />
</form>

<ReplaceDuplicateWalletDialog {...dialogProps} />
</>
)
}

Expand Down
4 changes: 3 additions & 1 deletion packages/neuron-ui/src/components/PasswordRequest/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ export default ({
}
case 'delete': {
await deleteWallet({ id: walletID, password })(dispatch).then(status => {
if (status === ErrorCode.PasswordIncorrect) {
if (isSuccessResponse({ status })) {
onSuccess?.()
} else if (status === ErrorCode.PasswordIncorrect) {
throw new PasswordIncorrectException()
}
})
Expand Down
Loading
Loading