Skip to content

Commit

Permalink
feat: Ability to display/export the private key (#3290)
Browse files Browse the repository at this point in the history
* feat: Ability to display/export the private key

* feat: test

* fix: comment

* fix: check

* fix: test

* fix: test

* fix: check

* fix: check

* fix: comment

* fix: comment
  • Loading branch information
devchenyan authored Jan 14, 2025
1 parent 7cc4585 commit 9a77b74
Show file tree
Hide file tree
Showing 23 changed files with 445 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@
}
}

.privateKey {
background: transparent;
border: none;
cursor: pointer;
&:hover {
svg {
g,
path {
stroke: var(--primary-color);
}
}
}
}

@media screen and (max-width: 1330px) {
.container {
.balance {
Expand Down
23 changes: 22 additions & 1 deletion packages/neuron-ui/src/components/AddressBook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'
import { useState as useGlobalState, useDispatch } from 'states'
import Dialog from 'widgets/Dialog'
import CopyZone from 'widgets/CopyZone'
import { Copy } from 'widgets/Icons/icon'
import ViewPrivateKey from 'components/ViewPrivateKey'
import { Copy, PrivateKey } from 'widgets/Icons/icon'
import Table, { TableProps, SortType } from 'widgets/Table'
import { shannonToCKBFormatter, useLocalDescription } from 'utils'
import { HIDE_BALANCE } from 'utils/const'
Expand Down Expand Up @@ -44,6 +45,7 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {

const dispatch = useDispatch()
const { onChangeEditStatus, onSubmitDescription } = useLocalDescription('address', walletId, dispatch)
const [viewPrivateKeyAddress, setViewPrivateKeyAddress] = useState('')

const columns = useMemo<TableProps<State.Address>['columns']>(
() => [
Expand Down Expand Up @@ -149,6 +151,21 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
return 0
},
},
{
title: '',
dataIndex: 'key',
align: 'left',
width: '40px',
render(_, __, { address }) {
return (
<Tooltip tip={t('addresses.view-private-key')} placement="left">
<button type="button" className={styles.privateKey} onClick={() => setViewPrivateKeyAddress(address)}>
<PrivateKey />
</button>
</Tooltip>
)
},
},
],
[t]
)
Expand Down Expand Up @@ -179,6 +196,10 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
}
/>
</div>

{!!viewPrivateKeyAddress && (
<ViewPrivateKey address={viewPrivateKeyAddress} onClose={() => setViewPrivateKeyAddress('')} />
)}
</div>
</Dialog>
)
Expand Down
36 changes: 23 additions & 13 deletions packages/neuron-ui/src/components/Receive/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Button from 'widgets/Button'
import CopyZone from 'widgets/CopyZone'
import QRCode from 'widgets/QRCode'
import Tooltip from 'widgets/Tooltip'
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder } from 'widgets/Icons/icon'
import ViewPrivateKey from 'components/ViewPrivateKey'
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder, PrivateKey } from 'widgets/Icons/icon'
import VerifyHardwareAddress from './VerifyHardwareAddress'
import styles from './receive.module.scss'
import { useCopyAndDownloadQrCode, useSwitchAddress } from './hooks'
Expand All @@ -29,6 +30,7 @@ export const AddressQrCodeWithCopyZone = ({
)

const [isCopySuccess, setIsCopySuccess] = useState(false)
const [showViewPrivateKey, setShowViewPrivateKey] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout>>()
const { ref, onCopyQrCode, onDownloadQrCode, showCopySuccess } = useCopyAndDownloadQrCode()

Expand Down Expand Up @@ -70,19 +72,27 @@ export const AddressQrCodeWithCopyZone = ({
<CopyZone content={showAddress} className={styles.showAddress}>
{showAddress}
</CopyZone>
<button
type="button"
className={styles.addressToggle}
onClick={onClick}
title={transformLabel}
onFocus={stopPropagation}
onMouseOver={stopPropagation}
onMouseUp={stopPropagation}
>
<AddressTransform />
{transformLabel}
</button>
<div className={styles.actionWrap}>
<button
type="button"
className={styles.addressToggle}
onClick={onClick}
title={transformLabel}
onFocus={stopPropagation}
onMouseOver={stopPropagation}
onMouseUp={stopPropagation}
>
<AddressTransform />
{transformLabel}
</button>
<button type="button" className={styles.privateKey} onClick={() => setShowViewPrivateKey(true)}>
<PrivateKey />
{t('addresses.view-private-key')}
</button>
</div>
</div>

{showViewPrivateKey && <ViewPrivateKey address={showAddress} onClose={() => setShowViewPrivateKey(false)} />}
</div>
)
}
Expand Down
48 changes: 32 additions & 16 deletions packages/neuron-ui/src/components/Receive/receive.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,26 +125,42 @@
color: var(--main-text-color);
}

.addressToggle {
width: 100%;
.actionWrap {
margin-top: 8px;
appearance: none;
border: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-family: PingFang SC;
font-style: normal;
font-weight: 500;
color: var(--primary-color);
line-height: normal;
cursor: pointer;
gap: 32px;

svg {
pointer-events: none;
margin-right: 5px;
button {
appearance: none;
border: none;
background: none;
font-size: 12px;
font-style: normal;
font-weight: 500;
color: var(--primary-color);
line-height: normal;
cursor: pointer;
display: flex;
align-items: center;
}

.addressToggle {
svg {
pointer-events: none;
margin-right: 5px;
}
}

.privateKey {
svg {
width: 16px;
margin-right: 3px;
g,
path {
stroke: var(--primary-color);
}
}
}
}

Expand Down
143 changes: 143 additions & 0 deletions packages/neuron-ui/src/components/ViewPrivateKey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useState as useGlobalState } from 'states'
import Dialog from 'widgets/Dialog'
import TextField from 'widgets/TextField'
import Alert from 'widgets/Alert'
import { errorFormatter, useCopy, isSuccessResponse } from 'utils'
import { Attention, Copy } from 'widgets/Icons/icon'
import { getPrivateKeyByAddress } from 'services/remote'
import styles from './viewPrivateKey.module.scss'

const ViewPrivateKey = ({ onClose, address }: { onClose?: () => void; address?: string }) => {
const [t] = useTranslation()
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [privateKey, setPrivateKey] = useState('')
const [isLoading, setIsLoading] = useState(false)
const { copied, onCopy, copyTimes } = useCopy()
const {
wallet: { id: walletID = '' },
} = useGlobalState()

useEffect(() => {
setPassword('')
setError('')
}, [setError, setPassword])

const onChange = useCallback(
(e: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement
setPassword(value)
setError('')
},
[setPassword, setError]
)

const onSubmit = useCallback(
(e?: React.FormEvent) => {
if (e) {
e.preventDefault()
}
if (!password) {
return
}
setIsLoading(true)
getPrivateKeyByAddress({
walletID,
address,
password,
})
.then(res => {
if (!isSuccessResponse(res)) {
setError(errorFormatter(res.message, t))
return
}
setPrivateKey(res.result)
})
.finally(() => {
setIsLoading(false)
})
},
[walletID, password, setError, t]
)

if (privateKey) {
return (
<Dialog
show
title={t('addresses.view-private-key')}
onConfirm={onClose}
onCancel={onClose}
showCancel={false}
confirmText={t('common.close')}
className={styles.dialog}
>
<div>
<div className={styles.tip}>
<Attention />
{t('addresses.view-private-key-tip')}
</div>

<TextField
className={styles.passwordInput}
placeholder={t('password-request.placeholder')}
width="100%"
label={<span className={styles.label}>{t('addresses.private-key')}</span>}
value={privateKey}
field="password"
type="password"
disabled
suffix={
<div className={styles.copy}>
<Copy onClick={() => onCopy(privateKey)} />
</div>
}
/>

{copied ? (
<Alert status="success" className={styles.notice} key={copyTimes.toString()}>
{t('common.copied')}
</Alert>
) : null}
</div>
</Dialog>
)
}
return (
<Dialog
show
title={t('addresses.view-private-key')}
onCancel={onClose}
onConfirm={onSubmit}
confirmText={t('wizard.next')}
isLoading={isLoading}
disabled={!password || isLoading}
className={styles.dialog}
>
<div>
<div className={styles.tip}>
<Attention />
{t('addresses.view-private-key-tip')}
</div>

<TextField
className={styles.passwordInput}
placeholder={t('password-request.placeholder')}
width="100%"
label={t('wizard.password')}
value={password}
field="password"
type="password"
onChange={onChange}
autoFocus
error={error}
/>
</div>
</Dialog>
)
}

ViewPrivateKey.displayName = 'ViewPrivateKey'

export default ViewPrivateKey
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@import '../../styles/mixin.scss';

.passwordInput {
margin-top: 16px;
}

.dialog {
width: 700px;
}

.tip {
color: var(--warn-text-color);
background: var(--warn-background-color);
margin: -20px -16px 0;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
font-size: 12px;
gap: 4px;
font-weight: 500;
border-bottom: 1px solid var(--warn-border-color);
}

.label {
font-weight: 500;
color: var(--main-text-color);
font-size: 14px;
}

.copy {
display: flex;
align-items: center;
margin-left: 6px;
}

.notice {
@include dialog-copy-animation;
}
Loading

2 comments on commit 9a77b74

@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 12760377514

@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 12760375971

Please sign in to comment.