Skip to content

Commit

Permalink
feat: Ability to display/export the private key
Browse files Browse the repository at this point in the history
  • Loading branch information
devchenyan committed Dec 28, 2024
1 parent 0d64329 commit 5e20cd8
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 40 deletions.
12 changes: 12 additions & 0 deletions packages/neuron-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,17 @@
},
"resolutions": {
"react-i18next": "14.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
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
49 changes: 33 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,43 @@
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-family: PingFang SC;
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
145 changes: 145 additions & 0 deletions packages/neuron-ui/src/components/ViewPrivateKey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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(
async (e?: React.FormEvent) => {
if (e) {
e.preventDefault()
}
if (!password) {
return
}
setIsLoading(true)
try {
const res = await getPrivateKeyByAddress({
walletID,
address,
password,
})

setIsLoading(false)

if (!isSuccessResponse(res)) {
setError(errorFormatter(res.message, t))
return
}
setPrivateKey(res.result)
} catch (err) {
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

0 comments on commit 5e20cd8

Please sign in to comment.