Skip to content

Commit

Permalink
feat: Same wallet import optimization (#3001)
Browse files Browse the repository at this point in the history
* fix: issue 291

* fix

* fix

* fix: remove unused files

* fix: test

* fix: Same wallet import optimization

* fix

* fix: comments

* fix: comments

* fix: comments

* fix: comments

* fix: Merge

* fix

---------

Co-authored-by: Chen Yu <[email protected]>
  • Loading branch information
devchenyan and Keith-CY authored Jan 29, 2024
1 parent 2809428 commit 21a2f35
Show file tree
Hide file tree
Showing 31 changed files with 693 additions and 150 deletions.
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

2 comments on commit 21a2f35

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging for test is done in 7695895920

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging for test is done in 7695894495

Please sign in to comment.