diff --git a/.ckb-version b/.ckb-version index 5affb131c0..420000f959 100644 --- a/.ckb-version +++ b/.ckb-version @@ -1 +1 @@ -v0.31.1 +v0.32.0 diff --git a/.gitignore b/.gitignore index 6707801586..4cf618898a 100755 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ node_modules # testing /coverage *.snap -/packages/neuron-wallet/tests-e2e/errors # production build diff --git a/CHANGELOG.md b/CHANGELOG.md index 7115913602..516c0f37ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +# 0.31.0-rc1 (2020-05-29) + +### Bundled CKB node + +[CKB v0.32.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.32.0) was released on May 22nd, 2020. This version of CKB node is now bundled and preconfigured in Neuron. + +### New features + +We added several new features with this version: + +* New style of logo. +* Enable exporting transaction history. +* Make the `Settings` window to be independent with new style. +* Add Language Switching function in `Settings` - `General` page. +* Export more bundled CKB logs for debug info. +* Include the bundled CKB version on the `About Neuron` window. +* Replace the `Expected speed` drop-down box with `Quick Pick Price` in `Advanced fee settings`. + +### User Experience + +We also make some improvements for better user expeierence: + +* Optimize the `Copy` component, also remove unnecessary normal context menu (right-click menu). +* Display password error in password dialog. + + # 0.30.0 (2020-05-15) ### Bundled CKB node diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b0068392d9..9f6f955304 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,7 +4,6 @@ trigger: include: - master - develop - - e2e-tests - rc/* tags: include: @@ -90,27 +89,6 @@ stages: yarn test name: Test -- stage: e2e_tests - displayName: Integration Tests - dependsOn: [] - condition: false # eq(variables['build.sourceBranch'], 'refs/heads/master') - jobs: - - job: Integration - displayName: Integration Tests - pool: - vmImage: 'macos-10.14' - steps: - - task: NodeTool@0 - inputs: - versionSpec: 12.x - displayName: 'Install Node.js' - - script: | - yarn global add lerna - yarn bootstrap - name: Bootstrap - - script: yarn test:e2e - name: Test - - stage: release displayName: Release Binaries condition: eq(variables['build.sourceBranch'], 'refs/heads/master') diff --git a/lerna.json b/lerna.json index a5da955a23..8e80e9f6bf 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.30.0", + "version": "0.31.0-rc1", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index a68f4d29ad..126a4ab3b2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.30.0", + "version": "0.31.0-rc1", "private": true, "author": { "name": "Nervos Core Dev", @@ -31,7 +31,6 @@ "build": "lerna run --stream build", "release": "yarn build && ./scripts/copy-ui-files.sh && ./scripts/release.sh", "test": "lerna run --parallel test", - "test:e2e": "yarn build && ./scripts/copy-ui-files.sh && lerna run --parallel test:e2e", "lint": "lerna run --stream lint", "postinstall": "lerna run rebuild:nativemodules", "db:chain": "node ./node_modules/.bin/typeorm" diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index 7a1cd4c3ad..f727b2df4e 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.30.0", + "version": "0.31.0-rc1", "private": true, "author": { "name": "Nervos Core Dev", @@ -58,7 +58,6 @@ "react-dom": "16.12.0", "react-i18next": "11.2.5", "react-router-dom": "5.1.2", - "react-scripts": "3.3.1", "styled-components": "5.0.0-beta.0" }, "devDependencies": { @@ -93,6 +92,7 @@ "node-sass": "4.13.0", "prettier": "1.19.1", "react-app-rewired": "2.1.5", + "react-scripts": "3.4.1", "react-test-renderer": "16.12.0", "rimraf": "3.0.0", "storybook-react-router": "1.0.8" diff --git a/packages/neuron-ui/public/favicon.ico b/packages/neuron-ui/public/favicon.ico index 9bbd9ca99c..9b60cd0b22 100644 Binary files a/packages/neuron-ui/public/favicon.ico and b/packages/neuron-ui/public/favicon.ico differ diff --git a/packages/neuron-ui/public/icon.png b/packages/neuron-ui/public/icon.png index e132bc4987..389fe2538b 100644 Binary files a/packages/neuron-ui/public/icon.png and b/packages/neuron-ui/public/icon.png differ diff --git a/packages/neuron-ui/src/components/Addresses/addresses.module.scss b/packages/neuron-ui/src/components/Addresses/addresses.module.scss index 01cc5354e4..a19b6cb458 100644 --- a/packages/neuron-ui/src/components/Addresses/addresses.module.scss +++ b/packages/neuron-ui/src/components/Addresses/addresses.module.scss @@ -14,14 +14,14 @@ $change-color: #6666cc; border-collapse: collapse; tbody { - input+span { + input + span { display: none !important; } tr:hover { background-color: #f5f5f5; - input+span { + input + span { display: flex !important; } } @@ -36,12 +36,10 @@ $change-color: #6666cc; border-bottom: 1px solid #b3b3b3; } - th { - @include SemiBoldText; + @include semi-bold-text; font-size: 1rem; font-weight: 600; - letter-spacing: 0.6px; color: #000; padding: 18px 0; @@ -52,7 +50,6 @@ $change-color: #6666cc; td { font-size: 0.875rem; - letter-spacing: 0.5px; color: #000; padding: 10px 0; @@ -69,7 +66,6 @@ $change-color: #6666cc; } } - .type { width: 100px; word-wrap: none; @@ -84,6 +80,7 @@ $change-color: #6666cc; } .address { + line-height: 1.625rem; &:hover { div::after { display: block; @@ -99,7 +96,7 @@ $change-color: #6666cc; .addressOverflow { word-break: break-all; text-align: right; - height: 1rem; + height: 1.625rem; overflow: hidden; text-align: right; } @@ -109,22 +106,21 @@ $change-color: #6666cc; } @media screen and (max-width: 1365px) { - .ellipsis { display: inline; } } @media screen and (max-width: 1680px) { - width: 30vw; + width: 20vw; &::after { position: absolute; top: 150%; left: 50%; + z-index: 1; content: attr(data-address); font-size: 0.875rem; - letter-spacing: 0.5px; color: #000; box-shadow: 1px 2px 6px 0 rgba(97, 97, 97, 0.5); background: #fff; @@ -134,14 +130,9 @@ $change-color: #6666cc; } } - @media screen and (max-width: 1350px) { - width: 20vw; - } - @media screen and (max-width: 1000px) { width: 15vw; } - } } @@ -167,13 +158,13 @@ $change-color: #6666cc; } .descriptionField { - @include descriptionField; + @include description-field; input { font-size: 0.875rem; } - &>div { + & > div { border: none; } @@ -185,11 +176,16 @@ $change-color: #6666cc; } .balance { + div { + display: flex; + align-items: center; + height: 1.625rem; + } span { display: inline-block; min-width: 200px; - @media screen and (max-width:1160px) { + @media screen and (max-width: 1160px) { min-width: auto; width: 8vw; } @@ -199,5 +195,4 @@ $change-color: #6666cc; .txCount { min-width: 100px; } - } diff --git a/packages/neuron-ui/src/components/Addresses/index.tsx b/packages/neuron-ui/src/components/Addresses/index.tsx index 1e86dce40d..3df920d283 100644 --- a/packages/neuron-ui/src/components/Addresses/index.tsx +++ b/packages/neuron-ui/src/components/Addresses/index.tsx @@ -3,16 +3,21 @@ import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Edit } from 'grommet-icons' import TextField from 'widgets/TextField' +import CopyZone from 'widgets/CopyZone' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' +import { useState as useGlobalState, useDispatch } from 'states' import { openExternal, openContextMenu } from 'services/remote' -import { useLocalDescription } from 'utils/hooks' -import { localNumberFormatter, shannonToCKBFormatter } from 'utils/formatters' -import { Routes } from 'utils/const' -import { backToTop } from 'utils/animations' -import getExplorerUrl from 'utils/getExplorerUrl' -import isMainnetUtil from 'utils/isMainnet' +import { + useLocalDescription, + backToTop, + RoutePath, + localNumberFormatter, + shannonToCKBFormatter, + getExplorerUrl, + isMainnet as isMainnetUtil, +} from 'utils' + import styles from './addresses.module.scss' const Addresses = () => { @@ -54,7 +59,7 @@ const Addresses = () => { { label: t('addresses.request-payment'), click: () => { - history.push(`${Routes.Receive}/${item.address}`) + history.push(`${RoutePath.Receive}/${item.address}`) }, }, { @@ -85,16 +90,20 @@ const Addresses = () => { {addresses.map(addr => { const isSelected = localDescription.key === addr.address const typeLabel = addr.type === 0 ? t('addresses.receiving-address') : t('addresses.change-address') + const balance = `${shannonToCKBFormatter(addr.balance)} CKB` + return ( {typeLabel} - +
- {addr.address.slice(0, -6)} - ... - {addr.address.slice(-6)} + + {addr.address.slice(0, -6)} + ... + {addr.address.slice(-6)} +
@@ -127,8 +136,10 @@ const Addresses = () => { } /> - - {`${shannonToCKBFormatter(addr.balance)} CKB`} + + + {balance} + {localNumberFormatter(addr.txCount)} diff --git a/packages/neuron-ui/src/components/BalanceSyncingIcon/balanceSyncIcon.module.scss b/packages/neuron-ui/src/components/BalanceSyncingIcon/balanceSyncIcon.module.scss index 5d4fab27f7..a1563ce800 100644 --- a/packages/neuron-ui/src/components/BalanceSyncingIcon/balanceSyncIcon.module.scss +++ b/packages/neuron-ui/src/components/BalanceSyncingIcon/balanceSyncIcon.module.scss @@ -5,7 +5,6 @@ $arrow: 10px; position: relative; font-size: 0.75rem; color: var(--nervos-green); - user-select: none; border-radius: 2px; z-index: 1; @@ -13,9 +12,10 @@ $arrow: 10px; width: 1rem; height: 0.75rem; pointer-events: none; + position: relative; + top: 1px; } - &::after { display: none; content: attr(data-content); @@ -47,7 +47,6 @@ $arrow: 10px; } &:hover { - &::after, &::before { display: block; diff --git a/packages/neuron-ui/src/components/BalanceSyncingIcon/index.tsx b/packages/neuron-ui/src/components/BalanceSyncingIcon/index.tsx index 45b64a5aa6..be761394e2 100644 --- a/packages/neuron-ui/src/components/BalanceSyncingIcon/index.tsx +++ b/packages/neuron-ui/src/components/BalanceSyncingIcon/index.tsx @@ -1,6 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { ConnectionStatus, SyncStatus } from 'utils/const' +import { ConnectionStatus, SyncStatus } from 'utils' import { ReactComponent as BalanceSyncing } from 'widgets/Icons/BalanceSyncing.svg' import { ReactComponent as BalanceSyncFailed } from 'widgets/Icons/BalanceSyncFailed.svg' import styles from './balanceSyncIcon.module.scss' diff --git a/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx b/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx index e98f561627..1ca7463ea8 100644 --- a/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx +++ b/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx @@ -1,11 +1,10 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import getCompensationPeriod from 'utils/getCompensationPeriod' -import getCompensatedTime from 'utils/getCompensatedTime' -import { WITHDRAW_EPOCHS, CompensationPeriod, IMMATURE_EPOCHS } from 'utils/const' -import { uniformTimeFormatter } from 'utils/formatters' +import { getCompensatedTime, getCompensationPeriod, CONSTANTS, CompensationPeriod, uniformTimeFormatter } from 'utils' import styles from './compensationPeriodTooltip.module.scss' +const { WITHDRAW_EPOCHS, IMMATURE_EPOCHS } = CONSTANTS + const HOUR = 3_600_000 const HOURS_PER_EPOCH = 4 * HOUR const SECS_PER_DAY = 24 * HOUR diff --git a/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss b/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss index 38399b1454..1e4efded0d 100644 --- a/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss +++ b/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss @@ -1,7 +1,7 @@ -@import "../../styles/mixin.scss"; +@import '../../styles/mixin.scss'; .dialog { - @include dialogContainer; + @include dialog-container; padding: 30px 50px; &::backdrop { @@ -21,7 +21,7 @@ line-height: 1.4; margin-bottom: 25px; - &>span:first-of-type { + & > span:first-of-type { font-weight: 900; &:after { @@ -32,7 +32,6 @@ } } - .errorMessage { display: flex; align-items: center; @@ -70,7 +69,7 @@ } .footer { - @include dialogFooter; + @include dialog-footer; button:last-of-type { margin-left: 9px; diff --git a/packages/neuron-ui/src/components/DepositDialog/index.tsx b/packages/neuron-ui/src/components/DepositDialog/index.tsx index 08f94e0869..5fa35265b2 100644 --- a/packages/neuron-ui/src/components/DepositDialog/index.tsx +++ b/packages/neuron-ui/src/components/DepositDialog/index.tsx @@ -6,11 +6,14 @@ import Spinner from 'widgets/Spinner' import Button from 'widgets/Button' import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg' import { openExternal } from 'services/remote' -import { SHANNON_CKB_RATIO, NERVOS_DAO_RFC_URL } from 'utils/const' -import { localNumberFormatter, shannonToCKBFormatter } from 'utils/formatters' -import { useDialog } from 'utils/hooks' +import { CONSTANTS, localNumberFormatter, shannonToCKBFormatter, useDialog } from 'utils' import styles from './depositDialog.module.scss' +const { SHANNON_CKB_RATIO } = CONSTANTS + +const NERVOS_DAO_RFC_URL = + 'https://www.github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md' + interface DepositDialogProps { show: boolean value: any diff --git a/packages/neuron-ui/src/components/GeneralSetting/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/index.tsx index 6580ee9354..2ed2c78ad7 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/index.tsx @@ -1,22 +1,25 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { Stack, Text, ProgressIndicator } from 'office-ui-fabric-react' +import { ProgressIndicator } from 'office-ui-fabric-react' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' -import { StateDispatch } from 'states/stateProvider/reducer' -import { addPopup } from 'states/stateProvider/actionCreators' -import { checkForUpdates, downloadUpdate, installUpdate, clearCellCache } from 'services/remote' +import Dropdown from 'widgets/Dropdown' +import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg' +import { StateDispatch, addPopup } from 'states' +import { checkForUpdates, downloadUpdate, installUpdate, clearCellCache, setLocale, getVersion } from 'services/remote' +import { cacheClearDate } from 'services/localCache' +import { CONSTANTS } from 'utils' + import styles from './style.module.scss' -const UpdateDownloadStatus = ({ - progress = 0, - newVersion = '', - releaseNotes = '', -}: { +const { LOCALES } = CONSTANTS +interface UpdateDowloadStatusProps { progress: number newVersion: string releaseNotes: string -}) => { +} + +const UpdateDownloadStatus = ({ progress = 0, newVersion = '', releaseNotes = '' }: UpdateDowloadStatusProps) => { const [t] = useTranslation() const available = newVersion !== '' && progress < 0 const downloaded = progress >= 1 @@ -33,24 +36,15 @@ const UpdateDownloadStatus = ({ /* eslint-disable react/no-danger */ return ( - - - {t('updates.updates-found-do-you-want-to-update', { version: newVersion })} - -

{t('updates.release-notes')}

+
+
{t('updates.updates-found-do-you-want-to-update', { version: newVersion })}
+
+
- -
) } @@ -60,35 +54,33 @@ const UpdateDownloadStatus = ({ } return ( - - - {t('updates.updates-downloaded-about-to-quit-and-install')} - - +
+
{t('updates.updates-downloaded-about-to-quit-and-install')}
+
+
) } - return ( - - ) + return } -const GeneralSetting = ({ updater, dispatch }: { updater: State.AppUpdater; dispatch: StateDispatch }) => { - const [t] = useTranslation() +interface GeneralSettingProps { + updater: State.AppUpdater + dispatch: StateDispatch +} + +const GeneralSetting = ({ updater, dispatch }: GeneralSettingProps) => { + const [t, i18n] = useTranslation() const [clearingCache, setClearingCache] = useState(false) + const [lng, setLng] = useState(i18n.language) + const [clearedDate, setClearedDate] = useState(cacheClearDate.load()) const checkUpdates = useCallback(() => { checkForUpdates() @@ -99,22 +91,31 @@ const GeneralSetting = ({ updater, dispatch }: { updater: State.AppUpdater; disp setTimeout(() => { clearCellCache().finally(() => { addPopup('clear-cache-successfully')(dispatch) + const date = new Date().toISOString().slice(0, 10) + cacheClearDate.save(date) + setClearedDate(date) setClearingCache(false) }) }, 100) - }, [dispatch]) + }, [dispatch, setClearedDate]) + + const onApplyLanguage = useCallback(() => { + setLocale(lng as typeof LOCALES[number]) + }, [lng]) + + const version = useMemo(() => { + return getVersion() + }, []) + + const showNewVersion = updater.version !== '' || updater.downloadProgress >= 0 return ( - - - - {updater.version !== '' || updater.downloadProgress >= 0 ? ( - - ) : ( +
+
{t('settings.general.version')}
+ {showNewVersion ? null : ( + <> +
{version}
+
+ + )} +
+ {showNewVersion ? ( + + ) : null} +
- - +
+ {clearedDate ? ( +
{t('settings.general.cache-cleared-on', { date: clearedDate })}
+ ) : null} +
+ {t('settings.general.clear-cache-description')} - - - - - - +
+
+
+ +
+
{t('settings.general.language')}
+
+ ({ key: locale, text: t(`settings.locale.${locale}`) }))} + selectedKey={lng} + onChange={(_, item) => { + if (item) { + setLng(item.key as typeof LOCALES[number]) + } + }} + /> +
+
+
+
) } diff --git a/packages/neuron-ui/src/components/GeneralSetting/style.module.scss b/packages/neuron-ui/src/components/GeneralSetting/style.module.scss index 328cb7b86d..9008153480 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/style.module.scss +++ b/packages/neuron-ui/src/components/GeneralSetting/style.module.scss @@ -1,27 +1,124 @@ +@import '../../styles/mixin.scss'; + +$action-button-width: 11.25rem; .container { + font-size: 0.875rem; + margin-top: 1.25rem; button { - width: 9.375rem; + min-width: auto; + width: $action-button-width; + box-sizing: border-box; + height: 1.625rem; + padding: 0; + } + + display: grid; + grid-template: + 'version-label version-value version-action' auto + 'language-label language-select language-action' auto + 'clear-cache-detail clear-cache-detail clear-cache-action' auto/ + 120px 1fr $action-button-width; + grid-gap: 30px 10px; +} +.label { + @include regular-text; + font-weight: 600; + align-self: center; + &:after { + content: ':'; } } -.releaseNotesStyle { - overflow: scroll; - height: 200px; - margin-bottom: 20px; - padding: 10px 15px 15px 15px; - border: solid 1px #ccc; +.version { + &.label { + grid-area: version-label; + } + + &.value { + align-self: center; + grid-area: version-value; + } + + &.action { + grid-area: version-action; + } +} - ul { - list-style-type: disc; - padding-left: 30px; +.newVersion { + grid-area: 1/2/2/4; +} - li { - margin: 5px 0; +.language { + &.label { + grid-area: language-label; + } + &.select { + grid-area: language-select; + max-width: 300px; + & > div { + margin: 0; } } - a { - text-decoration: none; - pointer-events: none; + &.action { + grid-area: language-action; + } +} +.clearCache { + &.detail { + grid-area: clear-cache-detail; + .date { + display: flex; + align-items: center; + font-size: 0.875rem; + height: 1.125rem; + margin-bottom: 5px; + } + .desc { + display: flex; + font-size: 0.6875rem; + color: #666; + } + svg { + height: 0.6875rem; + width: 0.6875rem; + filter: grayscale(1) opacity(0.6); + margin: 2px 5px 0 0; + } + } + &.action { + grid-area: clear-cache-action; + } +} + +.install { + display: grid; + grid-template: + 'note action' auto + 'release-note release-note'/ + 1fr $action-button-width; + + .releaseNotesStyle { + grid-area: release-note; + overflow: scroll; + min-height: 200px; + height: calc(100vh - 400px); + margin: 6px auto 20px; + padding: 10px 15px 15px 15px; + border: solid 1px #ccc; + + ul { + list-style-type: disc; + padding-left: 30px; + + li { + margin: 5px 0; + } + } + + a { + text-decoration: none; + pointer-events: none; + } } } diff --git a/packages/neuron-ui/src/components/History/history.module.scss b/packages/neuron-ui/src/components/History/history.module.scss index 448f1d986a..27e649dedb 100644 --- a/packages/neuron-ui/src/components/History/history.module.scss +++ b/packages/neuron-ui/src/components/History/history.module.scss @@ -1,8 +1,53 @@ @import '../../styles/mixin.scss'; .history { - // need more discussion - // min-height: calc(100vh - 100px); + .tools { + display: grid; + grid-template: + 'search-input search-button export-button' auto/ + 1fr auto auto; + margin-bottom: 15px; + .searchBox { + grid-area: search-input; + button:hover { + background: rgb(220, 220, 220); + } + } + + .searchBtn, + .exportBtn { + height: 1.625rem; + min-width: unset; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + } + + .searchBtn { + grid-area: search-button; + padding: 0 15px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + background-color: rgb(204, 204, 204); + color: rgb(97, 97, 97); + border: none; + } + + .exportBtn { + grid-area: export-button; + width: 1.625rem; + margin-left: 10px; + svg { + width: 16px; + height: 16px; + path { + fill: #fff; + } + } + } + } .listContainer { background-color: #fff; @@ -14,11 +59,11 @@ display: flex; flex: 1; flex-direction: column; - justify-content: flex-end + justify-content: flex-end; } } .noTxs { - @include MediumText; + @include medium-text; padding: 20px 13px; } diff --git a/packages/neuron-ui/src/components/History/hooks.ts b/packages/neuron-ui/src/components/History/hooks.ts index 2c0012cb03..3e4e9c58f1 100644 --- a/packages/neuron-ui/src/components/History/hooks.ts +++ b/packages/neuron-ui/src/components/History/hooks.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react' import { updateTransactionList } from 'states/stateProvider/actionCreators/transactions' -import { queryParsers } from 'utils/parsers' -import { backToTop } from 'utils/animations' +import { queryParsers, backToTop } from 'utils' export const useSearch = (search: string = '', walletID: string = '', dispatch: React.Dispatch) => { const [keywords, setKeywords] = useState('') diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 86578915da..a4d3f856a6 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -1,14 +1,16 @@ -import React, { useCallback, useMemo } from 'react' +import React, { useState, useCallback, useMemo } from 'react' import { useHistory, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Stack, SearchBox } from 'office-ui-fabric-react' import { Pagination } from '@uifabric/experiments' import TransactionList from 'components/TransactionList' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' +import { useState as useGlobalState, useDispatch } from 'states' +import Button from 'widgets/Button' +import { exportTransactions } from 'services/remote' +import { ReactComponent as Export } from 'widgets/Icons/ExportHistory.svg' -import { Routes } from 'utils/const' -import isMainnetUtil from 'utils/isMainnet' +import { RoutePath, isMainnet as isMainnetUtil } from 'utils' import { useSearch } from './hooks' import styles from './history.module.scss' @@ -31,10 +33,21 @@ const History = () => { const [t] = useTranslation() const history = useHistory() const { search } = useLocation() + const [isExporting, setIsExporting] = useState(false) const isMainnet = isMainnetUtil(networks, networkID) const { keywords, onKeywordsChange } = useSearch(search, id, dispatch) - const onSearch = useCallback(() => history.push(`${Routes.History}?keywords=${keywords}`), [history, keywords]) + const onSearch = useCallback(() => history.push(`${RoutePath.History}?keywords=${keywords}`), [history, keywords]) + const onExport = useCallback(() => { + setIsExporting(true) + const timer = setTimeout(() => { + setIsExporting(false) + }, 3000) + exportTransactions({ walletID: id }).finally(() => { + clearTimeout(timer) + setIsExporting(false) + }) + }, [id, setIsExporting]) const tipBlockNumber = useMemo(() => { return Math.max(+syncedBlockNumber, +chainBlockNumber).toString() @@ -43,21 +56,36 @@ const History = () => { const List = useMemo(() => { return ( - +
+ + +
{totalCount ? ( { lastPageIconProps={{ iconName: 'LastPage' }} format="buttons" onPageChange={(idx: number) => { - history.push(`${Routes.History}?pageNo=${idx + 1}&keywords=${keywords}`) + history.push(`${RoutePath.History}?pageNo=${idx + 1}&keywords=${keywords}`) }} /> ) : null} @@ -112,6 +140,8 @@ const History = () => { history, isMainnet, t, + isExporting, + onExport, ]) return List diff --git a/packages/neuron-ui/src/components/ImportKeystore/importKeystore.module.scss b/packages/neuron-ui/src/components/ImportKeystore/importKeystore.module.scss index c1b6762a90..7180840cf1 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/importKeystore.module.scss +++ b/packages/neuron-ui/src/components/ImportKeystore/importKeystore.module.scss @@ -13,5 +13,5 @@ } .actions { - @include formFooter; + @include form-footer; } diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index af8e030564..1fc341cbe5 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -3,21 +3,22 @@ import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import i18n from 'i18next' import { importKeystore, showOpenDialogModal, showErrorMessage } from 'services/remote' -import { useState as useGlobalState } from 'states/stateProvider' -import { useGoBack } from 'utils/hooks' -import generateWalletName from 'utils/generateWalletName' +import { useState as useGlobalState } from 'states' import TextField from 'widgets/TextField' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' -import { Routes, ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' +import { generateWalletName, RoutePath, ErrorCode, CONSTANTS, useGoBack, isSuccessResponse } from 'utils' + import styles from './importKeystore.module.scss' +const { MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } = CONSTANTS + export const importWalletWithKeystore = (params: Controller.ImportKeystoreParams) => ( history: ReturnType ) => { return importKeystore(params).then(res => { - if (res.status === 1) { - history.push(Routes.Overview) + if (isSuccessResponse(res)) { + history.push(window.neuron.role === 'main' ? RoutePath.Overview : RoutePath.SettingsWallets) } else if (res.status > 0) { showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { diff --git a/packages/neuron-ui/src/components/LaunchScreen/index.tsx b/packages/neuron-ui/src/components/LaunchScreen/index.tsx index 9b29df881c..106033107c 100644 --- a/packages/neuron-ui/src/components/LaunchScreen/index.tsx +++ b/packages/neuron-ui/src/components/LaunchScreen/index.tsx @@ -2,11 +2,10 @@ import React, { useEffect } from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Panel, PanelType, SpinnerSize } from 'office-ui-fabric-react' -import { useState as useGlobalState } from 'states/stateProvider' +import { useState as useGlobalState } from 'states' +import { RoutePath } from 'utils' import Spinner from 'widgets/Spinner' -import { Routes } from 'utils/const' - export const LaunchScreen = () => { const { wallet: { id = '' }, @@ -16,7 +15,7 @@ export const LaunchScreen = () => { useEffect(() => { if (id) { - history.push(Routes.Overview) + history.push(RoutePath.Overview) } }, [id, history]) diff --git a/packages/neuron-ui/src/components/NervosDAO/hooks.ts b/packages/neuron-ui/src/components/NervosDAO/hooks.ts index ffdbe55ab1..497f1bebc7 100644 --- a/packages/neuron-ui/src/components/NervosDAO/hooks.ts +++ b/packages/neuron-ui/src/components/NervosDAO/hooks.ts @@ -3,20 +3,16 @@ import { TFunction } from 'i18next' import { AppActions, StateAction } from 'states/stateProvider/reducer' import { updateNervosDaoData, clearNervosDaoData } from 'states/stateProvider/actionCreators' -import { verifyAmount } from 'utils/validators' -import calculateAPC from 'utils/calculateAPC' - -import { CKBToShannonFormatter, shannonToCKBFormatter } from 'utils/formatters' import { - MIN_AMOUNT, - MILLISECONDS_IN_YEAR, - MIN_DEPOSIT_AMOUNT, - MEDIUM_FEE_RATE, - SHANNON_CKB_RATIO, - MAX_DECIMAL_DIGITS, + calculateAPC, ErrorCode, CapacityUnit, -} from 'utils/const' + CONSTANTS, + CKBToShannonFormatter, + shannonToCKBFormatter, + isSuccessResponse, + verifyAmount, +} from 'utils' import { generateDaoWithdrawTx, @@ -26,6 +22,14 @@ import { } from 'services/remote' import { ckbCore, getHeaderByNumber, calculateDaoMaximumWithdraw } from 'services/chain' +const { + MIN_AMOUNT, + MILLISECONDS_IN_YEAR, + MIN_DEPOSIT_AMOUNT, + MEDIUM_FEE_RATE, + SHANNON_CKB_RATIO, + MAX_DECIMAL_DIGITS, +} = CONSTANTS let timer: NodeJS.Timeout const getRecordKey = ({ depositOutPoint, outPoint }: State.NervosDAORecord) => { @@ -49,7 +53,7 @@ export const useUpdateMaxDeposit = ({ feeRate: `${MEDIUM_FEE_RATE}`, }) .then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { const fee = BigInt(res.result.fee) const maxValue = fee < BigInt(wallet.balance) ? BigInt(wallet.balance) - fee : BigInt(0) setMaxDepositAmount(maxValue) @@ -160,7 +164,7 @@ export const useUpdateDepositValue = ({ capacity, walletID, }).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: AppActions.UpdateGeneratedTx, payload: res.result, @@ -295,7 +299,7 @@ export const useOnWithdrawDialogSubmit = ({ feeRate: `${MEDIUM_FEE_RATE}`, }) .then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: AppActions.UpdateGeneratedTx, payload: res.result, @@ -356,7 +360,7 @@ export const useOnActionClick = ({ feeRate: `${MEDIUM_FEE_RATE}`, }) .then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: AppActions.UpdateGeneratedTx, payload: res.result, diff --git a/packages/neuron-ui/src/components/NervosDAO/index.tsx b/packages/neuron-ui/src/components/NervosDAO/index.tsx index df48845d8c..c95ef48e5b 100644 --- a/packages/neuron-ui/src/components/NervosDAO/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAO/index.tsx @@ -1,25 +1,32 @@ import React, { useEffect, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import appState from 'states/initStates/app' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' +import appState from 'states/init/app' +import { useState as useGlobalState, useDispatch } from 'states' -import calculateFee from 'utils/calculateFee' -import { shannonToCKBFormatter } from 'utils/formatters' -import { MIN_DEPOSIT_AMOUNT, ConnectionStatus, SyncStatus } from 'utils/const' -import { backToTop } from 'utils/animations' -import getSyncStatus from 'utils/getSyncStatus' -import getCurrentUrl from 'utils/getCurrentUrl' +import { + CONSTANTS, + backToTop, + calculateFee, + ConnectionStatus, + SyncStatus, + shannonToCKBFormatter, + getCurrentUrl, + getSyncStatus, +} from 'utils' import DepositDialog from 'components/DepositDialog' import WithdrawDialog from 'components/WithdrawDialog' import DAORecord from 'components/NervosDAORecord' import BalanceSyncIcon from 'components/BalanceSyncingIcon' import Button from 'widgets/Button' +import CopyZone from 'widgets/CopyZone' import hooks from './hooks' import styles from './nervosDAO.module.scss' +const { MIN_DEPOSIT_AMOUNT } = CONSTANTS + const NervosDAO = () => { const [focusedRecord, setFocusedRecord] = useState('') const [tabIdx, setTabIdx] = useState('0') @@ -256,14 +263,35 @@ const NervosDAO = () => { const onlineAndSynced = ConnectionStatus.Online === connectionStatus && SyncStatus.SyncCompleted === syncStatus + const freeBalance = shannonToCKBFormatter(`${free}`) + const lockedBalance = shannonToCKBFormatter(`${locked}`) + const info = [ { key: 'free', - value: `${shannonToCKBFormatter(`${free}`)} CKB`, + value: ( + + {`${freeBalance} CKB`} + + ), }, { key: 'locked', - value: onlineAndSynced ? `${shannonToCKBFormatter(`${locked}`)} CKB` : `-- CKB`, + value: onlineAndSynced ? ( + + {`${lockedBalance} CKB`} + + ) : ( + `-- CKB` + ), }, { key: 'apc', @@ -279,13 +307,13 @@ const NervosDAO = () => { return (
{label} - {value} - +
) })} diff --git a/packages/neuron-ui/src/components/NervosDAO/nervosDAO.module.scss b/packages/neuron-ui/src/components/NervosDAO/nervosDAO.module.scss index 2ed93a63cf..3331250919 100644 --- a/packages/neuron-ui/src/components/NervosDAO/nervosDAO.module.scss +++ b/packages/neuron-ui/src/components/NervosDAO/nervosDAO.module.scss @@ -1,21 +1,20 @@ -@import "../../styles/mixin.scss"; +@import '../../styles/mixin.scss'; $infoHeight: 1.75rem; .nervosDAOContainer { display: grid; grid-template: - 'title title title'auto 'free network-alert deposit'auto 'locked . deposit'auto 'apc . .'auto 'records records records'auto/ minmax(350px, 500px) 35px 1fr; + 'title title title' auto + 'free network-alert deposit' auto + 'locked . deposit' auto + 'apc . .' auto + 'records records records' auto/ + minmax(350px, 500px) 35px 1fr; } .title { + @include page-title; grid-area: title; - font-size: 1.375rem; - font-weight: 900; - line-height: 1.75rem; - color: #000; - padding-top: 5px; - padding-bottom: 12px; - margin: 0; } .free { @@ -45,19 +44,24 @@ $infoHeight: 1.75rem; height: $infoHeight; font-size: 0.875rem; - span:first-child { + & > span { font-weight: 600; padding-right: 15px; + white-space: nowrap; &:after { content: ':'; } } - span:last-child { - text-overflow: ellipsis; - overflow: hidden; + & > div { + width: 70%; white-space: nowrap; + text-align: right; + } + .balance { + @include text-overflow-ellipsis; + padding: 0; } } @@ -75,7 +79,7 @@ $infoHeight: 1.75rem; border: none; &[disabled] { - @include disabledBtn; + @include disabled-button; } } } @@ -95,7 +99,7 @@ $infoHeight: 1.75rem; border-bottom: 1px solid #ccc; button { - @include BoldText; + @include bold-text; appearance: none; flex: 1; display: flex; @@ -112,8 +116,6 @@ $infoHeight: 1.75rem; color: var(--nervos-green); opacity: 0.8; } - - } .underline { @@ -136,7 +138,7 @@ $infoHeight: 1.75rem; } } - &>div { + & > div { margin: 10px auto; } @@ -144,5 +146,4 @@ $infoHeight: 1.75rem; font-size: 0.875rem; font-weight: 600; } - } diff --git a/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss b/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss index 2ad09b90d5..b66874ec8a 100644 --- a/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss +++ b/packages/neuron-ui/src/components/NervosDAORecord/daoRecordRow.module.scss @@ -193,7 +193,7 @@ .deposited, .withdrawn, .unlocked { - @include RegularText; + @include regular-text; position: relative; display: flex; justify-content: space-between; @@ -225,7 +225,7 @@ } &:last-of-type { - @include SemiBoldText; + @include semi-bold-text; } } } diff --git a/packages/neuron-ui/src/components/NervosDAORecord/hooks.ts b/packages/neuron-ui/src/components/NervosDAORecord/hooks.ts index 9b46c6c337..d57ef27cf0 100644 --- a/packages/neuron-ui/src/components/NervosDAORecord/hooks.ts +++ b/packages/neuron-ui/src/components/NervosDAORecord/hooks.ts @@ -1,8 +1,9 @@ import { useEffect, useCallback } from 'react' import { showTransactionDetails } from 'services/remote' import { getHeaderByNumber } from 'services/chain' -import { MILLISECONDS_IN_YEAR } from 'utils/const' -import calculateAPC from 'utils/calculateAPC' +import { calculateAPC, CONSTANTS } from 'utils' + +const { MILLISECONDS_IN_YEAR } = CONSTANTS export const useUpdateWithdrawEpochs = ({ isWithdrawn, diff --git a/packages/neuron-ui/src/components/NervosDAORecord/index.tsx b/packages/neuron-ui/src/components/NervosDAORecord/index.tsx index dba903437f..6be26b774e 100644 --- a/packages/neuron-ui/src/components/NervosDAORecord/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAORecord/index.tsx @@ -2,16 +2,24 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import CompensationProgressBar from 'components/CompensationProgressBar' import Button from 'widgets/Button' -import { shannonToCKBFormatter, uniformTimeFormatter } from 'utils/formatters' -import calculateClaimEpochValue from 'utils/calculateClaimEpochValue' -import { epochParser } from 'utils/parsers' -import getDAOCellStatus, { CellStatus } from 'utils/getDAOCellStatus' -import { IMMATURE_EPOCHS, ConnectionStatus, HOURS_PER_EPOCH } from 'utils/const' +import CopyZone from 'widgets/CopyZone' +import { + calculateClaimEpochValue, + ConnectionStatus, + CONSTANTS, + shannonToCKBFormatter, + uniformTimeFormatter, + getDAOCellStatus, + CellStatus, + epochParser, +} from 'utils' import CompensationPeriodTooltip from 'components/CompensationPeriodTooltip' import styles from './daoRecordRow.module.scss' import hooks from './hooks' +const { IMMATURE_EPOCHS, HOURS_PER_EPOCH } = CONSTANTS + const EPOCHS_PER_DAY = 6 const getDaysAndHours = (seconds: number) => { @@ -232,6 +240,8 @@ export const DAORecord = ({ ) } + const amount = shannonToCKBFormatter(capacity) + return (
{badge}
@@ -250,9 +260,7 @@ export const DAORecord = ({
-
- {`${shannonToCKBFormatter(capacity)} CKB`} -
+ {`${amount} CKB`} {progressOrPeriod}
diff --git a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts index 4998d3b277..e5b99417e3 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts +++ b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts @@ -1,20 +1,30 @@ import { useCallback } from 'react' import { useHistory } from 'react-router-dom' -import { StateDispatch } from 'states/stateProvider/reducer' -import { createNetwork, updateNetwork, addNotification } from 'states/stateProvider/actionCreators' +import { StateDispatch, createNetwork, updateNetwork, addNotification } from 'states' +import { ErrorCode, CONSTANTS } from 'utils' -import { MAX_NETWORK_NAME_LENGTH, ErrorCode } from 'utils/const' +const { MAX_NETWORK_NAME_LENGTH } = CONSTANTS -export const useOnSubmit = ( - id: string = '', - name: string = '', - remote: string = '', - networks: Readonly = [], - history: ReturnType, - dispatch: StateDispatch, +export const useOnSubmit = ({ + id = '', + name = '', + remote = '', + networks = [], + history, + dispatch, + disabled, + setIsUpdating, +}: { + id: string + name: string + remote: string + networks: Readonly + history: ReturnType + dispatch: StateDispatch disabled: boolean -) => + setIsUpdating: React.Dispatch +}) => useCallback( (e: React.FormEvent): void => { e.preventDefault() @@ -110,15 +120,16 @@ export const useOnSubmit = ( addNotification(errorMessage)(dispatch) return } + setIsUpdating(true) updateNetwork({ networkID: id!, options: { name, remote, }, - })(dispatch, history) + })(dispatch, history).then(() => setIsUpdating(false)) }, - [id, name, remote, networks, history, dispatch, disabled] + [id, name, remote, networks, history, dispatch, disabled, setIsUpdating] ) export default { diff --git a/packages/neuron-ui/src/components/NetworkEditor/index.tsx b/packages/neuron-ui/src/components/NetworkEditor/index.tsx index d2085039e3..e6bd6cd1d4 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/index.tsx +++ b/packages/neuron-ui/src/components/NetworkEditor/index.tsx @@ -6,18 +6,15 @@ import TextField from 'widgets/TextField' import Button from 'widgets/Button' import Spinner from 'widgets/Spinner' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' -import { verifyNetworkName, verifyURL } from 'utils/validators' -import { useGoBack } from 'utils/hooks' -import { MAX_NETWORK_NAME_LENGTH } from 'utils/const' +import { CONSTANTS, useGoBack, verifyNetworkName, verifyURL } from 'utils' +import { useState as useGlobalState, useDispatch } from 'states' import { useOnSubmit } from './hooks' import styles from './networkEditor.module.scss' +const { MAX_NETWORK_NAME_LENGTH } = CONSTANTS + const NetworkEditor = () => { const { - app: { - loadings: { network: isUpdating = false }, - }, settings: { networks = [] }, } = useGlobalState() const dispatch = useDispatch() @@ -36,6 +33,7 @@ const NetworkEditor = () => { url: '', urlError: '', }) + const [isUpdating, setIsUpdating] = useState(false) const disabled = !!( !editor.name || @@ -89,7 +87,16 @@ const NetworkEditor = () => { ) const goBack = useGoBack(history) - const onSubmit = useOnSubmit(id, editor.name, editor.url, networks, history, dispatch, disabled) + const onSubmit = useOnSubmit({ + id: id!, + name: editor.name, + remote: editor.url, + networks, + history, + dispatch, + disabled, + setIsUpdating, + }) return ( diff --git a/packages/neuron-ui/src/components/NetworkEditor/networkEditor.module.scss b/packages/neuron-ui/src/components/NetworkEditor/networkEditor.module.scss index 386351e02c..d5fca86c52 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/networkEditor.module.scss +++ b/packages/neuron-ui/src/components/NetworkEditor/networkEditor.module.scss @@ -1,5 +1,5 @@ @import '../../styles/mixin.scss'; .actions { - @include formFooter; + @include form-footer; } diff --git a/packages/neuron-ui/src/components/NetworkSetting/index.tsx b/packages/neuron-ui/src/components/NetworkSetting/index.tsx index ba21ab6c48..cc6d62037a 100644 --- a/packages/neuron-ui/src/components/NetworkSetting/index.tsx +++ b/packages/neuron-ui/src/components/NetworkSetting/index.tsx @@ -1,17 +1,19 @@ import React, { useEffect, useCallback } from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' +import { TFunction } from 'i18next' import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react' import Button from 'widgets/Button' +import { ReactComponent as EditNetwork } from 'widgets/Icons/Edit.svg' +import { ReactComponent as DeleteNetwork } from 'widgets/Icons/Delete.svg' -import chainState from 'states/initStates/chain' -import { setCurrentNetowrk, openContextMenu, deleteNetwork } from 'services/remote' +import chainState from 'states/init/chain' +import { setCurrentNetowrk } from 'services/remote' -import { Routes } from 'utils/const' -import { backToTop } from 'utils/animations' +import { backToTop, RoutePath, useOnHandleNetwork, useOnWindowResize, useToggleChoiceGroupBorder } from 'utils' import styles from './networkSetting.module.scss' -const Label = ({ type, t }: { type: 'ckb' | 'ckb_testnet' | 'ckb_dev' | string; t: any }) => { +const Label = ({ type, t }: { type: 'ckb' | 'ckb_testnet' | 'ckb_dev' | string; t: TFunction }) => { switch (type) { case 'ckb': { return {t('settings.network.mainnet')} @@ -32,54 +34,33 @@ const NetworkSetting = ({ chain = chainState, settings: { networks = [] } }: Sta backToTop() }, []) - const onChoiceChange = useCallback((_e, option?: IChoiceGroupOption) => { - if (option) { - setCurrentNetowrk(option.key) - } - }, []) - - const goToCreateNetwork = useCallback(() => { - history.push(`${Routes.NetworkEditor}/new`) - }, [history]) + const { networkID: currentId } = chain - const onContextMenu = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - e.preventDefault() - const { networkId } = (e.target as HTMLElement).dataset - const item = networks.find(n => n.id === networkId) - if (item) { - const isCurrent = item.id === chain.networkID - const isDefault = item.type === 0 - const menuTemplate = [ - { - label: t('common.select'), - enabled: !isCurrent, - click: () => { - setCurrentNetowrk(item.id) - }, - }, - { - label: t('common.edit'), - enabled: true, - click: () => { - history.push(`${Routes.NetworkEditor}/${item.id}`) - }, - }, - { - label: t('common.delete'), - enabled: !isDefault, - click: () => { - deleteNetwork(item.id) - }, - }, - ] - openContextMenu(menuTemplate) + const onChoiceChange = useCallback( + (_e, option?: IChoiceGroupOption) => { + if (option && option.key !== currentId) { + setCurrentNetowrk(option.key) } }, - [chain.networkID, networks, history, t] + [currentId] ) + const goToCreateNetwork = useCallback(() => { + history.push(`${RoutePath.NetworkEditor}/new`) + }, [history]) + + const toggleBottomBorder = useToggleChoiceGroupBorder(`.${styles.networks}`, styles.hasBottomBorder) + + useEffect(() => { + if (networks.length) { + toggleBottomBorder() + } + }, [toggleBottomBorder, networks.length]) + + useOnWindowResize(toggleBottomBorder) + + const onHandleNetwork = useOnHandleNetwork({ history }) + return (
{ + const isDefault = network.type === 0 return (
- + {text} + {`(${network.remote}`} + - {`(${network.remote})`} -
) }, diff --git a/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss b/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss index 650a3bdd67..5a5b8c8508 100644 --- a/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss +++ b/packages/neuron-ui/src/components/NetworkSetting/networkSetting.module.scss @@ -1,24 +1,86 @@ +@import '../../styles/mixin.scss'; + .container { display: grid; - grid-template: - 'networks'auto 'actions'auto; + grid-template: 'networks' auto 'actions' auto; grid-row-gap: 15px; } .networks { grid-area: networks; + height: calc(100vh - 200px); + overflow: auto; + padding: 0 0 5px 8px; } -.network { - span:nth-of-type(2) { - margin: 0 5px; +.choiceLabel { + display: flex !important; + align-items: center; + border: 1px solid transparent; + padding: 3px; + box-sizing: border-box; + + .networkLabel { + display: flex; + align-items: center; + pointer-events: none; + .url { + position: relative; + @include text-overflow-ellipsis; + max-width: 650px; + color: #999; + pointer-events: none; + padding-right: 0.6em; + &:before { + position: absolute; + right: 0; + content: ')'; + } + } + } + button { + display: flex; + height: 1.25rem; + align-items: center; + visibility: hidden; + appearance: none; + margin: 0 0 0 10px; + padding: 0; + border: none; + background: transparent; + svg { + pointer-events: none; + path, + g { + fill: #888; + } + } + &:hover { + svg { + path, + g { + fill: var(--nervos-green); + } + } + } + } + &:hover, + &:focus { + border-color: #aeaeae; + button { + visibility: visible; + } } } .actions { grid-area: actions; - + justify-self: flex-end; button { width: 9.375rem; } } + +.hasBottomBorder { + border-bottom: 0.5px solid #ccc; +} diff --git a/packages/neuron-ui/src/components/NetworkStatus/index.tsx b/packages/neuron-ui/src/components/NetworkStatus/index.tsx index 0314d7530b..7c9cd77857 100644 --- a/packages/neuron-ui/src/components/NetworkStatus/index.tsx +++ b/packages/neuron-ui/src/components/NetworkStatus/index.tsx @@ -1,8 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { ConnectionStatus, SyncStatus } from 'utils/const' -import { localNumberFormatter } from 'utils/formatters' - +import { ConnectionStatus, SyncStatus, localNumberFormatter } from 'utils' import styles from './networkStatus.module.scss' export interface NetworkStatusProps { diff --git a/packages/neuron-ui/src/components/NetworkStatus/networkStatus.module.scss b/packages/neuron-ui/src/components/NetworkStatus/networkStatus.module.scss index f2d73db7aa..20385f47c8 100644 --- a/packages/neuron-ui/src/components/NetworkStatus/networkStatus.module.scss +++ b/packages/neuron-ui/src/components/NetworkStatus/networkStatus.module.scss @@ -14,6 +14,7 @@ $hover-bg-color: #3cc68a4d; display: flex; align-items: center; line-height: 1em; + word-break: break-all; &::before { position: absolute; @@ -52,7 +53,7 @@ $hover-bg-color: #3cc68a4d; } .tooltip { - @include MediumText; + @include medium-text; display: none; position: absolute; bottom: calc(100% + 10px); @@ -69,7 +70,6 @@ $hover-bg-color: #3cc68a4d; box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.22); transition: all 0.2s ease-in-out; opacity: 0; - user-select: none; pointer-events: none; font-size: 0.75rem; line-height: 1.5em; diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index 83ba80ec71..28a9b29d1a 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -1,27 +1,27 @@ import React, { useCallback, useMemo, useEffect } from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import PropertyList, { Property } from 'widgets/PropertyList' -import Balance from 'widgets/Balance' +import BalanceSyncIcon from 'components/BalanceSyncingIcon' +import CopyZone from 'widgets/CopyZone' import { showTransactionDetails } from 'services/remote' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' -import { updateTransactionList } from 'states/stateProvider/actionCreators' +import { useState as useGlobalState, useDispatch, updateTransactionList } from 'states' -import { localNumberFormatter, shannonToCKBFormatter, uniformTimeFormatter } from 'utils/formatters' -import getSyncStatus from 'utils/getSyncStatus' -import getCurrentUrl from 'utils/getCurrentUrl' import { - SyncStatus as SyncStatusEnum, - SyncStatusThatBalanceUpdating, - ConnectionStatus, - PAGE_SIZE, - Routes, - CONFIRMATION_THRESHOLD, -} from 'utils/const' -import { backToTop } from 'utils/animations' + localNumberFormatter, + shannonToCKBFormatter, + uniformTimeFormatter, + backToTop, + CONSTANTS, + RoutePath, + getCurrentUrl, + getSyncStatus, +} from 'utils' + import styles from './overview.module.scss' +const { PAGE_SIZE, CONFIRMATION_THRESHOLD } = CONSTANTS + const genTypeLabel = (type: 'send' | 'receive', status: 'pending' | 'confirming' | 'success' | 'failed') => { switch (type) { case 'send': { @@ -51,7 +51,7 @@ const genTypeLabel = (type: 'send' | 'receive', status: 'pending' | 'confirming' const Overview = () => { const { app: { tipBlockNumber, tipBlockTimestamp }, - wallet: { id, name, balance = '' }, + wallet: { id, balance = '' }, chain: { tipBlockNumber: syncedBlockNumber, transactions: { items = [] }, @@ -87,42 +87,9 @@ const Overview = () => { })(dispatch) }, [id, dispatch]) const onGoToHistory = useCallback(() => { - history.push(Routes.History) + history.push(RoutePath.History) }, [history]) - const balanceProperties: Property[] = useMemo(() => { - const balanceValue = shannonToCKBFormatter(balance) - let prompt = null - if (ConnectionStatus.Connecting === connectionStatus) { - prompt = {t('sync.connecting')} - } else if (ConnectionStatus.Offline === connectionStatus) { - prompt = ( - - {t('sync.sync-failed')} - - ) - } else if (SyncStatusEnum.SyncNotStart === syncStatus) { - prompt = ( - - {t('sync.sync-not-start')} - - ) - } else if (SyncStatusThatBalanceUpdating.includes(syncStatus) || ConnectionStatus.Connecting === connectionStatus) { - prompt = {t('sync.syncing-balance')} - } - return [ - { - label: t('overview.balance'), - value: ( -
- - {prompt} -
- ), - }, - ] - }, [t, balance, syncStatus, connectionStatus]) - const onRecentActivityDoubleClick = useCallback((e: React.SyntheticEvent) => { const cellElement = e.target as HTMLTableCellElement if (cellElement?.parentElement?.dataset?.hash) { @@ -221,10 +188,19 @@ const Overview = () => { ) }, [recentItems, syncedBlockNumber, tipBlockNumber, t, onRecentActivityDoubleClick]) + const ckbBalance = shannonToCKBFormatter(balance) + return (
-

{name}

- + {/*

{name}

*/} +

{t('navbar.overview')}

+
+ {`${t('overview.balance')}:`} + + {`${ckbBalance}`} + + +

{t('overview.recent-activities')}

{items.length ? ( diff --git a/packages/neuron-ui/src/components/Overview/overview.module.scss b/packages/neuron-ui/src/components/Overview/overview.module.scss index 60bb3627be..125c1137d1 100644 --- a/packages/neuron-ui/src/components/Overview/overview.module.scss +++ b/packages/neuron-ui/src/components/Overview/overview.module.scss @@ -8,85 +8,50 @@ $confirming-color: #b3b3b3; .overview { display: grid; grid-template: - 'wallet-name wallet-name'auto 'balance balance'auto 'activities-title activities-title'auto 'activities activities'auto 'more-link more-link'auto/ 1fr auto; - // padding: 39px 11px 0; this size is from design, but not used now. - padding: 39px 0 0; + 'page-title page-title' auto + 'balance balance' auto + 'activities-title activities-title' auto + 'activities activities' auto + 'more-link more-link' auto/ + 1fr auto; } -.walletName, +.pageTitle, .recentActivitiesTitle { - @include BoldText; + @include bold-text; font-size: 1.375rem; - line-height: 1.22em; - letter-spacing: 1.1px; color: #000; margin: 0; } -.walletName { - grid-area: wallet-name; - margin-bottom: 11px; +.pageTitle { + @include page-title; + grid-area: page-title; } .recentActivitiesTitle { grid-area: activities-title; - margin-top: 45px; - margin-bottom: 17px; + margin-top: 30px; + margin-bottom: 15px; + font-size: 1rem; + font-weight: 500; } .balance { grid-area: balance; + font-size: 0.875rem; + font-weight: 600; } .balanceValue { - display: flex; - flex-direction: column; - text-align: left; -} - -.balancePrompt { - @include BoldText; - color: var(--nervos-green); - font-size: 0.8rem; - margin-top: 3px; -} - -.balanceInt { - @include BoldText; - font-size: 1.125rem; -} - -section { - position: absolute; - left: 0; - top: 100%; - width: 100%; - background-color: #f5f5f5; - box-shadow: 1px 1px 3px 0 rgba(0, 0, 0, 0.12); - padding: 6px 0; - z-index: 1; - - &>div { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 11px; - - &:hover { - background-color: #e3e3e3; - } - - &>span { - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.45px; - color: #000; - - &:last-child { - font-weight: normal; - } - } - + font-weight: 500; + max-width: 60%; + padding: 0 5px 0 60px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + &:after { + content: ' CKB'; } } @@ -116,7 +81,6 @@ section { text-align: left; font-size: 0.875rem; font-weight: 600; - letter-spacing: 0.7px; color: #000; line-height: 1em; padding: 16px 0; @@ -126,60 +90,58 @@ section { font-size: 0.875rem; line-height: 1em; padding: 2px 0; - letter-spacing: 0.7px; color: #000; } .txStatus { - &>div::after { + & > div::after { position: absolute; display: block; content: ''; border-radius: 50%; width: 8px; height: 8px; - left: 100%; + left: 0; top: 50%; transform: translateY(-50%); } - &[data-status="pending"]>div::after { + &[data-status='pending'] > div::after { background-color: $pending-color; filter: drop-shadow(0 0 1px $pending-color); animation: blink 5s infinite; } - &[data-status="confirming"]>div::after { + &[data-status='confirming'] > div::after { background-color: $pending-color; filter: drop-shadow(0 0 1px $pending-color); animation: blink 5s infinite; } - &[data-status="success"]>div::after { + &[data-status='success'] > div::after { background-color: $success-color; filter: drop-shadow(0 0 1px $success-color); } - &[data-status="failed"]>div::after { + &[data-status='failed'] > div::after { background-color: $failed-color; filter: drop-shadow(0 0 1px $failed-color); } - &>div { + & > div { display: flex; flex-direction: column; position: relative; - width: 150px; + padding-left: 20px; - &>span:first-child { - flex: 1 + & > span:first-child { + flex: 1; } - &>span:nth-child(2) { + & > span:nth-child(2) { color: #626262; font-size: 0.625rem; line-height: 0.8125rem; - letter-spacing: 0.5px; margin-top: 3px; } } @@ -188,22 +150,22 @@ section { .linkToHistory { grid-area: more-link; - font-size: 0.625rem; - letter-spacing: 0.5px; + font-size: 0.75rem; color: #000; margin-top: 13px; - &>span:hover { + & > span:hover { color: var(--nervos-green); } } .noActivities { grid-area: more-link; + font-size: 0.75rem; + color: #000; } @keyframes blink { - from, to { opacity: 0.5; diff --git a/packages/neuron-ui/src/components/PasswordRequest/index.tsx b/packages/neuron-ui/src/components/PasswordRequest/index.tsx index 96abe14640..8ebbf51067 100644 --- a/packages/neuron-ui/src/components/PasswordRequest/index.tsx +++ b/packages/neuron-ui/src/components/PasswordRequest/index.tsx @@ -1,12 +1,20 @@ -import React, { useRef, useCallback, useMemo } from 'react' +import React, { useState, useRef, useCallback, useMemo, useEffect } from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import TextField from 'widgets/TextField' -import { useDialog } from 'utils/hooks' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' -import { AppActions } from 'states/stateProvider/reducer' -import { sendTransaction, deleteWallet, backupWallet } from 'states/stateProvider/actionCreators' +import Spinner from 'widgets/Spinner' +import { useDialog, ResponseCode, ErrorCode, RoutePath } from 'utils' + +import { + useState as useGlobalState, + useDispatch, + AppActions, + sendTransaction, + deleteWallet, + backupWallet, +} from 'states' +import { PasswordIncorrectException } from 'exceptions' import styles from './passwordRequest.module.scss' const PasswordRequest = () => { @@ -14,7 +22,7 @@ const PasswordRequest = () => { app: { send: { description, generatedTx }, loadings: { sending: isSending = false }, - passwordRequest: { walletID = '', actionType = null, password = '' }, + passwordRequest: { walletID = '', actionType = null }, }, settings: { wallets = [] }, } = useGlobalState() @@ -22,6 +30,15 @@ const PasswordRequest = () => { const [t] = useTranslation() const history = useHistory() const dialogRef = useRef(null) + + const [password, setPassword] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + setPassword('') + setError('') + }, [actionType, setError, setPassword]) + const onDismiss = useCallback(() => { dispatch({ type: AppActions.DismissPasswordRequest, @@ -31,72 +48,84 @@ const PasswordRequest = () => { const wallet = useMemo(() => wallets.find(w => w.id === walletID), [walletID, wallets]) - const disabled = !password || (actionType === 'send' && isSending) + const isLoading = ['send', 'unlock'].includes(actionType || '') && isSending + const disabled = !password || isSending const onSubmit = useCallback( - (e?: React.FormEvent): void => { + async (e?: React.FormEvent) => { if (e) { e.preventDefault() } if (disabled) { return } - switch (actionType) { - case 'send': { - if (isSending) { + try { + switch (actionType) { + case 'send': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(status => { + if (status === ResponseCode.SUCCESS) { + history.push(RoutePath.History) + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) break } - sendTransaction({ - walletID, - tx: generatedTx, - description, - password, - })(dispatch, history) - break - } - case 'delete': { - deleteWallet({ - id: walletID, - password, - })(dispatch) - break - } - case 'backup': { - backupWallet({ - id: walletID, - password, - })(dispatch) - break - } - case 'unlock': { - if (isSending) { + case 'delete': { + await deleteWallet({ id: walletID, password })(dispatch).then(status => { + if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'backup': { + await backupWallet({ id: walletID, password })(dispatch).then(status => { + if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + case 'unlock': { + if (isSending) { + break + } + await sendTransaction({ walletID, tx: generatedTx, description, password })(dispatch).then(status => { + if (status === ResponseCode.SUCCESS) { + dispatch({ + type: AppActions.SetGlobalDialog, + payload: 'unlock-success', + }) + } else if (status === ErrorCode.PasswordIncorrect) { + throw new PasswordIncorrectException() + } + }) + break + } + default: { break } - sendTransaction({ - walletID, - tx: generatedTx, - description, - password, - })(dispatch, history, { type: 'unlock' }) - break } - default: { - break + } catch (err) { + if (err.code === ErrorCode.PasswordIncorrect) { + setError(t(err.message)) } } }, - [dispatch, walletID, password, actionType, description, history, isSending, generatedTx, disabled] + [dispatch, walletID, password, actionType, description, history, isSending, generatedTx, disabled, setError, t] ) const onChange = useCallback( (e: React.SyntheticEvent) => { const { value } = e.target as HTMLInputElement - dispatch({ - type: AppActions.UpdatePassword, - payload: value, - }) + setPassword(value) + setError('') }, - [dispatch] + [setPassword, setError] ) if (!wallet) { @@ -118,10 +147,13 @@ const PasswordRequest = () => { autoFocus required className={styles.passwordInput} + error={error} />
diff --git a/packages/neuron-ui/src/components/PasswordRequest/passwordRequest.module.scss b/packages/neuron-ui/src/components/PasswordRequest/passwordRequest.module.scss index 08a308eb54..23802741ea 100644 --- a/packages/neuron-ui/src/components/PasswordRequest/passwordRequest.module.scss +++ b/packages/neuron-ui/src/components/PasswordRequest/passwordRequest.module.scss @@ -1,7 +1,7 @@ -@import '../../styles//mixin.scss'; +@import '../../styles/mixin.scss'; .dialog { - @include dialogContainer; + @include dialog-container; padding: 49px 73px; &::backdrop { @@ -10,7 +10,7 @@ } .footer { - @include dialogFooter; + @include dialog-footer; button:last-of-type { margin-left: 9px; @@ -26,12 +26,12 @@ &:after { display: inline; - content: ':' + content: ':'; } } .walletName { - @include BoldText; + @include bold-text; color: var(--nervos-green); } diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 3789e55472..34e56a13e4 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -1,12 +1,10 @@ -import React, { useCallback, useMemo } from 'react' +import React, { useMemo } from 'react' import { useRouteMatch } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { TooltipHost } from 'office-ui-fabric-react' -import { ReactComponent as Copy } from 'widgets/Icons/ReceiveCopy.svg' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' +import { useState as useGlobalState, useDispatch } from 'states' import QRCode from 'widgets/QRCode' -import { addPopup } from 'states/stateProvider/actionCreators' +import CopyZone from 'widgets/CopyZone' import styles from './receive.module.scss' const Receive = () => { @@ -20,36 +18,10 @@ const Receive = () => { } = useRouteMatch() const accountAddress = useMemo( - () => address || (addresses.find(addr => addr.type === 0 && addr.txCount === 0) || { address: '' }).address || '', + () => (address || addresses.find(addr => addr.type === 0 && addr.txCount === 0)?.address) ?? '', [address, addresses] ) - const copyAddress = useCallback(() => { - window.navigator.clipboard.writeText(accountAddress) - addPopup('addr-copied')(dispatch) - }, [accountAddress, dispatch]) - - const Address = useMemo( - () => ( -
- - <> - - - - -
- ), - [copyAddress, accountAddress, t] - ) - if (!accountAddress) { return
{t('receive.address-not-found')}
} @@ -62,7 +34,11 @@ const Receive = () => { }} > - {Address} +
+ + {accountAddress} + +

{t('receive.prompt')}

) diff --git a/packages/neuron-ui/src/components/Receive/receive.module.scss b/packages/neuron-ui/src/components/Receive/receive.module.scss index bcdc9f87c7..7afcd40680 100644 --- a/packages/neuron-ui/src/components/Receive/receive.module.scss +++ b/packages/neuron-ui/src/components/Receive/receive.module.scss @@ -1,23 +1,7 @@ .address { - display: flex; - width: 520px; margin: 20px auto 0; - - &>div { - display: flex; - justify-content: space-between; - align-items: center; - flex: 1; - } - - input { - flex: 1; - padding: 6px 17px; - border: 1px solid #000; - color: #000; - font-size: 1rem; - letter-spacing: 0.6px; - } + display: flex; + justify-content: center; } .copyBtn { @@ -26,10 +10,8 @@ background: transparent; &:hover { - g, path { - stroke: var(--nervos-green); fill: var(--nervos-green); } @@ -39,6 +21,5 @@ .notation { width: 450px; font-size: 0.75rem; - letter-spacing: 0.5px; - margin: 20px auto 0; + margin: 6px auto 0; } diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index ee1ca6d46e..e8a5d4fe0a 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -1,15 +1,21 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react' import { TFunction } from 'i18next' -import i18n from 'utils/i18n' import jsQR from 'jsqr' import { AppActions, StateDispatch } from 'states/stateProvider/reducer' import { captureScreenshot, showErrorMessage } from 'services/remote' import { generateTx, generateSendingAllTx } from 'services/remote/wallets' -import { outputsToTotalAmount, CKBToShannonFormatter, shannonToCKBFormatter } from 'utils/formatters' -import { verifyTransactionOutputs, verifyAddress } from 'utils/validators' -import calculateFee from 'utils/calculateFee' +import { + outputsToTotalAmount, + CKBToShannonFormatter, + shannonToCKBFormatter, + calculateFee, + verifyTransactionOutputs, + verifyAddress, +} from 'utils' +import i18n from 'utils/i18n' + import styles from './send.module.scss' let generateTxTimer: ReturnType diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index 018c52c190..4c3a327ca0 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -17,34 +17,33 @@ import Calendar from 'widgets/Icons/Calendar.png' import ActiveCalendar from 'widgets/Icons/ActiveCalendar.png' import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' -import appState from 'states/initStates/app' +import { useState as useGlobalState, useDispatch } from 'states' +import appState from 'states/init/app' import { PlaceHolders, ErrorCode, - MAX_DECIMAL_DIGITS, - MAINNET_TAG, SyncStatus, SyncStatusThatBalanceUpdating, ConnectionStatus, - SINCE_FIELD_SIZE, -} from 'utils/const' -import getSyncStatus from 'utils/getSyncStatus' -import getCurrentUrl from 'utils/getCurrentUrl' -import { shannonToCKBFormatter, localNumberFormatter } from 'utils/formatters' -import { + CONSTANTS, + shannonToCKBFormatter, + localNumberFormatter, + getCurrentUrl, + getSyncStatus, verifyTotalAmount, verifyTransactionOutputs, verifyAmount, verifyAmountRange, verifyAddress, -} from 'utils/validators' +} from 'utils' -import DatetimePicker from 'widgets/DatetimePicker' +import DatetimePicker, { formatDate } from 'widgets/DatetimePicker' import { useInitialize } from './hooks' import styles from './send.module.scss' +const { MAX_DECIMAL_DIGITS, MAINNET_TAG, SINCE_FIELD_SIZE } = CONSTANTS + const Send = () => { const { app: { @@ -305,7 +304,7 @@ const Send = () => {
calendar active-calendar - {item.date ? `${t('send.release-on')}: ${new Date(+item.date).toLocaleDateString()}` : null} + {item.date ? `${t('send.release-on')}: ${formatDate(new Date(+item.date))}` : null} + + ) }, }))} onChange={onChange} + styles={{ + label: { + padding: '3px', + }, + }} />
{buttons.map(({ label, ariaLabel, url }) => ( diff --git a/packages/neuron-ui/src/components/WalletSetting/walletSetting.module.scss b/packages/neuron-ui/src/components/WalletSetting/walletSetting.module.scss index b3ba7b1d4d..6319a5c73a 100644 --- a/packages/neuron-ui/src/components/WalletSetting/walletSetting.module.scss +++ b/packages/neuron-ui/src/components/WalletSetting/walletSetting.module.scss @@ -1,21 +1,77 @@ .container { display: grid; - grid-template: - 'wallets'auto 'actions'auto; + grid-template: 'wallets' auto 'actions' auto; grid-row-gap: 15px; - } .wallets { grid-area: wallets; - + height: calc(100vh - 200px); + overflow: auto; + padding: 0 0 5px 8px; } .actions { grid-area: actions; + justify-self: flex-end; button { min-width: 9.375rem; margin-right: 15px; } } + +.choiceLabel { + display: flex !important; + align-items: center; + border: 1px solid transparent; + padding: 3px; + box-sizing: border-box; + + .walletName { + display: inline-block; + width: 500px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + button { + display: flex; + align-items: center; + height: 1.25rem; + visibility: hidden; + appearance: none; + margin: 0 0 0 10px; + padding: 0; + border: none; + background: transparent; + svg { + width: 0.875rem; + height: 0.875rem; + pointer-events: none; + path, + g { + fill: #888; + } + } + &:hover { + svg { + path, + g { + fill: var(--nervos-green); + } + } + } + } + &:hover, + &:focus { + border-color: #aeaeae; + button { + visibility: visible; + } + } +} + +.hasBottomBorder { + border-bottom: 0.5px solid #ccc; +} diff --git a/packages/neuron-ui/src/components/WalletWizard/index.tsx b/packages/neuron-ui/src/components/WalletWizard/index.tsx index 0980ce9ab4..7fbe366371 100644 --- a/packages/neuron-ui/src/components/WalletWizard/index.tsx +++ b/packages/neuron-ui/src/components/WalletWizard/index.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react' import { useHistory, useRouteMatch } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import i18n from 'utils/i18n' import { Stack, Icon, Text, TextField, FontSizes } from 'office-ui-fabric-react' import Button from 'widgets/Button' import CustomTextField from 'widgets/TextField' @@ -10,18 +9,27 @@ import Spinner from 'widgets/Spinner' import withWizard, { WizardElementProps, WithWizardState } from 'components/withWizard' import { createWallet, importMnemonic, generateMnemonic, validateMnemonic, showErrorMessage } from 'services/remote' -import { Routes, MnemonicAction, ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' +import { + generateWalletName, + RoutePath, + MnemonicAction, + ErrorCode, + CONSTANTS, + isSuccessResponse, + verifyPasswordComplexity, +} from 'utils' import { buttonGrommetIconStyles } from 'utils/icons' -import { verifyPasswordComplexity } from 'utils/validators' -import generateWalletName from 'utils/generateWalletName' +import i18n from 'utils/i18n' import styles from './walletWizard.module.scss' +const { MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } = CONSTANTS + const createWalletWithMnemonic = (params: Controller.ImportMnemonicParams) => ( history: ReturnType ) => { return createWallet(params).then(res => { - if (res.status === 1) { - history.push(Routes.Overview) + if (isSuccessResponse(res)) { + history.push(window.neuron.role === 'main' ? RoutePath.Overview : RoutePath.SettingsWallets) } else if (res.status > 0) { showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { @@ -37,8 +45,8 @@ const importWalletWithMnemonic = (params: Controller.ImportMnemonicParams) => ( history: ReturnType ) => { return importMnemonic(params).then(res => { - if (res.status === 1) { - history.push(Routes.Overview) + if (isSuccessResponse(res)) { + history.push(window.neuron.role === 'main' ? RoutePath.Overview : RoutePath.SettingsWallets) } else if (res.status > 0) { showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { @@ -93,7 +101,7 @@ const Welcome = ({ rootPath = '/wizard', wallets = [] }: WizardElementProps) => const history = useHistory() useEffect(() => { if (wallets.length) { - history.push(Routes.Overview) + history.push(RoutePath.Overview) } }, [wallets, history]) @@ -120,7 +128,7 @@ const Welcome = ({ rootPath = '/wizard', wallets = [] }: WizardElementProps) => {t('common.or')} - @@ -115,7 +123,7 @@ const Navbar = () => { syncedBlockNumber={syncedBlockNumber} networkName={networkName} connectionStatus={connectionStatus} - onAction={() => history.push(Routes.SettingsNetworks)} + onAction={() => showSettings({ tab: 'networks' })} />
diff --git a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss index 67345c3518..eafc5f9491 100644 --- a/packages/neuron-ui/src/containers/Navbar/navbar.module.scss +++ b/packages/neuron-ui/src/containers/Navbar/navbar.module.scss @@ -1,4 +1,4 @@ -@import '../../styles//mixin.scss'; +@import '../../styles/mixin.scss'; $hover-bg-color: #3cc68a4d; $left-padding: 15px; @@ -16,7 +16,7 @@ $left-padding: 15px; } .name { - @include RegularText; + @include regular-text; display: flex; justify-content: flex-start; align-items: center; @@ -39,7 +39,7 @@ $left-padding: 15px; background-color: #191919; button { - @include MediumText; + @include medium-text; appearance: none; border: none; color: white; @@ -68,6 +68,7 @@ $left-padding: 15px; & > div { align-items: flex-end; padding-left: $left-padding * 2; + padding-right: $left-padding; padding-bottom: 5px; } } @@ -90,7 +91,6 @@ $left-padding: 15px; margin: 5px $left-padding; border-bottom: 1px solid currentColor; padding: 4px 0; - user-select: none; pointer-events: none; font-size: 0.75rem; display: flex; diff --git a/packages/neuron-ui/src/containers/Notification/index.tsx b/packages/neuron-ui/src/containers/Notification/index.tsx index a572f3240f..76de68400e 100644 --- a/packages/neuron-ui/src/containers/Notification/index.tsx +++ b/packages/neuron-ui/src/containers/Notification/index.tsx @@ -2,14 +2,17 @@ import React, { useMemo, useCallback, MouseEventHandler } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { Stack, MessageBar, MessageBarType, IconButton, Panel, PanelType, Text } from 'office-ui-fabric-react' -import { useState as useGlobalState, useDispatch } from 'states/stateProvider' -import { StateDispatch } from 'states/stateProvider/reducer' import { + useState as useGlobalState, + useDispatch, + StateDispatch, toggleAllNotificationVisibility, toggleTopAlertVisibility, dismissNotification, dismissGlobalDialog, -} from 'states/stateProvider/actionCreators' +} from 'states' +import { useOnLocaleChange } from 'utils' + import GlobalDialog from 'widgets/GlobalDialog' import AlertDialog from 'widgets/AlertDialog' import styles from './Notification.module.scss' @@ -71,7 +74,8 @@ export const NoticeContent = () => { }, } = useGlobalState() const dispatch = useDispatch() - const [t] = useTranslation() + const [t, i18n] = useTranslation() + useOnLocaleChange(i18n) const notificationsInDesc = useMemo(() => [...notifications].reverse(), [notifications]) const notification: State.Message | undefined = notificationsInDesc[0] diff --git a/packages/neuron-ui/src/containers/Settings/index.tsx b/packages/neuron-ui/src/containers/Settings/index.tsx new file mode 100644 index 0000000000..e04b11c351 --- /dev/null +++ b/packages/neuron-ui/src/containers/Settings/index.tsx @@ -0,0 +1,134 @@ +import React, { useCallback, useEffect } from 'react' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next' + +import SettingTabs from 'components/SettingTabs' +import NetworkEditor from 'components/NetworkEditor' +import WalletEditor from 'components/WalletEditor' +import WalletWizard from 'components/WalletWizard' +import ImportKeystore from 'components/ImportKeystore' +import PasswordRequest from 'components/PasswordRequest' + +import { useDispatch, NeuronWalletActions, AppActions } from 'states' + +import { LocalCacheKey } from 'services/localCache' +import { + AppUpdater as AppUpdaterSubject, + Navigation as NavigationSubject, + Command as CommandSubject, +} from 'services/subjects' +import { getPlatform } from 'services/remote' +import { RoutePath, useRoutes, useOnLocalStorageChange, useOnLocaleChange } from 'utils' + +export const settingContents: CustomRouter.Route[] = [ + { name: `SettingTabs`, path: RoutePath.Settings, exact: false, component: SettingTabs }, + { name: `NetworkEditor`, path: RoutePath.NetworkEditor, params: '/:id', exact: false, component: NetworkEditor }, + { name: `WalletEditor`, path: RoutePath.WalletEditor, params: '/:id', exact: false, component: WalletEditor }, + { name: `WalletWizard`, path: RoutePath.WalletWizard, exact: false, component: WalletWizard }, + { name: `ImportKeystore`, path: RoutePath.ImportKeystore, exact: false, component: ImportKeystore }, + { name: `PasswordRequest`, path: '/', exact: false, component: PasswordRequest }, +] + +const Settings = () => { + const dispatch = useDispatch() + const history = useHistory() + const [, i18n] = useTranslation() + useOnLocaleChange(i18n) + useEffect(() => { + const isMac = getPlatform() === 'darwin' + window.document.title = i18n.t(`settings.title.${isMac ? 'mac' : 'normal'}`) + // eslint-disable-next-line + }, [i18n.language]) + + useEffect(() => { + const onNavigate = (url: string) => history.push(url) + + const onAppUpdaterUpdates = (info: Subject.AppUpdater) => { + dispatch({ type: NeuronWalletActions.UpdateAppUpdaterStatus, payload: info }) + } + + const onCommand = ({ type, payload }: Subject.CommandMetaInfo) => { + if (payload) { + switch (type) { + case 'delete-wallet': { + dispatch({ type: AppActions.RequestPassword, payload: { walletID: payload, actionType: 'delete' } }) + break + } + case 'backup-wallet': { + dispatch({ type: AppActions.RequestPassword, payload: { walletID: payload, actionType: 'backup' } }) + break + } + default: { + // ignore + } + } + } else { + console.warn('Empty payload from command') + } + } + + const navSubscription = NavigationSubject.subscribe(onNavigate) + const appUpdaterSubscription = AppUpdaterSubject.subscribe(onAppUpdaterUpdates) + const commandSubscription = CommandSubject.subscribe(onCommand) + + return () => { + navSubscription.unsubscribe() + appUpdaterSubscription.unsubscribe() + commandSubscription.unsubscribe() + } + }, [dispatch, history]) + + const onChange = useCallback( + (e: StorageEvent) => { + if (!e.newValue) { + return + } + try { + switch (e.key as LocalCacheKey) { + case LocalCacheKey.CurrentWallet: { + dispatch({ + type: NeuronWalletActions.UpdateCurrentWallet, + payload: JSON.parse(e.newValue) as Partial, + }) + break + } + case LocalCacheKey.Wallets: { + dispatch({ + type: NeuronWalletActions.UpdateWalletList, + payload: JSON.parse(e.newValue) as State.WalletIdentity[], + }) + break + } + case LocalCacheKey.CurrentNetworkID: { + dispatch({ + type: NeuronWalletActions.UpdateCurrentNetworkID, + payload: e.newValue, + }) + break + } + case LocalCacheKey.Networks: { + dispatch({ + type: NeuronWalletActions.UpdateNetworkList, + payload: JSON.parse(e.newValue) as State.Network[], + }) + break + } + default: { + // ignore + } + } + } catch (err) { + console.error(err) + } + }, + [dispatch] + ) + + useOnLocalStorageChange(onChange) + const routes = useRoutes(settingContents) + return <>{routes} +} + +Settings.displayName = 'Settings' + +export default Settings diff --git a/packages/neuron-ui/src/exceptions/index.ts b/packages/neuron-ui/src/exceptions/index.ts new file mode 100644 index 0000000000..9a5b1e8734 --- /dev/null +++ b/packages/neuron-ui/src/exceptions/index.ts @@ -0,0 +1,73 @@ +import { ErrorCode } from 'utils/enums' + +export class FieldInvalidException extends Error { + public code = ErrorCode.FieldInvalid + public i18n: { + fieldName: string + } + + constructor(fieldName: string) { + super(`messages.codes.${ErrorCode.FieldInvalid}`) + this.i18n = { + fieldName, + } + } +} + +export class FieldRequiredException extends Error { + public code = ErrorCode.FieldRequired + public i18n: { + fieldName: string + } + + constructor(fieldName: string) { + super(`messages.codes.${ErrorCode.FieldRequired}`) + this.i18n = { fieldName } + } +} + +export class FieldTooLongException extends Error { + public code = ErrorCode.FieldTooLong + public i18n: { + fieldName: string + length: number + } + + constructor(fieldName: string, length: number) { + super(`messages.codes.${ErrorCode.FieldTooLong}`) + this.i18n = { + fieldName, + length, + } + } +} + +export class FieldUsedException extends Error { + public code = ErrorCode.FieldUsed + public i18n: { + fieldName: string + } + + constructor(fieldName: string) { + super(`messages.codes.${ErrorCode.FieldUsed}`) + this.i18n = { + fieldName, + } + } +} + +export class AmountNotEnoughException extends Error { + public code = ErrorCode.AmountNotEnough + constructor() { + super(`messages.codes.${ErrorCode.AmountNotEnough}`) + } +} + +export class PasswordIncorrectException extends Error { + public code = ErrorCode.PasswordIncorrect + constructor() { + super(`messages.codes.${ErrorCode.PasswordIncorrect}`) + } +} + +export default undefined diff --git a/packages/neuron-ui/src/index.tsx b/packages/neuron-ui/src/index.tsx index ab429339ad..2174051d11 100755 --- a/packages/neuron-ui/src/index.tsx +++ b/packages/neuron-ui/src/index.tsx @@ -1,59 +1,56 @@ import React from 'react' import ReactDOM from 'react-dom' -import { HashRouter as Router, Route } from 'react-router-dom' +import { HashRouter as Router } from 'react-router-dom' import 'theme' import 'styles/index.scss' import 'utils/i18n' +import { useRoutes } from 'utils' import Navbar from 'containers/Navbar' import Notification from 'containers/Notification' import Main from 'containers/Main' +import Settings from 'containers/Settings' import Transaction from 'components/Transaction' import SignAndVerify from 'components/SignAndVerify' import ErrorBoundary from 'components/ErrorBoundary' -import withProviders from 'states/stateProvider' - -export const containers: CustomRouter.Route[] = [ - { - name: 'Navbar', - path: '/', - exact: false, - component: Navbar, - }, - { - name: 'Main', - path: '/', - exact: false, - component: Main, - }, - { - name: 'Notification', - path: '/', - exact: false, - component: Notification, - }, -] - -const App = withProviders(() => ( - - - {containers.map(container => { - return - })} - - -)) - -Object.defineProperty(App, 'displayName', { - value: 'App', -}) +import { withProvider } from 'states' if (window.location.hash.startsWith('#/transaction/')) { ReactDOM.render(, document.getElementById('root')) } else if (window.location.hash.startsWith('#/sign-verify/')) { ReactDOM.render(, document.getElementById('root')) } else { + const isSettings = window.location.hash.startsWith('#/settings/') + + window.neuron = { + role: isSettings ? 'settings' : 'main', + } + + const containers: CustomRouter.Route[] = isSettings + ? [ + { name: 'Main', path: '/', exact: false, component: Settings }, + { name: 'Notification', path: '/', exact: false, component: Notification }, + ] + : [ + { name: 'Navbar', path: '/', exact: false, component: Navbar }, + { name: 'Main', path: '/', exact: false, component: Main }, + { name: 'Notification', path: '/', exact: false, component: Notification }, + ] + + const App = withProvider(() => { + const routes = useRoutes(containers) + return ( + + {routes} + + ) + }) + + Object.defineProperty(App, 'displayName', { + value: 'App', + }) + ReactDOM.render(, document.getElementById('root')) } diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index f38fbcbbdf..c412c5ed7e 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -16,7 +16,6 @@ "receive": "Receive", "history": "History", "addresses": "Addresses", - "settings": "Settings", "nervos-dao": "Nervos DAO", "special-assets": "Customized Assets", "sync-not-start": "Sync not started yet", @@ -53,7 +52,8 @@ "failed": "Failed", "pending": "Pending", "confirming": "Confirming" - } + }, + "copy-balance": "Copy Balance" }, "wizard": { "welcome-to-nervos-neuron": "Welcome to Neuron", @@ -115,7 +115,7 @@ "fee": "Transaction Fee", "advanced-fee-settings": "Advanced fee settings", "price": "Price", - "expected-speed": "Expected speed", + "pick-price": "Quick Pick Price", "total-cycles": "Total RISC-V Cycles", "scan-screen-qr-code": "Scan the QR Code on the screen", "set-locktime": "Set Locktime", @@ -123,7 +123,7 @@ "release-on": "Release on" }, "receive": { - "click-to-copy": "Click to copy the address", + "copy-address": "Copy Address", "address-not-found": "Address not found", "prompt": "Neuron picks a new receiving address for better privacy. Please go to the Address Book if you want to use a previously used receiving address.", "address-qrcode": "Address QR Code", @@ -155,15 +155,21 @@ "blockNumber": "Block Number", "basic-information": "Basic Information", "search": { + "button": "Search", "placeholder": "Search tx hash, address or date (yyyy-mm-dd)" }, + "export-history": "Export Transaction History", "confirmations": "Confirmations", "confirming-with-count": "{{confirmations}} Confirmations", "view-on-explorer": "View on Explorer", "copy-transaction-hash": "Copy Transaction Hash", - "no-txs": "No Transaction History" + "no-txs": "No Transaction History", + "copy-tx-hash": "Copy Transaction Hash", + "copy-balance": "Copy Balance", + "copy-address": "Copy Address" }, "transaction": { + "window-title": "Transaction: {{hash}}", "date": "Date", "transaction-hash": "Transaction Hash", "block-number": "Block Number", @@ -193,6 +199,10 @@ "view-on-explorer": "View on Explorer" }, "settings": { + "title": { + "normal": "Settings", + "mac": "Preference" + }, "go-to-overview": "Go to Overview", "setting-tabs": { "general": "General", @@ -204,7 +214,11 @@ "clearing-cache": "Clearing...", "clear-cache-description": "Clear cache if you encounter data sync or balance display problems. Neuron will rescan block data.", "show": "Show", - "hide": "Hide" + "hide": "Hide", + "version": "Version", + "language": "Language", + "cache-cleared-on": "Cache cleared on {{date}}", + "apply": "Apply" }, "wallet-manager": { "edit-wallet": { @@ -232,6 +246,12 @@ "mainnet": "Mainnet", "testnet": "Testnet", "devnet": "Devnet" + }, + "locale": { + "en": "English", + "en-US": "English(United States)", + "zh": "中文(简体)", + "zh-TW": "中文(繁體)" } }, "password-request": { @@ -272,7 +292,9 @@ "edit": "Edit", "delete": "Delete", "click-to-edit": "Click to edit", - "notice": "Notice" + "notice": "Notice", + "copy": "Copy", + "copied": "Copied" }, "notification-panel": { "title": "Notifications" @@ -361,6 +383,7 @@ "free": "Available", "locked": "Locked", "deposit": "Deposit", + "copy-balance": "Copy Balance", "deposit-records": "Deposits", "completed-records": "Completed", "apc": "Current APC", @@ -420,8 +443,7 @@ "checking-updates": "Checking...", "downloading-update": "Downloading update...", "update-not-available": "There are currently no updates available.", - "release-notes": "Release Notes:", - "updates-found-do-you-want-to-update": "An update ({{version}}) is available, do you want to download and install now?", + "updates-found-do-you-want-to-update": "An update ({{version}}) is available", "download-update": "Install Update", "updates-downloaded-about-to-quit-and-install": "Update downloaded. Ready to install and relaunch.", "quit-and-install": "Install and relaunch" @@ -450,6 +472,7 @@ "start-tomorrow": "Selected time should start from tomorrow." }, "sign-and-verify": { + "window-title": "Sign or Verify Message", "sign-or-verify-message": "Sign or Verify Message", "message": "Message", "address": "Address", diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 94796d98bc..fdec2ea6d1 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -16,7 +16,6 @@ "receive": "收款", "history": "交易歷史", "addresses": "地址管理", - "settings": "設定", "nervos-dao": "Nervos DAO", "special-assets": "自定義資產", "sync-not-start": "同步尚未開始", @@ -53,7 +52,8 @@ "failed": "失敗", "pending": "已提交", "confirming": "確認中" - } + }, + "copy-balance": "複製餘額" }, "wizard": { "welcome-to-nervos-neuron": "歡迎使用 Neuron", @@ -115,7 +115,7 @@ "fee": "交易費", "advanced-fee-settings": "交易費高級設定", "price": "單價", - "expected-speed": "預期成交速度", + "pick-price": "快速選擇單價", "total-cycles": "RISC-V Cycles 總數", "scan-screen-qr-code": "掃描荧幕上的二維碼", "set-locktime": "設置鎖定時間", @@ -123,7 +123,7 @@ "release-on": "鎖定至" }, "receive": { - "click-to-copy": "點擊複製地址", + "copy-address": "複製地址", "address-not-found": "未找到地址", "prompt": "為了保護隱私,Neuron 會自動選擇一個新收款地址。如果您想使用舊的收款地址,請訪問地址管理頁面。", "address-qrcode": "地址二維碼", @@ -155,15 +155,21 @@ "blockNumber": "區塊高度", "basic-information": "基本資訊", "search": { + "button": "蒐索", "placeholder": "使用交易雜湊、地址或日期(yyyy-mm-dd)進行蒐索" }, + "export-history": "導出交易歷史", "confirmations": "確認次數", "confirming-with-count": "已確認 {{confirmations}} 次", "view-on-explorer": "在瀏覽器中查看", "copy-transaction-hash": "複製交易雜湊", - "no-txs": "沒有交易記錄" + "no-txs": "沒有交易記錄", + "copy-tx-hash": "複製交易 Hash", + "copy-balance": "複製餘額", + "copy-address": "複製地址" }, "transaction": { + "window-title": "交易: {{hash}}", "date": "時間", "transaction-hash": "交易雜湊", "block-number": "區塊高度", @@ -193,6 +199,10 @@ "view-on-explorer": "在瀏覽器中查看" }, "settings": { + "title": { + "normal": "設定", + "mac": "偏好設置" + }, "go-to-overview": "返回總覽畫面", "setting-tabs": { "general": "通用", @@ -202,9 +212,13 @@ "general": { "clear-cache": "清空緩存", "clearing-cache": "清空中…", - "clear-cache-description": "當資料同步或顯示出現問題時,可以清空緩存,Neuron 會重新同步所有塊數據。", + "clear-cache-description": "當資料同步或餘額顯示出現問題時,可以清空緩存,Neuron 會重新同步所有塊數據。", "show": "顯示", - "hide": "隱藏" + "hide": "隱藏", + "version": "版本", + "language": "語言", + "cache-cleared-on": "上次清空緩存時間 {{date}}", + "apply": "應用" }, "wallet-manager": { "edit-wallet": { @@ -232,6 +246,12 @@ "mainnet": "主網", "testnet": "測試網", "devnet": "開發網" + }, + "locale": { + "en": "English", + "en-US": "English(United States)", + "zh": "中文(简体)", + "zh-TW": "中文(繁體)" } }, "password-request": { @@ -272,7 +292,9 @@ "edit": "編輯", "delete": "删除", "click-to-edit": "點擊編輯", - "notice": "注意事項" + "notice": "注意事項", + "copy": "複製", + "copied": "已複製" }, "notification-panel": { "title": "通知中心" @@ -361,6 +383,7 @@ "free": "當前可用", "locked": "已鎖定", "deposit": "存入", + "copy-balance": "複製餘額", "deposit-records": "存入", "completed-records": "已解鎖", "apc": "當前年化鎖定補貼率", @@ -420,8 +443,7 @@ "checking-updates": "正在檢查…", "downloading-update": "正在下載更新…", "update-not-available": "已經在使用最新版本", - "release-notes": "Release Notes:", - "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下載。要下載和安裝嗎?", + "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下載", "download-update": "下載安裝", "updates-downloaded-about-to-quit-and-install": "下載完成。請安裝新版本。", "quit-and-install": "安裝並重啓應用" @@ -450,6 +472,7 @@ "start-tomorrow": "所選時間不能早於明天。" }, "sign-and-verify": { + "window-title": "簽名/驗簽信息", "sign-or-verify-message": "簽名/驗簽信息", "message": "信息", "address": "地址", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 2b9ee60616..ce306c68a6 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -16,7 +16,6 @@ "receive": "收款", "history": "交易历史", "addresses": "地址管理", - "settings": "设置", "nervos-dao": "Nervos DAO", "special-assets": "自定义资产", "sync-not-start": "同步尚未开始", @@ -53,7 +52,8 @@ "failed": "失败", "pending": "已提交", "confirming": "确认中" - } + }, + "copy-balance": "复制余额" }, "wizard": { "welcome-to-nervos-neuron": "欢迎使用 Neuron", @@ -115,7 +115,7 @@ "fee": "交易费", "advanced-fee-settings": "交易费高级设置", "price": "单价", - "expected-speed": "预期成交速度", + "pick-price": " 快速选择单价", "total-cycles": "RISC-V Cycles 总数", "scan-screen-qr-code": "扫描屏幕上的二维码", "set-locktime": "设置锁定时间", @@ -123,7 +123,7 @@ "release-on": "锁定至" }, "receive": { - "click-to-copy": "点击复制地址", + "copy-address": "复制地址", "address-not-found": "未找到地址", "prompt": "为了保护隐私,Neuron 会自动选择一个新收款地址。如果您想使用旧的收款地址,请访问地址管理页面。", "address-qrcode": "地址二维码", @@ -155,15 +155,21 @@ "blockNumber": "区块高度", "basic-information": "基本信息", "search": { + "button": "搜索", "placeholder": "使用交易哈希、地址或日期(yyyy-mm-dd)进行搜索" }, + "export-history": "导出交易历史", "confirmations": "确认次数", "confirming-with-count": " 已确认 {{confirmations}} 次", "view-on-explorer": "在浏览器中查看", "copy-transaction-hash": "复制交易哈希", - "no-txs": "没有交易记录" + "no-txs": "没有交易记录", + "copy-tx-hash": "复制交易 Hash", + "copy-balance": "复制余额", + "copy-address": "复制地址" }, "transaction": { + "window-title": "交易: {{hash}}", "date": "时间", "transaction-hash": "交易哈希", "block-number": "区块高度", @@ -193,6 +199,10 @@ "view-on-explorer": "在浏览器中查看" }, "settings": { + "title": { + "normal": "设置", + "mac": "偏好设置" + }, "go-to-overview": "返回总览画面", "setting-tabs": { "general": "通用", @@ -202,9 +212,13 @@ "general": { "clear-cache": "清空缓存", "clearing-cache": "清空中...", - "clear-cache-description": "当数据同步或显示出现问题时,可以清空缓存,Neuron 会重新同步所有块数据。", + "clear-cache-description": "当数据同步或余额显示出现问题时,可以清空缓存,Neuron 会重新同步所有块数据。", "show": "显示", - "hide": "隐藏" + "hide": "隐藏", + "version": "版本", + "language": "语言", + "cache-cleared-on": "上次清空缓存时间 {{date}}", + "apply": "应用" }, "wallet-manager": { "edit-wallet": { @@ -232,6 +246,12 @@ "mainnet": "主网", "testnet": "测试网", "devnet": "开发网" + }, + "locale": { + "en": "English", + "en-US": "English(United States)", + "zh": "中文(简体)", + "zh-TW": "中文(繁體)" } }, "password-request": { @@ -272,7 +292,9 @@ "edit": "编辑", "delete": "删除", "click-to-edit": "点击编辑", - "notice": "注意事项" + "notice": "注意事项", + "copy": "复制", + "copied": "已复制" }, "notification-panel": { "title": "通知中心" @@ -361,6 +383,7 @@ "free": "当前可用", "locked": "已锁定", "deposit": "存入", + "copy-balance": "复制余额", "deposit-records": "存入", "completed-records": "已解锁", "apc": "当前年化锁定补贴率", @@ -420,8 +443,7 @@ "checking-updates": "正在检查...", "downloading-update": "正在下载更新...", "update-not-available": "已经在使用最新版本", - "release-notes": "Release Notes:", - "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下载。要下载和安装吗?", + "updates-found-do-you-want-to-update": "新版本 {{version}} 可供下载", "download-update": "下载安装", "updates-downloaded-about-to-quit-and-install": "下载完成。请安装新版本。", "quit-and-install": "安装并重启应用" @@ -450,6 +472,7 @@ "start-tomorrow": "所选时间不能早于明天。" }, "sign-and-verify": { + "window-title": "签名/验签信息", "sign-or-verify-message": "签名/验签信息", "message": "信息", "address": "地址", diff --git a/packages/neuron-ui/src/services/localCache.ts b/packages/neuron-ui/src/services/localCache.ts index 01fede29cc..e6bc7d0712 100644 --- a/packages/neuron-ui/src/services/localCache.ts +++ b/packages/neuron-ui/src/services/localCache.ts @@ -4,6 +4,7 @@ export enum LocalCacheKey { Wallets = 'wallets', CurrentWallet = 'currentWallet', CurrentNetworkID = 'currentNetworkID', + CacheClearDate = 'cacheClearDate', } export const addresses = { @@ -105,6 +106,16 @@ export const currentNetworkID = { }, } +export const cacheClearDate = { + save: (date: string) => { + window.localStorage.setItem(LocalCacheKey.CacheClearDate, date) + return true + }, + load: () => { + return window.localStorage.getItem(LocalCacheKey.CacheClearDate) ?? '' + }, +} + export default { LocalCacheKey, addresses, @@ -112,4 +123,5 @@ export default { wallets, currentWallet, currentNetworkID, + cacheClearDate, } diff --git a/packages/neuron-ui/src/services/remote/app.ts b/packages/neuron-ui/src/services/remote/app.ts index 898c69830f..8e7cf20167 100644 --- a/packages/neuron-ui/src/services/remote/app.ts +++ b/packages/neuron-ui/src/services/remote/app.ts @@ -1,8 +1,11 @@ +import { LOCALES } from 'utils/const' import { remoteApi } from './remoteApiWrapper' export const getSystemCodeHash = remoteApi('get-system-codehash') export const getNeuronWalletState = remoteApi('load-init-data') export const openInWindow = remoteApi('open-in-window') export const handleViewError = remoteApi('handle-view-error') +export const showSettings = remoteApi('show-settings') +export const setLocale = remoteApi('set-locale') export const clearCellCache = remoteApi('clear-cache') diff --git a/packages/neuron-ui/src/services/remote/index.ts b/packages/neuron-ui/src/services/remote/index.ts index abfd90f96f..027034acf2 100644 --- a/packages/neuron-ui/src/services/remote/index.ts +++ b/packages/neuron-ui/src/services/remote/index.ts @@ -11,11 +11,19 @@ const REMOTE_MODULE_NOT_FOUND = const LIMITED_TO_ELECTRON = 'This function is limited to Electron' export const getLocale = () => { - if (!window.remote) { + if (!window.ipcRenderer) { console.warn(REMOTE_MODULE_NOT_FOUND) return window.navigator.language } - return window.remote.require('electron').app.getLocale() + return window.ipcRenderer.sendSync('get-locale') +} + +export const getVersion = () => { + return window.remote?.app?.getVersion() ?? '' +} + +export const getPlatform = () => { + return window.remote?.process?.platform ?? 'Unknown' } export const getWinID = () => { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index c1f59e1f09..a3341635bb 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -1,9 +1,12 @@ -interface SuccessFromController { - status: 1 +import { ResponseCode, ErrorCode, isSuccessResponse } from 'utils' + +export interface SuccessFromController { + status: ResponseCode.SUCCESS result: any } -interface FailureFromController { - status: 0 | 105 + +export interface FailureFromController { + status: ResponseCode.FAILURE | ErrorCode.CapacityNotEnoughForChange | number message: | string | { @@ -15,7 +18,7 @@ interface FailureFromController { export type ControllerResponse = SuccessFromController | FailureFromController export const RemoteNotLoadError = { - status: 0 as 0, + status: ResponseCode.FAILURE, message: { content: 'The remote module is not found, please make sure the UI is running inside the Electron App', }, @@ -29,6 +32,8 @@ type Action = | 'load-init-data' | 'open-in-window' | 'handle-view-error' + | 'show-settings' + | 'set-locale' // Wallets | 'get-all-wallets' | 'get-current-wallet' @@ -54,6 +59,7 @@ type Action = | 'get-transaction' | 'show-transaction-details' | 'update-transaction-description' + | 'export-transactions' // Dao | 'get-dao-data' | 'generate-dao-deposit-tx' @@ -81,7 +87,7 @@ export const remoteApi = (action: Action) => async (params: T): Promise const res: SuccessFromController | FailureFromController = await window.ipcRenderer .invoke(action, params) .catch(() => ({ - status: 0, + status: ResponseCode.FAILURE, message: { content: 'Invalid response format', }, @@ -96,20 +102,20 @@ export const remoteApi = (action: Action) => async (params: T): Promise if (!res) { return { - status: 1, + status: ResponseCode.SUCCESS, result: null, } } - if (res.status === 1) { + if (isSuccessResponse(res)) { return { - status: 1, + status: ResponseCode.SUCCESS, result: res.result || null, } } return { - status: res.status || 0, + status: res.status || ResponseCode.FAILURE, message: typeof res.message === 'string' ? { content: res.message } : res.message || '', } } diff --git a/packages/neuron-ui/src/services/remote/transactions.ts b/packages/neuron-ui/src/services/remote/transactions.ts index 63781e01ec..765d966f98 100644 --- a/packages/neuron-ui/src/services/remote/transactions.ts +++ b/packages/neuron-ui/src/services/remote/transactions.ts @@ -12,4 +12,5 @@ export const getTransaction = remoteApi<{ walletID: string; hash: string }>('get export const updateTransactionDescription = remoteApi( 'update-transaction-description' ) +export const exportTransactions = remoteApi('export-transactions') export const showTransactionDetails = remoteApi('show-transaction-details') diff --git a/packages/neuron-ui/src/services/subjects.ts b/packages/neuron-ui/src/services/subjects.ts index c16e877765..09d5d44129 100644 --- a/packages/neuron-ui/src/services/subjects.ts +++ b/packages/neuron-ui/src/services/subjects.ts @@ -1,3 +1,7 @@ +import { CONSTANTS } from 'utils' + +const { LOCALES } = CONSTANTS + const FallbackSubject = { subscribe: (args: any) => { console.warn('The remote module is not found, please make sure the UI is running inside the Electron App') @@ -21,6 +25,8 @@ const SubjectConstructor = ( | 'synced-block-number-updated' | 'command' | 'app-updater-updated' + | 'navigation' + | 'set-locale' ) => { return window.ipcRenderer ? { @@ -46,6 +52,8 @@ export const ConnectionStatus = SubjectConstructor('co export const SyncedBlockNumber = SubjectConstructor('synced-block-number-updated') export const AppUpdater = SubjectConstructor('app-updater-updated') export const Command = SubjectConstructor('command') +export const Navigation = SubjectConstructor('navigation') +export const SetLocale = SubjectConstructor('set-locale') export default { DataUpdate, @@ -57,4 +65,6 @@ export default { SyncedBlockNumber, AppUpdater, Command, + Navigation, + SetLocale, } diff --git a/packages/neuron-ui/src/states/index.ts b/packages/neuron-ui/src/states/index.ts new file mode 100644 index 0000000000..771992778a --- /dev/null +++ b/packages/neuron-ui/src/states/index.ts @@ -0,0 +1,4 @@ +export { initStates } from './init' +export * from './stateProvider/provider' +export * from './stateProvider/reducer' +export * from './stateProvider/actionCreators' diff --git a/packages/neuron-ui/src/states/initStates/app.ts b/packages/neuron-ui/src/states/init/app.ts similarity index 88% rename from packages/neuron-ui/src/states/initStates/app.ts rename to packages/neuron-ui/src/states/init/app.ts index 0676463994..a3f586cd66 100644 --- a/packages/neuron-ui/src/states/initStates/app.ts +++ b/packages/neuron-ui/src/states/init/app.ts @@ -1,4 +1,6 @@ -import { CapacityUnit } from 'utils/const' +import { CapacityUnit, CONSTANTS } from 'utils' + +const { INIT_SEND_PRICE } = CONSTANTS const appState: Readonly = { tipBlockNumber: '', @@ -17,14 +19,13 @@ const appState: Readonly = { date: undefined, }, ], - price: '1000', + price: INIT_SEND_PRICE, description: '', generatedTx: '', }, passwordRequest: { actionType: null, walletID: '', - password: '', }, messages: { networks: null, @@ -41,7 +42,6 @@ const appState: Readonly = { sending: false, addressList: false, transactionList: false, - network: false, }, showTopAlert: false, showAllNotifications: false, diff --git a/packages/neuron-ui/src/states/initStates/chain.ts b/packages/neuron-ui/src/states/init/chain.ts similarity index 93% rename from packages/neuron-ui/src/states/initStates/chain.ts rename to packages/neuron-ui/src/states/init/chain.ts index de834e11f9..f77bb02ad7 100644 --- a/packages/neuron-ui/src/states/initStates/chain.ts +++ b/packages/neuron-ui/src/states/init/chain.ts @@ -1,5 +1,5 @@ import { currentNetworkID } from 'services/localCache' -import { ConnectionStatus } from 'utils/const' +import { ConnectionStatus } from 'utils' export const transactionState: Readonly = { value: '', diff --git a/packages/neuron-ui/src/states/initStates/index.ts b/packages/neuron-ui/src/states/init/index.ts similarity index 61% rename from packages/neuron-ui/src/states/initStates/index.ts rename to packages/neuron-ui/src/states/init/index.ts index f393cf795e..5a09206282 100644 --- a/packages/neuron-ui/src/states/initStates/index.ts +++ b/packages/neuron-ui/src/states/init/index.ts @@ -5,14 +5,7 @@ import settings from './settings' import nervosDAO from './nervosDAO' import updater from './updater' -export * from './app' -export * from './chain' -export * from './wallet' -export * from './settings' -export * from './nervosDAO' -export * from './updater' - -const initStates = { +export const initStates = { app, chain, wallet, diff --git a/packages/neuron-ui/src/states/initStates/nervosDAO.ts b/packages/neuron-ui/src/states/init/nervosDAO.ts similarity index 100% rename from packages/neuron-ui/src/states/initStates/nervosDAO.ts rename to packages/neuron-ui/src/states/init/nervosDAO.ts diff --git a/packages/neuron-ui/src/states/initStates/settings.ts b/packages/neuron-ui/src/states/init/settings.ts similarity index 100% rename from packages/neuron-ui/src/states/initStates/settings.ts rename to packages/neuron-ui/src/states/init/settings.ts diff --git a/packages/neuron-ui/src/states/initStates/updater.ts b/packages/neuron-ui/src/states/init/updater.ts similarity index 100% rename from packages/neuron-ui/src/states/initStates/updater.ts rename to packages/neuron-ui/src/states/init/updater.ts diff --git a/packages/neuron-ui/src/states/initStates/wallet.ts b/packages/neuron-ui/src/states/init/wallet.ts similarity index 100% rename from packages/neuron-ui/src/states/initStates/wallet.ts rename to packages/neuron-ui/src/states/init/wallet.ts diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts index a236af2a3e..569d966e86 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts @@ -1,9 +1,8 @@ import { NeuronWalletActions, AppActions, StateDispatch } from 'states/stateProvider/reducer' import { getNeuronWalletState } from 'services/remote' -import initStates from 'states/initStates' -import { Routes, ErrorCode } from 'utils/const' +import initStates from 'states/init' +import { RoutePath, ErrorCode, addressesToBalance, isSuccessResponse } from 'utils' import { WalletWizardPath } from 'components/WalletWizard' -import { addressesToBalance } from 'utils/formatters' import { wallets as walletsCache, addresses as addressesCache, @@ -15,7 +14,7 @@ import { export const initAppState = () => (dispatch: StateDispatch, history: any) => { getNeuronWalletState() .then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { const { wallets = [], currentWallet: wallet = initStates.wallet, @@ -39,9 +38,9 @@ export const initAppState = () => (dispatch: StateDispatch, history: any) => { }, }) if (!wallet) { - history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) + history.push(`${RoutePath.WalletWizard}${WalletWizardPath.Welcome}`) } else { - history.push(Routes.Overview) + history.push(RoutePath.Overview) } currentWalletCache.save(wallet) @@ -50,11 +49,11 @@ export const initAppState = () => (dispatch: StateDispatch, history: any) => { networksCache.save(networks) currentNetworkIDCache.save(currentNetworkID) } else { - history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) + history.push(`${RoutePath.WalletWizard}${WalletWizardPath.Welcome}`) } }) .catch(() => { - history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) + history.push(`${RoutePath.WalletWizard}${WalletWizardPath.Welcome}`) }) } @@ -130,16 +129,3 @@ export const toggleIsAllowedToFetchList = (allowed?: boolean) => (dispatch: Stat payload: allowed, }) } - -export default { - initAppState, - addNotification, - addPopup, - dismissGlobalDialog, - dismissNotification, - showAlertDialog, - dismissAlertDialog, - toggleTopAlertVisibility, - toggleAllNotificationVisibility, - toggleIsAllowedToFetchList, -} diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/index.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/index.ts index b1ea341f64..4705714c26 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/index.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/index.ts @@ -1,17 +1,4 @@ -import app from './app' -import wallets from './wallets' -import transactions from './transactions' -import settings from './settings' - export * from './app' export * from './wallets' export * from './transactions' export * from './settings' -export const actionCreators = { - ...app, - ...wallets, - ...transactions, - ...settings, -} - -export default actionCreators diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts index 41e2c4eb18..bd3294c0b2 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts @@ -1,6 +1,5 @@ import { createNetwork as createRemoteNetwork, updateNetwork as updateRemoteNetwork } from 'services/remote' -import { Routes } from 'utils/const' -import { failureResToNotification } from 'utils/formatters' +import { RoutePath, failureResToNotification } from 'utils' import { addNotification, addPopup } from './app' import { AppActions, StateDispatch } from '../reducer' @@ -16,7 +15,7 @@ export const createNetwork = (params: Controller.CreateNetworkParams) => (dispat .then(res => { if (res.status === 1) { addPopup('create-network-successfully')(dispatch) - history.push(Routes.SettingsNetworks) + history.push(RoutePath.SettingsNetworks) } else { addNotification(failureResToNotification(res))(dispatch) } @@ -32,32 +31,13 @@ export const createNetwork = (params: Controller.CreateNetworkParams) => (dispat } export const updateNetwork = (params: Controller.UpdateNetworkParams) => (dispatch: StateDispatch, history: any) => { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { - network: true, - }, + return updateRemoteNetwork(params).then(res => { + if (res.status === 1) { + addPopup('update-network-successfully')(dispatch) + history.push(RoutePath.SettingsNetworks) + } else { + addNotification(failureResToNotification(res))(dispatch) + } + return res.status }) - updateRemoteNetwork(params) - .then(res => { - if (res.status === 1) { - addPopup('update-network-successfully')(dispatch) - history.push(Routes.SettingsNetworks) - } else { - addNotification(failureResToNotification(res))(dispatch) - } - }) - .finally(() => { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { - network: false, - }, - }) - }) -} - -export default { - createNetwork, - updateNetwork, } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts index 5eda61b6c8..a3082716a7 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts @@ -4,12 +4,12 @@ import { getTransactionList, updateTransactionDescription as updateRemoteTransactionDescription, } from 'services/remote' -import { failureResToNotification } from 'utils/formatters' +import { failureResToNotification, isSuccessResponse } from 'utils' import { addNotification } from './app' export const updateTransactionList = (params: GetTransactionListParams) => (dispatch: StateDispatch) => { getTransactionList(params).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: NeuronWalletActions.UpdateTransactionList, payload: res.result, @@ -43,7 +43,3 @@ export const updateTransactionDescription = (params: Controller.UpdateTransactio } }) } - -export default { - updateTransactionList, -} diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts index 16ec10b48a..f66c02f10b 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts @@ -11,22 +11,17 @@ import { deleteWallet as deleteRemoteWallet, backupWallet as backupRemoteWallet, } from 'services/remote' -import { emptyWallet } from 'states/initStates/wallet' -import { emptyNervosDaoData } from 'states/initStates/nervosDAO' -import { WalletWizardPath } from 'components/WalletWizard' +import { emptyWallet } from 'states/init/wallet' +import { emptyNervosDaoData } from 'states/init/nervosDAO' import { wallets as walletsCache, currentWallet as currentWalletCache } from 'services/localCache' -import { Routes, ErrorCode } from 'utils/const' -import { addressesToBalance, failureResToNotification } from 'utils/formatters' +import { ErrorCode, ResponseCode, addressesToBalance, failureResToNotification, isSuccessResponse } from 'utils' import { NeuronWalletActions } from '../reducer' import { addNotification, addPopup } from './app' -export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) => { - getCurrentWallet().then(res => { - if (res.status === 1) { +export const updateCurrentWallet = () => (dispatch: StateDispatch) => { + return getCurrentWallet().then(res => { + if (isSuccessResponse(res)) { const payload = res.result || emptyWallet - if (!payload || !payload.id) { - history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) - } dispatch({ type: NeuronWalletActions.UpdateCurrentWallet, payload, @@ -35,45 +30,36 @@ export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) } else { addNotification(failureResToNotification(res))(dispatch) } + return !!(res as any)?.result?.id }) } -export const updateWalletList = () => (dispatch: StateDispatch, history: any) => { - getWalletList().then(res => { - if (res.status === 1) { +export const updateWalletList = () => (dispatch: StateDispatch) => { + return getWalletList().then(res => { + if (isSuccessResponse(res)) { const payload = res.result || [] - if (!payload.length) { - history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) - } - dispatch({ - type: NeuronWalletActions.UpdateWalletList, - payload, - }) + dispatch({ type: NeuronWalletActions.UpdateWalletList, payload }) walletsCache.save(payload) } else { addNotification(failureResToNotification(res))(dispatch) } + return !!(res as any)?.result?.length }) } -export const updateWalletProperty = (params: Controller.UpdateWalletParams) => ( - dispatch: StateDispatch, - history?: any -) => { - updateWallet(params).then(res => { - if (res.status === 1) { +export const updateWalletProperty = (params: Controller.UpdateWalletParams) => (dispatch: StateDispatch) => { + return updateWallet(params).then(res => { + if (isSuccessResponse(res)) { addPopup('update-wallet-successfully')(dispatch) - if (history) { - history.push(Routes.SettingsWallets) - } } else { addNotification(failureResToNotification(res))(dispatch) } + return res.status }) } export const setCurrentWallet = (id: string) => (dispatch: StateDispatch) => { setRemoteCurrentWallet(id).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: AppActions.Ignore, payload: null, @@ -84,70 +70,46 @@ export const setCurrentWallet = (id: string) => (dispatch: StateDispatch) => { }) } -export const sendTransaction = (params: Controller.SendTransactionParams) => ( - dispatch: StateDispatch, - history: any, - options?: { - type: 'unlock' - } -) => { +export const sendTransaction = (params: Controller.SendTransactionParams) => async (dispatch: StateDispatch) => { dispatch({ type: AppActions.UpdateLoadings, payload: { sending: true, }, }) - setTimeout(() => { - sendTx(params) - .then(res => { - if (res.status === 1) { - dispatch({ - type: AppActions.ClearNotificationsOfCode, - payload: ErrorCode.PasswordIncorrect, - }) - if (options && options.type === 'unlock') { - dispatch({ - type: AppActions.SetGlobalDialog, - payload: 'unlock-success', - }) - } else { - history.push(Routes.History) - } - } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.status, - content: (typeof res.message === 'string' ? res.message : res.message.content || '').replace( - /(\b"|"\b)/g, - '' - ), - meta: typeof res.message === 'string' ? undefined : res.message.meta, - })(dispatch) - } - dispatch({ - type: AppActions.DismissPasswordRequest, - }) - }) - .catch(err => { - console.warn(err) - }) - .finally(() => { - dispatch({ - type: AppActions.UpdateLoadings, - payload: { - sending: false, - }, - }) + try { + const res = await sendTx(params) + if (isSuccessResponse(res)) { + dispatch({ type: AppActions.DismissPasswordRequest }) + } else if (res.status !== ErrorCode.PasswordIncorrect) { + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.status, + content: typeof res.message === 'string' ? res.message : res.message.content, + meta: typeof res.message === 'string' ? undefined : res.message.meta, + })(dispatch) + dispatch({ + type: AppActions.DismissPasswordRequest, }) - }, 0) + } + return res.status + } catch (err) { + console.warn(err) + return ResponseCode.FAILURE + } finally { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + } } export const updateAddressListAndBalance = (params: Controller.GetAddressesByWalletIDParams) => ( dispatch: StateDispatch ) => { getAddressesByWalletID(params).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { const addresses = res.result || [] const balance = addressesToBalance(addresses) dispatch({ @@ -163,16 +125,13 @@ export const updateAddressListAndBalance = (params: Controller.GetAddressesByWal export const updateAddressDescription = (params: Controller.UpdateAddressDescriptionParams) => ( dispatch: StateDispatch ) => { - const descriptionParams = { - address: params.address, - description: params.description, - } + const descriptionParams = { address: params.address, description: params.description } dispatch({ type: NeuronWalletActions.UpdateAddressDescription, payload: descriptionParams, }) updateRemoteAddressDescription(params).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: NeuronWalletActions.UpdateAddressDescription, payload: descriptionParams, @@ -183,42 +142,65 @@ export const updateAddressDescription = (params: Controller.UpdateAddressDescrip }) } -export const deleteWallet = (params: Controller.DeleteWalletParams) => (dispatch: StateDispatch) => { +export const deleteWallet = (params: Controller.DeleteWalletParams) => async (dispatch: StateDispatch) => { dispatch({ - type: AppActions.DismissPasswordRequest, + type: AppActions.UpdateLoadings, + payload: { sending: true }, }) - deleteRemoteWallet(params).then(res => { - if (res.status === 1) { - addPopup('delete-wallet-successfully')(dispatch) + try { + const res = await deleteRemoteWallet(params) + if (res.status !== ErrorCode.PasswordIncorrect) { dispatch({ - type: AppActions.ClearNotificationsOfCode, - payload: ErrorCode.PasswordIncorrect, + type: AppActions.DismissPasswordRequest, }) - } else { - addNotification(failureResToNotification(res))(dispatch) + if (isSuccessResponse(res)) { + addPopup('delete-wallet-successfully')(dispatch) + } else { + addNotification(failureResToNotification(res))(dispatch) + } } - }) + return res.status + } catch (err) { + console.warn(err) + return 0 + } finally { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + } } -export const backupWallet = (params: Controller.BackupWalletParams) => (dispatch: StateDispatch) => { +export const backupWallet = (params: Controller.BackupWalletParams) => async (dispatch: StateDispatch) => { dispatch({ - type: AppActions.DismissPasswordRequest, + type: AppActions.UpdateLoadings, + payload: { sending: true }, }) - backupRemoteWallet(params).then(res => { - if (res.status === 1) { + try { + const res = await backupRemoteWallet(params) + if (res.status !== ErrorCode.PasswordIncorrect) { dispatch({ - type: AppActions.ClearNotificationsOfCode, - payload: ErrorCode.PasswordIncorrect, + type: AppActions.DismissPasswordRequest, }) - } else { - addNotification(failureResToNotification(res))(dispatch) + if (!isSuccessResponse(res)) { + addNotification(failureResToNotification(res))(dispatch) + } } - }) + return res.status + } catch (err) { + console.warn(err) + return 0 + } finally { + dispatch({ + type: AppActions.UpdateLoadings, + payload: { sending: false }, + }) + } } export const updateNervosDaoData = (walletID: Controller.GetNervosDaoDataParams) => (dispatch: StateDispatch) => { getDaoData(walletID).then(res => { - if (res.status === 1) { + if (isSuccessResponse(res)) { dispatch({ type: NeuronWalletActions.UpdateNervosDaoData, payload: { records: res.result }, @@ -235,16 +217,3 @@ export const clearNervosDaoData = () => (dispatch: StateDispatch) => { payload: emptyNervosDaoData, }) } - -export default { - updateCurrentWallet, - updateWalletList, - updateWallet, - setCurrentWallet, - sendTransaction, - updateAddressListAndBalance, - updateAddressDescription, - deleteWallet, - backupWallet, - updateNervosDaoData, -} diff --git a/packages/neuron-ui/src/states/stateProvider/index.tsx b/packages/neuron-ui/src/states/stateProvider/provider.tsx similarity index 83% rename from packages/neuron-ui/src/states/stateProvider/index.tsx rename to packages/neuron-ui/src/states/stateProvider/provider.tsx index 607023ba25..51c6678942 100644 --- a/packages/neuron-ui/src/states/stateProvider/index.tsx +++ b/packages/neuron-ui/src/states/stateProvider/provider.tsx @@ -1,5 +1,5 @@ import React, { createContext, useReducer, useContext } from 'react' -import initStates from 'states/initStates' +import initStates from 'states/init' import { StateDispatch, reducer } from './reducer' const basicDispatch = console.info @@ -9,7 +9,7 @@ export const NeuronWalletContext = createContext<{ state: State.AppWithNeuronWal dispatch: basicDispatch, }) -const withProviders = (Comp: React.ComponentType) => (props: React.Props) => { +export const withProvider = (Comp: React.ComponentType) => (props: React.Props) => { const [providers, dispatch] = useReducer(reducer, initStates) Object.defineProperty(Comp, 'displayName', { @@ -26,4 +26,4 @@ const withProviders = (Comp: React.ComponentType) => (props: React.Props) = export const useState = () => useContext(NeuronWalletContext).state export const useDispatch = () => useContext(NeuronWalletContext).dispatch -export default withProviders +export default withProvider diff --git a/packages/neuron-ui/src/states/stateProvider/reducer.ts b/packages/neuron-ui/src/states/stateProvider/reducer.ts index fc93ba77f3..478431d23a 100644 --- a/packages/neuron-ui/src/states/stateProvider/reducer.ts +++ b/packages/neuron-ui/src/states/stateProvider/reducer.ts @@ -1,6 +1,6 @@ import produce, { Draft } from 'immer' -import initStates from 'states/initStates' -import { ConnectionStatus, ErrorCode } from 'utils/const' +import initStates from 'states/init' +import { ConnectionStatus, ErrorCode } from 'utils' export enum NeuronWalletActions { InitAppState = 'initAppState', @@ -41,7 +41,6 @@ export enum AppActions { CleanTransactions = 'cleanTransactions', RequestPassword = 'requestPassword', DismissPasswordRequest = 'dismissPasswordRequest', - UpdatePassword = 'updatePassword', UpdateChainInfo = 'updateChainInfo', UpdateLoadings = 'updateLoadings', UpdateAlertDialog = 'updateAlertDialog', @@ -72,7 +71,6 @@ export type StateAction = | { type: AppActions.CleanTransactions } | { type: AppActions.RequestPassword; payload: Omit } | { type: AppActions.DismissPasswordRequest } - | { type: AppActions.UpdatePassword; payload: string } | { type: AppActions.UpdateChainInfo; payload: Partial } | { type: AppActions.UpdateLoadings; payload: any } | { type: AppActions.UpdateAlertDialog; payload: State.AlertDialog } @@ -236,17 +234,13 @@ export const reducer = produce((state: Draft, action: break } case AppActions.RequestPassword: { - state.app.passwordRequest = { ...action.payload, password: '' } + state.app.passwordRequest = action.payload break } case AppActions.DismissPasswordRequest: { state.app.passwordRequest = initStates.app.passwordRequest break } - case AppActions.UpdatePassword: { - state.app.passwordRequest.password = action.payload - break - } case AppActions.UpdateMessage: { /** * payload: {type,content, timestamp} diff --git a/packages/neuron-ui/src/stories/Addresses.stories.tsx b/packages/neuron-ui/src/stories/Addresses.stories.tsx index cecbe1192c..87a7841870 100644 --- a/packages/neuron-ui/src/stories/Addresses.stories.tsx +++ b/packages/neuron-ui/src/stories/Addresses.stories.tsx @@ -5,8 +5,7 @@ import StoryRouter from 'storybook-react-router' import { withKnobs, text, number } from '@storybook/addon-knobs' import { action } from '@storybook/addon-actions' import Addresses from 'components/Addresses' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' import addressesStates from './data/addresses' const stories = storiesOf('Addresses', module).addDecorator(StoryRouter()) diff --git a/packages/neuron-ui/src/stories/BalanceSyncIcon.stories.tsx b/packages/neuron-ui/src/stories/BalanceSyncIcon.stories.tsx index 3523605dc1..109b5280e0 100644 --- a/packages/neuron-ui/src/stories/BalanceSyncIcon.stories.tsx +++ b/packages/neuron-ui/src/stories/BalanceSyncIcon.stories.tsx @@ -1,6 +1,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import { ConnectionStatus, SyncStatus } from 'utils/const' +import { ConnectionStatus, SyncStatus } from 'utils' import BalanceSyncIcon, { BalanceSyncIconProps } from 'components/BalanceSyncingIcon' const stories = storiesOf('Balance Sync Icon', module) diff --git a/packages/neuron-ui/src/stories/CopyZone.stories.tsx b/packages/neuron-ui/src/stories/CopyZone.stories.tsx new file mode 100644 index 0000000000..c5dfb92bb5 --- /dev/null +++ b/packages/neuron-ui/src/stories/CopyZone.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import CopyZone from 'widgets/CopyZone' + +const stories = storiesOf('Copy Zone', module) + +stories.add('Basic', () => { + return ( + + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + ) +}) diff --git a/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx b/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx index a8823d6ac1..f84b7b4236 100644 --- a/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx +++ b/packages/neuron-ui/src/stories/GeneralSetting.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { withKnobs } from '@storybook/addon-knobs' import GeneralSetting from 'components/GeneralSetting' -import initStates from 'states/initStates' +import { initStates } from 'states' const states: { [title: string]: boolean } = { 'Clear cell cache on': true, diff --git a/packages/neuron-ui/src/stories/History.stories.tsx b/packages/neuron-ui/src/stories/History.stories.tsx index 3f0365780a..b957545fb9 100644 --- a/packages/neuron-ui/src/stories/History.stories.tsx +++ b/packages/neuron-ui/src/stories/History.stories.tsx @@ -4,8 +4,7 @@ import StoryRouter from 'storybook-react-router' import { withKnobs, text, number, boolean } from '@storybook/addon-knobs' import { action } from '@storybook/addon-actions' import History from 'components/History' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' import transactions from './data/transactions' const dispatch = (a: any) => action('Dispatch')(JSON.stringify(a, null, 2)) diff --git a/packages/neuron-ui/src/stories/Navbar.stories.tsx b/packages/neuron-ui/src/stories/Navbar.stories.tsx index 91a6e9559a..308333c560 100644 --- a/packages/neuron-ui/src/stories/Navbar.stories.tsx +++ b/packages/neuron-ui/src/stories/Navbar.stories.tsx @@ -3,8 +3,7 @@ import { storiesOf } from '@storybook/react' import StoryRouter from 'storybook-react-router' import { action } from '@storybook/addon-actions' import Navbar from 'containers/Navbar' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' const wallets: State.WalletIdentity[] = [{ id: 'wallet id', name: 'wallet name' }] diff --git a/packages/neuron-ui/src/stories/NervosDAO.stories.tsx b/packages/neuron-ui/src/stories/NervosDAO.stories.tsx index 8fb807d864..45e09807f6 100644 --- a/packages/neuron-ui/src/stories/NervosDAO.stories.tsx +++ b/packages/neuron-ui/src/stories/NervosDAO.stories.tsx @@ -4,8 +4,7 @@ import { storiesOf } from '@storybook/react' import { withKnobs, text } from '@storybook/addon-knobs' import { action } from '@storybook/addon-actions' import NervosDAO from 'components/NervosDAO' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' import transactions from './data/transactions' import addresses from './data/addresses' diff --git a/packages/neuron-ui/src/stories/NetworkEditor.stories.tsx b/packages/neuron-ui/src/stories/NetworkEditor.stories.tsx new file mode 100644 index 0000000000..d62297b7ec --- /dev/null +++ b/packages/neuron-ui/src/stories/NetworkEditor.stories.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import StoryRouter from 'storybook-react-router' +import { action } from '@storybook/addon-actions' +import NetworkEditor from 'components/NetworkEditor' +import { initStates, NeuronWalletContext } from 'states' + +const stories = storiesOf('Settings', module).addDecorator(StoryRouter()) + +const dispatch = (dispatchAction: any) => action('dispatch')(dispatchAction) + +stories.add('Network Editor', () => { + return ( + + + + ) +}) diff --git a/packages/neuron-ui/src/stories/NetworkSetting.stories.tsx b/packages/neuron-ui/src/stories/NetworkSetting.stories.tsx index a8336c2ecc..ce7cda1263 100644 --- a/packages/neuron-ui/src/stories/NetworkSetting.stories.tsx +++ b/packages/neuron-ui/src/stories/NetworkSetting.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import StoryRouter from 'storybook-react-router' import NetworkSetting from 'components/NetworkSetting' -import initStates from 'states/initStates' +import { initStates } from 'states' const states: { [title: string]: State.Network[] } = { 'Empty List': [], diff --git a/packages/neuron-ui/src/stories/NetworkStatus.stories.tsx b/packages/neuron-ui/src/stories/NetworkStatus.stories.tsx index c185ab0d89..62eb5540ad 100644 --- a/packages/neuron-ui/src/stories/NetworkStatus.stories.tsx +++ b/packages/neuron-ui/src/stories/NetworkStatus.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { withKnobs, text, number } from '@storybook/addon-knobs' import NetworkStatus, { NetworkStatusProps } from 'components/NetworkStatus' -import { SyncStatus } from 'utils/const' +import { SyncStatus } from 'utils' const states: { [index: string]: NetworkStatusProps } = { Online: { diff --git a/packages/neuron-ui/src/stories/Overview.stories.tsx b/packages/neuron-ui/src/stories/Overview.stories.tsx index 008042a3ca..c73c96b726 100644 --- a/packages/neuron-ui/src/stories/Overview.stories.tsx +++ b/packages/neuron-ui/src/stories/Overview.stories.tsx @@ -4,8 +4,7 @@ import { withKnobs, text, boolean } from '@storybook/addon-knobs' import StoryRouter from 'storybook-react-router' import { action } from '@storybook/addon-actions' import Overview from 'components/Overview' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' import transactions from './data/transactions' import addresses from './data/addresses' diff --git a/packages/neuron-ui/src/stories/PasswordRequest.stories.tsx b/packages/neuron-ui/src/stories/PasswordRequest.stories.tsx index 3777106d23..c00120e364 100644 --- a/packages/neuron-ui/src/stories/PasswordRequest.stories.tsx +++ b/packages/neuron-ui/src/stories/PasswordRequest.stories.tsx @@ -3,8 +3,7 @@ import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' import StoryRouter from 'storybook-react-router' import PasswordRequest from 'components/PasswordRequest' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' const dispatch = action('Dispatch') @@ -16,7 +15,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'delete', - password: '', }, }, }, @@ -31,7 +29,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'delete', - password: '', }, }, }, @@ -46,7 +43,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'delete', - password: '123456', }, }, }, @@ -61,7 +57,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'backup', - password: '', }, }, }, @@ -76,7 +71,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'backup', - password: '123456', }, }, }, @@ -91,7 +85,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'unlock', - password: '', }, }, }, @@ -106,7 +99,6 @@ const states: { [title: string]: State.AppWithNeuronWallet } = { passwordRequest: { walletID: '1', actionType: 'send', - password: '123456', }, }, }, diff --git a/packages/neuron-ui/src/stories/PropertyList.stories.tsx b/packages/neuron-ui/src/stories/PropertyList.stories.tsx deleted file mode 100644 index 39ceae5bd6..0000000000 --- a/packages/neuron-ui/src/stories/PropertyList.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { storiesOf } from '@storybook/react' -import PropertyList, { Property, CellStyles } from 'widgets/PropertyList' - -const states: { [title: string]: { properties: Property[] } } = { - basic: { - properties: Array.from({ length: 15 }, (_, idx) => ({ - label: `Property ${idx}`.repeat((idx % 5) + 1), - value: `Value of property ${idx}`.repeat((idx % 5) + 1), - })), - }, -} - -const cellStyles: CellStyles = { - labelWidth: '100px', - valueWidth: '200px', -} - -const stories = storiesOf('Property List', module) - -Object.entries(states).forEach(([title, props]) => { - stories.add(title, () => { - return - }) -}) diff --git a/packages/neuron-ui/src/stories/Receive.stories.tsx b/packages/neuron-ui/src/stories/Receive.stories.tsx index 5b708021bf..af0b6603fc 100644 --- a/packages/neuron-ui/src/stories/Receive.stories.tsx +++ b/packages/neuron-ui/src/stories/Receive.stories.tsx @@ -4,8 +4,7 @@ import { storiesOf } from '@storybook/react' import { withKnobs, text, number } from '@storybook/addon-knobs' import { action } from '@storybook/addon-actions' import Receive from 'components/Receive' -import initStates from 'states/initStates' -import { NeuronWalletContext } from 'states/stateProvider' +import { initStates, NeuronWalletContext } from 'states' import addresses from './data/addresses' const dispatch = action('Dispatch') diff --git a/packages/neuron-ui/src/stories/SettingTabs.stories.tsx b/packages/neuron-ui/src/stories/SettingTabs.stories.tsx new file mode 100644 index 0000000000..2270b7f921 --- /dev/null +++ b/packages/neuron-ui/src/stories/SettingTabs.stories.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import StoryRouter from 'storybook-react-router' +import { action } from '@storybook/addon-actions' +import SettingTabs from 'components/SettingTabs' +import { initStates, NeuronWalletContext } from 'states' + +const stories = storiesOf('Settings', module).addDecorator(StoryRouter()) + +const dispatch = (dispatchAction: any) => action('dispatch')(dispatchAction) + +stories.add('Setting Tabs', () => { + return ( + + + + ) +}) diff --git a/packages/neuron-ui/src/stories/WalletEditor.stories.tsx b/packages/neuron-ui/src/stories/WalletEditor.stories.tsx new file mode 100644 index 0000000000..cd9c23b7bf --- /dev/null +++ b/packages/neuron-ui/src/stories/WalletEditor.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import StoryRouter from 'storybook-react-router' +import { action } from '@storybook/addon-actions' +import WalletEditor from 'components/WalletEditor' +import { initStates, NeuronWalletContext } from 'states' + +const wallets: State.WalletIdentity[] = [{ id: 'wallet-id', name: 'wallet name' }] + +const stories = storiesOf('Settings', module).addDecorator(StoryRouter()) + +const dispatch = (dispatchAction: any) => action('dispatch')(dispatchAction) + +stories.add('Wallet Editor', () => { + return ( + + + + ) +}) diff --git a/packages/neuron-ui/src/stories/WalletSetting.stories.tsx b/packages/neuron-ui/src/stories/WalletSetting.stories.tsx index 3ab85c71bc..e7ae0ac5ce 100644 --- a/packages/neuron-ui/src/stories/WalletSetting.stories.tsx +++ b/packages/neuron-ui/src/stories/WalletSetting.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import StoryRouter from 'storybook-react-router' import WalletSetting from 'components/WalletSetting' -import initStates from 'states/initStates' +import { initStates } from 'states' const states: { [title: string]: State.WalletIdentity[] } = { 'Empty List': [], diff --git a/packages/neuron-ui/src/stories/index.tsx b/packages/neuron-ui/src/stories/index.tsx index 6e50376885..1a33c54c31 100644 --- a/packages/neuron-ui/src/stories/index.tsx +++ b/packages/neuron-ui/src/stories/index.tsx @@ -7,6 +7,9 @@ import './Overview.stories' import './Addresses.stories' import './History.stories' import './Receive.stories' +import './SettingTabs.stories' +import './WalletEditor.stories' +import './NetworkEditor.stories' import './TransactionList.stories' import './GeneralSetting.stories' import './WalletSetting.stories' @@ -14,7 +17,6 @@ import './NetworkSetting.stories' import './PasswordRequest.stories' import './TransactionFeePanel.stories' import './NetworkStatus.stories' -import './PropertyList.stories' import './AlertDialog.stories' import './NervosDAO.stories' import './DepositDialog.stories' @@ -31,3 +33,4 @@ import './CompensationProgressBar.stories' import './BalanceSyncIcon.stories' import './Navbar.stories' +import './CopyZone.stories' diff --git a/packages/neuron-ui/src/stories/styles.scss b/packages/neuron-ui/src/stories/styles.scss index 00218907c3..24bc0371bc 100644 --- a/packages/neuron-ui/src/stories/styles.scss +++ b/packages/neuron-ui/src/stories/styles.scss @@ -9,3 +9,7 @@ body { linear-gradient(90deg, rgba(255, 255, 255, 0.3) 1px, transparent 1px); background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px; } + +#root { + grid-area: 1/1/5/5; +} diff --git a/packages/neuron-ui/src/styles/index.scss b/packages/neuron-ui/src/styles/index.scss index e08f43a0e5..847fb1fe42 100755 --- a/packages/neuron-ui/src/styles/index.scss +++ b/packages/neuron-ui/src/styles/index.scss @@ -6,6 +6,7 @@ outline: none !important; -webkit-tap-highlight-color: none !important; cursor: default !important; + user-select: none; } [type='button'], @@ -25,7 +26,7 @@ html { } body { - @include RegularText; + @include regular-text; margin: 0; padding: 0; background: #f8f8f8; @@ -34,14 +35,19 @@ body { } .label { - padding: 3px 4px; - font-size: 12px; + display: flex; + justify-content: center; + align-items: center; + height: 18px; + width: 58px; + padding: 0 4px; + font-size: 9px; font-weight: 600; - line-height: 1; color: #fff; border-radius: 2px; box-shadow: inset 0 -1px 0 rgba(27, 31, 35, 0.12); text-transform: capitalize; + margin-left: 10px; &.primary { background-color: #6f42c1; @@ -63,10 +69,6 @@ body { } } -.monospacedFont { - font-family: monospace; -} - .ms-DetailsHeader-cell:hover { background-color: inherit; } @@ -164,6 +166,16 @@ body { } // hack fabric choice field +.ms-ChoiceField-field { + &::before { + top: 4px; + left: 3px; + } + &::after { + top: 9px !important; + left: 8px !important; + } +} .ms-ChoiceField-field.is-checked { &::before, &::after { diff --git a/packages/neuron-ui/src/styles/layout.scss b/packages/neuron-ui/src/styles/layout.scss index f90e8c4afa..57fa84e72d 100644 --- a/packages/neuron-ui/src/styles/layout.scss +++ b/packages/neuron-ui/src/styles/layout.scss @@ -1,4 +1,4 @@ -:root{ +:root { --sidebar-width: 200px; } @@ -25,7 +25,7 @@ footer { .main-content { grid-area: content; overflow: auto; - padding: 15px 30px; + padding: 30px 30px; } .notification { @@ -33,7 +33,7 @@ footer { z-index: 1; } -.dialog{ +.dialog { grid-area: 1/1/5/5; dialog { margin: 0; diff --git a/packages/neuron-ui/src/styles/mixin.scss b/packages/neuron-ui/src/styles/mixin.scss index 46f95a10f3..b4704d46b3 100644 --- a/packages/neuron-ui/src/styles/mixin.scss +++ b/packages/neuron-ui/src/styles/mixin.scss @@ -1,26 +1,26 @@ // font -@mixin BoldText { +@mixin bold-text { font-family: 'SourceCodePro-Regular', 'SourceHanSansCN-Regular', monospace; font-weight: 900; } -@mixin SemiBoldText { +@mixin semi-bold-text { font-family: 'SourceCodePro-Regular', 'SourceHanSansCN-Regular', monospace; font-weight: 700; } -@mixin MediumText { +@mixin medium-text { font-family: 'SourceCodePro-Regular', 'SourceHanSansCN-Regular', monospace; font-weight: 500; } -@mixin RegularText { +@mixin regular-text { font-family: 'SourceCodePro-Regular', 'SourceHanSansCN-Regular', monospace; font-weight: 400; } // layout -@mixin dialogContainer { +@mixin dialog-container { border: none; border-radius: 2px; box-shadow: 4px 7px 10px 0 rgba(0, 0, 0, 0.22); @@ -29,19 +29,19 @@ box-sizing: border-box; } -@mixin dialogTitle { - @include BoldText; +@mixin dialog-title { + @include bold-text; font-size: 1.125rem; letter-spacing: 0.9px; margin-bottom: 25px; } -@mixin dialogFooter { +@mixin dialog-footer { display: flex; justify-content: flex-end; } -@mixin dialogConfirmButton { +@mixin dialog-confirm-button { border: none; width: 5.125rem; height: 1.875rem; @@ -60,12 +60,12 @@ backdrop-filter: blur(1px); } -@mixin disabledBtn { +@mixin disabled-button { background-color: #e3e3e3; color: #666; } -@mixin formFooter { +@mixin form-footer { display: flex; justify-content: flex-end; align-items: center; @@ -80,7 +80,7 @@ } } -@mixin descriptionField { +@mixin description-field { display: flex; & > div { @@ -100,3 +100,18 @@ border-color: #333 !important; } } + +@mixin page-title { + @include bold-text; + font-size: 1.375rem; + color: #000; + padding: 0; + margin: 0 0 23px 0; +} + +@mixin text-overflow-ellipsis { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/packages/neuron-ui/src/tests/calculation/calculateAPC/index.test.ts b/packages/neuron-ui/src/tests/calculation/calculateAPC/index.test.ts index 7b41349e39..b111a7d8dd 100644 --- a/packages/neuron-ui/src/tests/calculation/calculateAPC/index.test.ts +++ b/packages/neuron-ui/src/tests/calculation/calculateAPC/index.test.ts @@ -2,12 +2,9 @@ import calculateAPC from 'utils/calculateAPC' import fixtures from './fixtures.json' describe('calculate the apc', () => { - const fixtureTable = Object.entries(fixtures).map(([title, { startYearNumber, endYearNumber, expected }]) => [ - title, - startYearNumber, - endYearNumber, - expected, - ]) + const fixtureTable: [string, number, number, number][] = Object.entries( + fixtures + ).map(([title, { startYearNumber, endYearNumber, expected }]) => [title, startYearNumber, endYearNumber, expected]) test.each(fixtureTable)(`%s`, (_title, startYearNumber, endYearNumber, expected) => { const apc = calculateAPC({ diff --git a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts index 76d9fd68c3..cda2254715 100644 --- a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts +++ b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/fixtures.ts @@ -1,4 +1,4 @@ -import { CapacityUnit } from 'utils/const' +import { CapacityUnit } from 'utils/enums' const fixtures = [ { diff --git a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts index 0e5565f06f..ba6cd25116 100644 --- a/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts +++ b/packages/neuron-ui/src/tests/formatters/CKBToShannonFormatter/index.test.ts @@ -1,8 +1,12 @@ -import { CapacityUnit } from 'utils/const' import { CKBToShannonFormatter } from 'utils/formatters' +import { CapacityUnit } from 'utils' import fixtures from './fixtures' -const fixtureTable = fixtures.map(({ ckb: { amount, unit }, expected }) => [amount, unit, expected]) +const fixtureTable: [string, CapacityUnit, string][] = fixtures.map(({ ckb: { amount, unit }, expected }) => [ + amount, + unit, + expected, +]) describe(`Verify CKB to Shannons formatter`, () => { test.each(fixtureTable)(`%s %s => %s shannons`, (amount: string, unit: CapacityUnit, expected: string) => { diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts index 9dcd4ac08c..341239319b 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts @@ -1,4 +1,4 @@ -import { ErrorCode } from 'utils/const' +import { ErrorCode } from 'utils/enums' const fixtures: { [title: string]: { diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts index 9efb3336d9..84553e9eb2 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts @@ -1,5 +1,5 @@ import { verifyAmount } from 'utils/validators' -import { ErrorCode } from '../../../utils/const' +import { ErrorCode } from 'utils/enums' import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts index ea59d674e6..8691608975 100644 --- a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts @@ -1,4 +1,4 @@ -import { ErrorCode } from 'utils/const' +import { ErrorCode } from 'utils/enums' const fixtures: { [title: string]: { diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts index 43b350166a..8c686f482a 100644 --- a/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts @@ -1,5 +1,5 @@ import { verifyNetworkName } from 'utils/validators' -import { ErrorCode } from 'utils/const' +import { ErrorCode } from 'utils/enums' import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { name, usedNames, expected }]) => [ diff --git a/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts index 7a2b71fea9..c77dc46592 100644 --- a/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts @@ -1,4 +1,4 @@ -import { ErrorCode } from 'utils/const' +import { ErrorCode } from 'utils/enums' const fixtures: { [title: string]: { diff --git a/packages/neuron-ui/src/theme.tsx b/packages/neuron-ui/src/theme.tsx index 086f2f1375..d0eb5b15ca 100644 --- a/packages/neuron-ui/src/theme.tsx +++ b/packages/neuron-ui/src/theme.tsx @@ -59,7 +59,7 @@ registerIcons({ completed: , cancel: , MiniCopy: , - Search: , + Search: , FirstPage: , LastPage: , PrevPage: , diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index f506e48023..70e283ee36 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -74,7 +74,6 @@ declare namespace State { interface PasswordRequest { readonly actionType: 'send' | 'backup' | 'delete' | 'unlock' | null readonly walletID: string - readonly password: string } type AlertDialog = Readonly<{ title: string; message: string }> | null @@ -100,7 +99,6 @@ declare namespace State { sending: boolean addressList: boolean transactionList: boolean - network: boolean }> readonly showTopAlert: boolean readonly showAllNotifications: boolean diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index ae55e1cdf7..6749a3342c 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -3,6 +3,11 @@ declare namespace Controller { url: string title: string } + + interface ShowSettingsParams { + tab: 'general' | 'wallets' | 'networks' + } + interface CreateWalletParams { name: string mnemonic: string @@ -161,4 +166,10 @@ declare namespace Controller { data: string } } + + namespace ExportTransactions { + interface Params { + walletID: string + } + } } diff --git a/packages/neuron-ui/src/types/Subject/index.d.ts b/packages/neuron-ui/src/types/Subject/index.d.ts index 0348084b50..41b81445a3 100644 --- a/packages/neuron-ui/src/types/Subject/index.d.ts +++ b/packages/neuron-ui/src/types/Subject/index.d.ts @@ -32,4 +32,5 @@ declare namespace Subject { version: string releaseNotes: string } + type URL = string } diff --git a/packages/neuron-ui/src/types/global/index.d.ts b/packages/neuron-ui/src/types/global/index.d.ts index 63fc4dd47b..b82cd55e4c 100644 --- a/packages/neuron-ui/src/types/global/index.d.ts +++ b/packages/neuron-ui/src/types/global/index.d.ts @@ -5,7 +5,8 @@ declare interface Window { getCurrentWindow: Function getGlobal: (name: string) => any require: (module: string) => any - process?: any + process: any + app: any } require: any nativeImage: any @@ -14,6 +15,10 @@ declare interface Window { on(channel: string, listener: Function) removeListener(channel: string, listener: Function) removeAllListeners(channel: string) + sendSync(channel: string, ...args: any[]): any + } + neuron: { + role: 'main' | 'settings' } } diff --git a/packages/neuron-ui/src/utils/calculateAPC.ts b/packages/neuron-ui/src/utils/calculateAPC.ts index a49db8c794..c2471c6101 100644 --- a/packages/neuron-ui/src/utils/calculateAPC.ts +++ b/packages/neuron-ui/src/utils/calculateAPC.ts @@ -29,7 +29,7 @@ const apcInPeriod = ({ startYearNumber, endYearNumber }: { startYearNumber: numb return rate } -const calculateAPC = ( +export const calculateAPC = ( { startYearNumber, endYearNumber }: { startYearNumber: number; endYearNumber: number }, scale: boolean = true ) => { diff --git a/packages/neuron-ui/src/utils/calculateClaimEpochValue.ts b/packages/neuron-ui/src/utils/calculateClaimEpochValue.ts index 96802cc92e..4168164451 100644 --- a/packages/neuron-ui/src/utils/calculateClaimEpochValue.ts +++ b/packages/neuron-ui/src/utils/calculateClaimEpochValue.ts @@ -6,7 +6,7 @@ interface EpochInfo { length: bigint } -export default (depositEpochInfo: EpochInfo, withdrawingEpochInfo: EpochInfo) => { +export const calculateClaimEpochValue = (depositEpochInfo: EpochInfo, withdrawingEpochInfo: EpochInfo) => { let depositedEpochs = withdrawingEpochInfo.number - depositEpochInfo.number const depositEpochFraction = depositEpochInfo.index * withdrawingEpochInfo.length const currentEpochFraction = withdrawingEpochInfo.index * depositEpochInfo.length @@ -21,3 +21,5 @@ export default (depositEpochInfo: EpochInfo, withdrawingEpochInfo: EpochInfo) => Number(depositEpochInfo.number + minLockEpochs) + Number(depositEpochInfo.index) / Number(depositEpochInfo.length) return targetEpochValue } + +export default calculateClaimEpochValue diff --git a/packages/neuron-ui/src/utils/calculateFee.ts b/packages/neuron-ui/src/utils/calculateFee.ts index e36fa958e5..709c2a0390 100644 --- a/packages/neuron-ui/src/utils/calculateFee.ts +++ b/packages/neuron-ui/src/utils/calculateFee.ts @@ -1,4 +1,4 @@ -export default (tx: any) => { +export const calculateFee = (tx: any) => { if (!tx) { return '0' } @@ -13,3 +13,5 @@ export default (tx: any) => { return (inputCapacities - outputCapacities).toString() } + +export default calculateFee diff --git a/packages/neuron-ui/src/utils/canvasActions.ts b/packages/neuron-ui/src/utils/canvasActions.ts deleted file mode 100644 index 36fd787534..0000000000 --- a/packages/neuron-ui/src/utils/canvasActions.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface Point { - x: number - y: number -} -export const drawPolygon = (_canvas: any, points: Point[], config: { color?: string }) => { - const canvas = _canvas - if (points.length < 3) { - throw new Error('Not a Polygon') - } - canvas.beginPath() - canvas.moveTo(points[0].x, points[0].y) - points.slice(1).forEach((p: Point) => { - canvas.lineTo(p.x, p.y) - }) - canvas.lineTo(points[0].x, points[0].y) - canvas.lineWidth = 4 - canvas.strokeStyle = config.color || 'red' - canvas.stroke() -} - -export default undefined diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 0929809404..b1ea7c5da8 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -19,120 +19,16 @@ export const MIN_DEPOSIT_AMOUNT = 102 export const SHANNON_CKB_RATIO = 1e8 -export const MEDIUM_FEE_RATE = 6000 +export const MEDIUM_FEE_RATE = 2000 export const WITHDRAW_EPOCHS = 180 export const IMMATURE_EPOCHS = 4 export const MILLISECONDS_IN_YEAR = 365 * 24 * 3600 * 1000 export const HOURS_PER_EPOCH = 4 export const HOURS_PER_DAY = 24 -export const CONNECTING_DEADLINE = Date.now() + 10_000 +export const INIT_SEND_PRICE = '1000' export const NERVOS_DAO_RFC_URL = 'https://www.github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md' -export enum ConnectionStatus { - Online = 'online', - Offline = 'offline', - Connecting = 'connecting', -} - -export enum Routes { - Launch = '/', - Overview = '/overview', - WalletWizard = '/wizard', - Wallet = '/wallet', - Send = '/send', - Receive = '/receive', - History = '/history', - Transaction = '/transaction', - Addresses = '/addresses', - Settings = '/settings', - SettingsGeneral = '/settings/general', - SettingsWallets = '/settings/wallets', - SettingsNetworks = '/settings/networks', - CreateWallet = '/wallets/new', - ImportWallet = '/wallets/import', - ImportKeystore = '/keystore/import', - NetworkEditor = '/network', - WalletEditor = '/editwallet', - Prompt = '/prompt', - NervosDAO = '/nervos-dao', - SpecialAssets = '/special-assets', -} - -export enum CapacityUnit { - CKB = 'ckb', - CKKB = 'ckkb', - CKGB = 'ckgb', -} - -export enum Price { - Immediately = '18000', - TenBlocks = '6000', - HundredBlocks = '3000', - FiveHundredsBlocks = '0', -} - -export const PlaceHolders = { - send: { - Calculating: '······', - Amount: 'eg: 100', - }, -} - -export enum MnemonicAction { - Create = 'create', - Verify = 'verify', - Import = 'import', -} - -export const FULL_SCREENS = [`${Routes.Transaction}/`, `/wizard/`, `/keystore/`] - -export enum ErrorCode { - // Errors from RPC - ErrorFromRPC = -3, - // Errors from neuron-wallet - AmountNotEnough = 100, - AmountTooSmall = 101, - PasswordIncorrect = 103, - NodeDisconnected = 104, - CapacityNotEnoughForChange = 105, - LocktimeAmountTooSmall = 107, - AddressNotFound = 108, - // Parameter validation errors from neuron-ui - FieldRequired = 201, - FieldUsed = 202, - FieldTooLong = 203, - FieldTooShort = 204, - FieldInvalid = 205, - DecimalExceed = 206, - NotNegative = 207, - ProtocolRequired = 208, - NoWhiteSpaces = 209, - FieldIrremovable = 301, - FieldNotFound = 303, - CameraUnavailable = 304, - AddressIsEmpty = 305, - MainnetAddressRequired = 306, - TestnetAddressRequired = 307, -} - -export enum SyncStatus { - SyncNotStart, - SyncPending, - Syncing, - SyncCompleted, -} - -export const SyncStatusThatBalanceUpdating = [SyncStatus.Syncing, SyncStatus.SyncPending] - -export enum PRESET_SCRIPT { - Locktime = 'SingleMultiSign', -} - -export enum CompensationPeriod { - SUGGEST_START = 0.767, - REQUEST_START = 0.967, - REQUEST_END = 1, -} +export const LOCALES = ['zh', 'zh-TW', 'en', 'en-US'] as const diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts new file mode 100644 index 0000000000..2e050cedfe --- /dev/null +++ b/packages/neuron-ui/src/utils/enums.ts @@ -0,0 +1,108 @@ +export enum ConnectionStatus { + Online = 'online', + Offline = 'offline', + Connecting = 'connecting', +} + +export enum RoutePath { + Launch = '/', + Overview = '/overview', + WalletWizard = '/wizard', + Wallet = '/wallet', + Send = '/send', + Receive = '/receive', + History = '/history', + Transaction = '/transaction', + Addresses = '/addresses', + Settings = '/settings', + SettingsGeneral = '/settings/general', + SettingsWallets = '/settings/wallets', + SettingsNetworks = '/settings/networks', + CreateWallet = '/wallets/new', + ImportWallet = '/wallets/import', + ImportKeystore = '/keystore/import', + NetworkEditor = '/network', + WalletEditor = '/editwallet', + Prompt = '/prompt', + NervosDAO = '/nervos-dao', + SpecialAssets = '/special-assets', +} + +export enum CapacityUnit { + CKB = 'ckb', + CKKB = 'ckkb', + CKGB = 'ckgb', +} + +export enum Price { + High = '5000', + Medium = '2000', + Low = '1000', + Zero = '0', +} + +export const PlaceHolders = { + send: { + Calculating: '······', + Amount: 'eg: 100', + }, +} + +export enum MnemonicAction { + Create = 'create', + Verify = 'verify', + Import = 'import', +} + +export enum ErrorCode { + // Errors from RPC + ErrorFromRPC = -3, + // Errors from neuron-wallet + AmountNotEnough = 100, + AmountTooSmall = 101, + PasswordIncorrect = 103, + NodeDisconnected = 104, + CapacityNotEnoughForChange = 105, + LocktimeAmountTooSmall = 107, + AddressNotFound = 108, + // Parameter validation errors from neuron-ui + FieldRequired = 201, + FieldUsed = 202, + FieldTooLong = 203, + FieldTooShort = 204, + FieldInvalid = 205, + DecimalExceed = 206, + NotNegative = 207, + ProtocolRequired = 208, + NoWhiteSpaces = 209, + FieldIrremovable = 301, + FieldNotFound = 303, + CameraUnavailable = 304, + AddressIsEmpty = 305, + MainnetAddressRequired = 306, + TestnetAddressRequired = 307, +} + +export enum SyncStatus { + SyncNotStart, + SyncPending, + Syncing, + SyncCompleted, +} + +export const SyncStatusThatBalanceUpdating = [SyncStatus.Syncing, SyncStatus.SyncPending] + +export enum PresetScript { + Locktime = 'SingleMultiSign', +} + +export enum CompensationPeriod { + SUGGEST_START = 0.767, + REQUEST_START = 0.967, + REQUEST_END = 1, +} + +export enum ResponseCode { + FAILURE, + SUCCESS, +} diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index aec9c15b51..9b4d637c54 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -1,4 +1,4 @@ -import { CapacityUnit } from './const' +import { CapacityUnit } from './enums' const base = 10e9 const numberParser = (value: string, exchange: string) => { @@ -212,16 +212,3 @@ export const difficultyFormatter = (value: bigint) => { return `${localNumberFormatter(value)} H` } - -export default { - queryFormatter, - currencyFormatter, - CKBToShannonFormatter, - shannonToCKBFormatter, - localNumberFormatter, - uniformTimeFormatter, - difficultyFormatter, - addressesToBalance, - outputsToTotalAmount, - failureResToNotification, -} diff --git a/packages/neuron-ui/src/utils/generateWalletName.ts b/packages/neuron-ui/src/utils/generateWalletName.ts index ac4dc7cd39..1a36c3251a 100644 --- a/packages/neuron-ui/src/utils/generateWalletName.ts +++ b/packages/neuron-ui/src/utils/generateWalletName.ts @@ -1,4 +1,4 @@ -const generateWalletName = (wallets: Readonly, baseNum: number = 0, t: any): string => { +export const generateWalletName = (wallets: Readonly, baseNum: number = 0, t: any): string => { const walletName = t('wizard.wallet-suffix', { suffix: baseNum }) if (wallets.some(wallet => wallet.name === walletName)) { return generateWalletName(wallets, baseNum + 1, t) diff --git a/packages/neuron-ui/src/utils/getCompensatedTime.ts b/packages/neuron-ui/src/utils/getCompensatedTime.ts index b94dfd8076..5e39b3217b 100644 --- a/packages/neuron-ui/src/utils/getCompensatedTime.ts +++ b/packages/neuron-ui/src/utils/getCompensatedTime.ts @@ -5,7 +5,7 @@ export interface CompensatedTimeParams { depositEpochValue: number } -export default ({ currentEpochValue, depositEpochValue }: CompensatedTimeParams) => { +export const getCompensatedTime = ({ currentEpochValue, depositEpochValue }: CompensatedTimeParams) => { const totalHours = Math.floor((currentEpochValue - depositEpochValue) * HOURS_PER_EPOCH) const days = Math.floor(totalHours / HOURS_PER_DAY) const hours = totalHours % HOURS_PER_DAY @@ -15,3 +15,5 @@ export default ({ currentEpochValue, depositEpochValue }: CompensatedTimeParams) hours, } } + +export default getCompensatedTime diff --git a/packages/neuron-ui/src/utils/getCompensationPeriod.ts b/packages/neuron-ui/src/utils/getCompensationPeriod.ts index 5889553d1d..4e3a432d74 100644 --- a/packages/neuron-ui/src/utils/getCompensationPeriod.ts +++ b/packages/neuron-ui/src/utils/getCompensationPeriod.ts @@ -5,7 +5,7 @@ export interface CompensationPeriodParams { endEpochValue: number } -const getCompensationPeriod = ({ currentEpochValue, endEpochValue }: CompensationPeriodParams) => { +export const getCompensationPeriod = ({ currentEpochValue, endEpochValue }: CompensationPeriodParams) => { const pastEpochs = currentEpochValue - endEpochValue + WITHDRAW_EPOCHS const totalHours = Math.ceil((WITHDRAW_EPOCHS - pastEpochs) * HOURS_PER_EPOCH) const leftDays = Math.floor(totalHours / HOURS_PER_DAY) diff --git a/packages/neuron-ui/src/utils/getCurrentUrl.ts b/packages/neuron-ui/src/utils/getCurrentUrl.ts index ed51010b1e..2cf47fe3d3 100644 --- a/packages/neuron-ui/src/utils/getCurrentUrl.ts +++ b/packages/neuron-ui/src/utils/getCurrentUrl.ts @@ -1,4 +1,6 @@ -export default (id: string, networks: Readonly) => { +export const getCurrentUrl = (id: string, networks: Readonly) => { const network = networks.find(n => n.id === id) return network?.remote } + +export default getCurrentUrl diff --git a/packages/neuron-ui/src/utils/getDAOCellStatus.ts b/packages/neuron-ui/src/utils/getDAOCellStatus.ts index 2af10e13f8..49c3224624 100644 --- a/packages/neuron-ui/src/utils/getDAOCellStatus.ts +++ b/packages/neuron-ui/src/utils/getDAOCellStatus.ts @@ -28,7 +28,7 @@ export interface DAOCellStatusParams { depositEpoch: string } -export default ({ +export const getDAOCellStatus = ({ unlockInfo, withdrawInfo, status, @@ -82,3 +82,5 @@ export default ({ } return CellStatus.Deposited } + +export default getDAOCellStatus diff --git a/packages/neuron-ui/src/utils/getExplorerUrl.ts b/packages/neuron-ui/src/utils/getExplorerUrl.ts index 38999836a1..85e0d8f16c 100644 --- a/packages/neuron-ui/src/utils/getExplorerUrl.ts +++ b/packages/neuron-ui/src/utils/getExplorerUrl.ts @@ -1,3 +1,5 @@ -export default (isMainnet: boolean = true) => { +export const getExplorerUrl = (isMainnet: boolean = true) => { return isMainnet ? 'https://explorer.nervos.org' : 'https://explorer.nervos.org/aggron' } + +export default getExplorerUrl diff --git a/packages/neuron-ui/src/utils/getSyncStatus.ts b/packages/neuron-ui/src/utils/getSyncStatus.ts index 4157e1e8fe..6090de4aa4 100644 --- a/packages/neuron-ui/src/utils/getSyncStatus.ts +++ b/packages/neuron-ui/src/utils/getSyncStatus.ts @@ -1,11 +1,12 @@ -import { SyncStatus, BUFFER_BLOCK_NUMBER, MAX_TIP_BLOCK_DELAY } from 'utils/const' +import { SyncStatus } from 'utils/enums' +import { BUFFER_BLOCK_NUMBER, MAX_TIP_BLOCK_DELAY } from 'utils/const' const TEN_MINS = 10 * 60 * 1000 let blockNumber10MinAgo: string = '' let timestamp10MinAgo: number | undefined let prevUrl: string | undefined -export default ({ +export const getSyncStatus = ({ syncedBlockNumber, tipBlockNumber, tipBlockTimestamp, @@ -44,3 +45,5 @@ export default ({ } return SyncStatus.Syncing } + +export default getSyncStatus diff --git a/packages/neuron-ui/src/utils/hooks.ts b/packages/neuron-ui/src/utils/hooks.ts index e60d0e7c6f..751ef275c1 100644 --- a/packages/neuron-ui/src/utils/hooks.ts +++ b/packages/neuron-ui/src/utils/hooks.ts @@ -1,12 +1,19 @@ import { useState, useMemo, useCallback, useEffect } from 'react' -import { TFunction } from 'i18next' -import { openContextMenu } from 'services/remote' -import { updateTransactionDescription, updateAddressDescription } from 'states/stateProvider/actionCreators' -import { StateDispatch, AppActions } from 'states/stateProvider/reducer' -import { epochParser } from 'utils/parsers' +import { useHistory } from 'react-router-dom' +import { TFunction, i18n as i18nType } from 'i18next' +import { openContextMenu, requestPassword, setCurrentNetowrk, deleteNetwork } from 'services/remote' +import { SetLocale as SetLocaleSubject } from 'services/subjects' +import { + StateDispatch, + AppActions, + updateTransactionDescription, + updateAddressDescription, + setCurrentWallet, +} from 'states' +import { epochParser, RoutePath } from 'utils' import calculateClaimEpochValue from 'utils/calculateClaimEpochValue' -export const useGoBack = (history: any) => { +export const useGoBack = (history: ReturnType) => { return useCallback(() => { history.goBack() }, [history]) @@ -156,27 +163,32 @@ export const useDialog = ({ } export const useOnDefaultContextMenu = (t: TFunction) => - useCallback(() => { - const contextMenuTemplate = [ - { label: t('contextmenu.cut'), role: 'cut' }, - { - label: t('contextmenu.copy'), - role: 'copy', - }, - { - label: t('contextmenu.paste'), - role: 'paste', - }, - { - type: 'separator', - }, - { - label: t('contextmenu.selectall'), - role: 'selectAll', - }, - ] - openContextMenu(contextMenuTemplate) - }, [t]) + useCallback( + (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName === 'INPUT') { + const contextMenuTemplate = [ + { label: t('contextmenu.cut'), role: 'cut' }, + { + label: t('contextmenu.copy'), + role: 'copy', + }, + { + label: t('contextmenu.paste'), + role: 'paste', + }, + { + type: 'separator', + }, + { + label: t('contextmenu.selectall'), + role: 'selectAll', + }, + ] + openContextMenu(contextMenuTemplate) + } + }, + [t] + ) export const useExitOnWalletChange = () => { const listener = (e: StorageEvent) => { @@ -191,11 +203,136 @@ export const useExitOnWalletChange = () => { } }, []) } -export default { - useGoBack, - useLocalDescription, - useCalculateEpochs, - useDialog, - useOnDefaultContextMenu, - useExitOnWalletChange, + +export const useOnLocalStorageChange = (handler: (e: StorageEvent) => void) => { + return useEffect(() => { + window.addEventListener('storage', handler) + return () => { + window.removeEventListener('storage', handler) + } + }, [handler]) +} + +export const useOnLocaleChange = (i18n: i18nType) => { + return useEffect(() => { + const subcription = SetLocaleSubject.subscribe(lng => { + i18n.changeLanguage(lng) + }) + return () => { + subcription.unsubscribe() + } + }, [i18n]) +} + +export const useOnHandleWallet = ({ + history, + dispatch, +}: { + history: ReturnType + dispatch: StateDispatch +}) => + useCallback( + (e: React.SyntheticEvent) => { + const { + target: { + dataset: { action }, + }, + currentTarget: { + dataset: { id }, + }, + } = e as any + switch (action) { + case 'edit': { + history.push(`${RoutePath.WalletEditor}/${id}`) + break + } + case 'delete': { + requestPassword({ walletID: id, action: 'delete-wallet' }) + break + } + case 'backup': { + requestPassword({ + walletID: id, + action: 'backup-wallet', + }) + break + } + case 'select': { + setCurrentWallet(id)(dispatch) + break + } + default: { + // ignore + } + } + }, + [dispatch, history] + ) + +export const useOnWindowResize = (handler: () => void) => { + useEffect(() => { + let rAFTimer: number | null = null + const listener = () => { + if (rAFTimer) { + window.cancelAnimationFrame(rAFTimer) + } + rAFTimer = window.requestAnimationFrame(handler) + } + + window.addEventListener('resize', listener) + return () => { + window.removeEventListener('resize', listener) + } + }, [handler]) } + +export const useToggleChoiceGroupBorder = (containerSelector: string, borderClassName: string) => + useCallback(() => { + const walletListContainer = document.querySelector(containerSelector) + if (!walletListContainer) { + return + } + const walletList = walletListContainer.querySelector('[role=radiogroup]') + if (!walletList) { + return + } + const containerHeight = +window.getComputedStyle(walletListContainer).height.slice(0, -2) + const listHeight = +window.getComputedStyle(walletList).height.slice(0, -2) + if (containerHeight > listHeight + 5) { + walletListContainer.classList.remove(borderClassName) + } else { + walletListContainer.classList.add(borderClassName) + } + }, [containerSelector, borderClassName]) + +export const useOnHandleNetwork = ({ history }: { history: ReturnType }) => + useCallback( + (e: React.SyntheticEvent) => { + const { + target: { + dataset: { action }, + }, + currentTarget: { + dataset: { id }, + }, + } = e as any + switch (action) { + case 'edit': { + history.push(`${RoutePath.NetworkEditor}/${id}`) + break + } + case 'delete': { + deleteNetwork(id) + break + } + case 'select': { + setCurrentNetowrk(id) + break + } + default: { + // ignore + } + } + }, + [history] + ) diff --git a/packages/neuron-ui/src/utils/index.ts b/packages/neuron-ui/src/utils/index.ts new file mode 100644 index 0000000000..44e86ce07a --- /dev/null +++ b/packages/neuron-ui/src/utils/index.ts @@ -0,0 +1,22 @@ +import * as CONSTANTS from './const' + +export * from './enums' +export * from './animations' +export * from './calculateAPC' +export * from './calculateClaimEpochValue' +export * from './calculateFee' +export * from './formatters' +export * from './generateWalletName' +export * from './getCompensatedTime' +export * from './getCompensationPeriod' +export * from './getCurrentUrl' +export * from './getDAOCellStatus' +export * from './getExplorerUrl' +export * from './getSyncStatus' +export * from './hooks' +export * from './renderHooks' +export * from './is' +export * from './parsers' +export * from './validators' + +export { CONSTANTS } diff --git a/packages/neuron-ui/src/utils/instantiateMethodCall.ts b/packages/neuron-ui/src/utils/instantiateMethodCall.ts deleted file mode 100644 index 71b65e5247..0000000000 --- a/packages/neuron-ui/src/utils/instantiateMethodCall.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default (obj: object) => { - const handler = { - get: (target: Function, method: string) => { - return function callMethod(...args: any[]) { - const result = target.apply(null, [method, ...args]) - return result - } - }, - } - return new Proxy(obj, handler) -} diff --git a/packages/neuron-ui/src/utils/is.ts b/packages/neuron-ui/src/utils/is.ts new file mode 100644 index 0000000000..7b1095c5dc --- /dev/null +++ b/packages/neuron-ui/src/utils/is.ts @@ -0,0 +1,11 @@ +import { ControllerResponse, SuccessFromController } from 'services/remote/remoteApiWrapper' +import { ResponseCode } from 'utils/enums' +import { MAINNET_TAG } from './const' + +export const isMainnet = (networks: Readonly, networkID: string) => { + return (networks.find(n => n.id === networkID) || {}).chain === MAINNET_TAG +} + +export const isSuccessResponse = (res: ControllerResponse): res is SuccessFromController => { + return res.status === ResponseCode.SUCCESS +} diff --git a/packages/neuron-ui/src/utils/isMainnet.ts b/packages/neuron-ui/src/utils/isMainnet.ts deleted file mode 100644 index 8b8e5ba3b0..0000000000 --- a/packages/neuron-ui/src/utils/isMainnet.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MAINNET_TAG } from './const' - -export default (networks: Readonly, networkID: string) => { - return (networks.find(n => n.id === networkID) || {}).chain === MAINNET_TAG -} diff --git a/packages/neuron-ui/src/utils/parsers.ts b/packages/neuron-ui/src/utils/parsers.ts index 17392c9927..6ab67d6191 100644 --- a/packages/neuron-ui/src/utils/parsers.ts +++ b/packages/neuron-ui/src/utils/parsers.ts @@ -36,5 +36,3 @@ export const epochParser = (epoch: string) => { value: res.length > 0 ? Number(res.number) + Number(res.index) / Number(res.length) : Number(res.number), } } - -export default { queryParsers, epochParser } diff --git a/packages/neuron-ui/src/utils/renderHooks.tsx b/packages/neuron-ui/src/utils/renderHooks.tsx new file mode 100644 index 0000000000..5527db5876 --- /dev/null +++ b/packages/neuron-ui/src/utils/renderHooks.tsx @@ -0,0 +1,16 @@ +import React, { useMemo } from 'react' +import { Route } from 'react-router-dom' + +export const useRoutes = (contents: CustomRouter.Route[]) => + useMemo(() => { + return contents.map(content => ( + + )) + }, [contents]) + +export default undefined diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index 69c8d6ebe4..9beebcce72 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -6,8 +6,8 @@ import { SINCE_FIELD_SIZE, MAX_DECIMAL_DIGITS, SHANNON_CKB_RATIO, - ErrorCode, } from 'utils/const' +import { ErrorCode } from 'utils/enums' import { CKBToShannonFormatter } from 'utils/formatters' import { ckbCore } from 'services/chain' @@ -152,12 +152,3 @@ export const verifyURL = (url: string) => { } return true } - -export default { - verifyAddress, - verifyAmountRange, - verifyTotalAmount, - verifyPasswordComplexity, - verifyTransactionOutputs, - verifyNetworkName, -} diff --git a/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss b/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss index c1ac7b382f..3f9b007c06 100644 --- a/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss +++ b/packages/neuron-ui/src/widgets/AlertDialog/alertDialog.module.scss @@ -1,7 +1,7 @@ -@import '../../styles//mixin.scss'; +@import '../../styles/mixin.scss'; .alertDialog { - @include dialogContainer; + @include dialog-container; padding: 60px 72px; &::backdrop { @@ -28,6 +28,6 @@ } &::backdrop { - @include overlay + @include overlay; } } diff --git a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx index 3976283d66..9ca7261c1e 100644 --- a/packages/neuron-ui/src/widgets/AlertDialog/index.tsx +++ b/packages/neuron-ui/src/widgets/AlertDialog/index.tsx @@ -2,7 +2,7 @@ import React, { useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { dismissAlertDialog } from 'states/stateProvider/actionCreators' import { AppActions } from 'states/stateProvider/reducer' -import { useDialog } from 'utils/hooks' +import { useDialog } from 'utils' import Button from 'widgets/Button' import styles from './alertDialog.module.scss' diff --git a/packages/neuron-ui/src/widgets/Balance/balance.module.scss b/packages/neuron-ui/src/widgets/Balance/balance.module.scss index ac8a248913..3d660ca173 100644 --- a/packages/neuron-ui/src/widgets/Balance/balance.module.scss +++ b/packages/neuron-ui/src/widgets/Balance/balance.module.scss @@ -1,7 +1,7 @@ @import '../../styles/mixin.scss'; .balance { - @include RegularText; + @include regular-text; display: block; font-size: 1rem; overflow: hidden; @@ -10,7 +10,7 @@ word-break: keep-all; .int { - @include BoldText; + @include bold-text; font-size: 1.125em; } diff --git a/packages/neuron-ui/src/widgets/Button/button.module.scss b/packages/neuron-ui/src/widgets/Button/button.module.scss index c171fdba59..6e4da697c6 100644 --- a/packages/neuron-ui/src/widgets/Button/button.module.scss +++ b/packages/neuron-ui/src/widgets/Button/button.module.scss @@ -1,15 +1,15 @@ @import '../../styles/mixin.scss'; .button { - @include MediumText; + @include medium-text; appearance: none; min-width: 5.125rem; font-size: 0.75rem; line-height: 1rem; font-weight: 500; - letter-spacing: 0.6px; padding: 7px 7px; border: none; + border-radius: 2px; margin: 0; box-sizing: border-box; @@ -20,7 +20,7 @@ border: 1px solid #000; &:hover { - @include SemiBoldText; + @include semi-bold-text; } } @@ -29,7 +29,7 @@ color: #000; &:hover { - @include SemiBoldText; + @include semi-bold-text; background-color: #d1d1d1; } } @@ -42,18 +42,17 @@ color: #fff; &:hover { - @include SemiBoldText; + @include semi-bold-text; background-color: #21b574; } } &:active { - @include SemiBoldText; + @include semi-bold-text; filter: invert(0.2); } &[disabled] { - border: none !important; opacity: 0.5; box-shadow: none !important; pointer-events: none; diff --git a/packages/neuron-ui/src/widgets/CopyZone/copyZone.module.scss b/packages/neuron-ui/src/widgets/CopyZone/copyZone.module.scss new file mode 100644 index 0000000000..d7ec95224d --- /dev/null +++ b/packages/neuron-ui/src/widgets/CopyZone/copyZone.module.scss @@ -0,0 +1,28 @@ +.container { + position: relative; + display: inline-block; + padding: 0 3px; + + &::before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + justify-content: center; + align-items: center; + content: attr(data-prompt); + font-size: 0.875rem; + font-weight: 500; + color: #666; + background-color: rgba(224, 224, 224, 0.9); + backdrop-filter: blur(1px); + user-select: none; + white-space: nowrap; + } + + &:hover::before { + display: flex; + } +} diff --git a/packages/neuron-ui/src/widgets/CopyZone/index.tsx b/packages/neuron-ui/src/widgets/CopyZone/index.tsx new file mode 100644 index 0000000000..72852312e3 --- /dev/null +++ b/packages/neuron-ui/src/widgets/CopyZone/index.tsx @@ -0,0 +1,43 @@ +import React, { useState, useCallback, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import styles from './copyZone.module.scss' + +type CopyZoneProps = React.PropsWithChildren<{ + name?: string + content: string + style?: React.CSSProperties + className?: string +}> +const CopyZone = ({ children, content, name, style, className = '' }: CopyZoneProps) => { + const [t] = useTranslation() + const [copied, setCopied] = useState(false) + const timer = useRef>() + const prompt = copied ? t('common.copied') : name || t(`common.copy`) + + const onCopy = useCallback(() => { + setCopied(true) + window.navigator.clipboard.writeText(content) + clearTimeout(timer.current!) + timer.current = setTimeout(() => { + setCopied(false) + }, 1000) + }, [setCopied, content]) + + return ( +
+ {children} +
+ ) +} + +CopyZone.displayName = 'CopyZone' + +export default CopyZone diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss b/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss index 707dd39fe2..82dce2985e 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss +++ b/packages/neuron-ui/src/widgets/DatetimePicker/datetimePicker.module.scss @@ -3,8 +3,7 @@ $width: 374px; :global { .p-calendar { - @include RegularText; - user-select: none; + @include regular-text; width: $width; padding: 20px 0; @@ -26,7 +25,7 @@ $width: 374px; margin-bottom: 16px; .p-datepicker-next.p-link { - order: 1 + order: 1; } @mixin chevron-line { @@ -66,7 +65,6 @@ $width: 374px; } } - .pi-chevron-right { &::before { @include chevron-line; @@ -86,7 +84,6 @@ $width: 374px; } } - .p-datepicker-month { margin-right: 5px; } @@ -115,7 +112,6 @@ $width: 374px; height: 30px; border-radius: 50%; color: #000; - user-select: none; font-size: 12px; margin: auto 3px; @@ -124,7 +120,6 @@ $width: 374px; color: #fff; background-color: var(--nervos-green); } - } } } @@ -146,7 +141,6 @@ $width: 374px; display: none; } - .popup { display: flex; flex-direction: column; @@ -158,20 +152,19 @@ $width: 374px; box-shadow: 4px 7px 10px 0 rgba(0, 0, 0, 0.22); .title { - @include dialogTitle; + @include dialog-title; } .timezone { span:first-child { - @include SemiBoldText; + @include semi-bold-text; margin-right: 5px; } - @include RegularText; + @include regular-text; margin-bottom: 8px; } - input, .displayedTime { width: $width; @@ -184,12 +177,12 @@ $width: 374px; .displayedTime { span:first-child { - @include RegularText; + @include regular-text; margin-right: 5px; } span:last-child { - @include SemiBoldText; + @include semi-bold-text; } } @@ -197,27 +190,26 @@ $width: 374px; border-color: var(--nervos-green); } - &[data-status="edit"] { + &[data-status='edit'] { .calendar { display: flex; } } - } .actions { - @include dialogFooter; + @include dialog-footer; width: $width; margin-top: 28px; button:last-child { - @include dialogConfirmButton; + @include dialog-confirm-button; margin-left: 9px; } } .notice { - @include RegularText; + @include regular-text; width: $width; font-size: 0.75rem; letter-spacing: 0.5px; @@ -232,10 +224,9 @@ $width: 374px; } .error { - @include RegularText; + @include regular-text; color: red; font-size: 0.75rem; margin-top: 5px; } - } diff --git a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx index bf40e9b051..755330a5a9 100644 --- a/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx +++ b/packages/neuron-ui/src/widgets/DatetimePicker/index.tsx @@ -12,6 +12,16 @@ if (UTC > 0) { UTC = `UTC${UTC}` } +export const formatDate = (datetime: Date) => { + const month = (datetime.getMonth() + 1).toString().padStart(2, '0') + const date = datetime + .getDate() + .toString() + .padStart(2, '0') + const year = datetime.getFullYear() + return `${month}/${date}/${year}` +} + export interface DatetimePickerProps { title?: string preset?: Date | string | number | null @@ -29,7 +39,7 @@ const DatetimePicker = ({ const [t] = useTranslation() const [status, setStatus] = useState<'done' | 'edit'>('done') const [datetime, setDatetime] = useState(preset ? new Date(+preset) : null) - const [display, setDisplay] = useState(new Date(datetime).toLocaleDateString()) + const [display, setDisplay] = useState(formatDate(new Date(datetime))) const locale: any = { firstDayOfWeek: 0, @@ -72,7 +82,7 @@ const DatetimePicker = ({ const onSelected = useCallback( e => { if (e.target.tagName === 'SPAN' && e.target.className !== 'p-disabled') { - setDisplay(new Date(datetime).toLocaleDateString()) + setDisplay(formatDate(new Date(datetime))) setStatus('done') } }, diff --git a/packages/neuron-ui/src/widgets/Dropdown/index.tsx b/packages/neuron-ui/src/widgets/Dropdown/index.tsx new file mode 100644 index 0000000000..92f83994be --- /dev/null +++ b/packages/neuron-ui/src/widgets/Dropdown/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import { Dropdown, IDropdownProps, Icon } from 'office-ui-fabric-react' + +const CustomDropdown = (props: IDropdownProps) => ( + { + return + }} + styles={{ + label: { + fontSize: '0.75rem', + fontWeight: 500, + }, + + title: { + fontSize: '0.75rem!important', + fontWeight: 500, + height: '1.625rem', + lineHeight: '1.625rem', + }, + dropdownOptionText: { + fontSize: '0.75rem!important', + boxShadow: 'border-box', + }, + dropdownItem: { + fontSize: '0.75rem!important', + boxShadow: 'border-box', + minHeight: 'auto', + }, + dropdownItemSelected: { + fontSize: '0.75rem!important', + minHeight: 'auto', + backgroundColor: '#e3e3e3', + }, + root: { + fontSize: '0.75rem', + marginBottom: '10px', + }, + }} + {...props} + /> +) + +export default CustomDropdown diff --git a/packages/neuron-ui/src/widgets/GlobalDialog/globalDialog.module.scss b/packages/neuron-ui/src/widgets/GlobalDialog/globalDialog.module.scss index b4e620c9c6..4adce7c65b 100644 --- a/packages/neuron-ui/src/widgets/GlobalDialog/globalDialog.module.scss +++ b/packages/neuron-ui/src/widgets/GlobalDialog/globalDialog.module.scss @@ -12,11 +12,10 @@ height: 100vh; .dialog { - @include dialogContainer; + @include dialog-container; padding: 57px 73px; background-color: #fff; min-width: auto; - user-select: none; img { width: 54px; @@ -24,7 +23,7 @@ margin-bottom: 15px; } - @include SemiBoldText; + @include semi-bold-text; display: flex; flex-direction: column; justify-content: center; diff --git a/packages/neuron-ui/src/widgets/Icons/BackupWallet.svg b/packages/neuron-ui/src/widgets/Icons/BackupWallet.svg new file mode 100644 index 0000000000..14e5cb5c97 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/BackupWallet.svg @@ -0,0 +1,8 @@ + + + + Backup + + + + diff --git a/packages/neuron-ui/src/widgets/Icons/DeleteWallet.svg b/packages/neuron-ui/src/widgets/Icons/Delete.svg similarity index 100% rename from packages/neuron-ui/src/widgets/Icons/DeleteWallet.svg rename to packages/neuron-ui/src/widgets/Icons/Delete.svg diff --git a/packages/neuron-ui/src/widgets/Icons/EditWallet.svg b/packages/neuron-ui/src/widgets/Icons/Edit.svg similarity index 100% rename from packages/neuron-ui/src/widgets/Icons/EditWallet.svg rename to packages/neuron-ui/src/widgets/Icons/Edit.svg diff --git a/packages/neuron-ui/src/widgets/Icons/ExportHistory.svg b/packages/neuron-ui/src/widgets/Icons/ExportHistory.svg new file mode 100644 index 0000000000..58d051f7a8 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/ExportHistory.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/neuron-ui/src/widgets/PropertyList/PropertyList.module.scss b/packages/neuron-ui/src/widgets/PropertyList/PropertyList.module.scss deleted file mode 100644 index 188cca3c39..0000000000 --- a/packages/neuron-ui/src/widgets/PropertyList/PropertyList.module.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import '../../styles/mixin.scss'; - -.propertyCell { - display: flex; - padding: 5px 5px 5px 0; - font-size: 14px; - - .label, - .value { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .label { - @include BoldText; - grid-area: label; - padding-right: 15px; - flex-shrink: 0; - } - - .value { - grid-area: value; - } -} diff --git a/packages/neuron-ui/src/widgets/PropertyList/index.tsx b/packages/neuron-ui/src/widgets/PropertyList/index.tsx deleted file mode 100644 index 056f8aa450..0000000000 --- a/packages/neuron-ui/src/widgets/PropertyList/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { List, getTheme } from 'office-ui-fabric-react' -import styles from './PropertyList.module.scss' - -const theme = getTheme() - -export interface Property { - label: string - value: string | number | Element | React.ReactNode -} -export interface CellStyles { - color?: string - labelWidth?: string - valueWidth?: string - height?: string -} - -const onRenderCell = (item?: Property & CellStyles) => - item ? ( -
- - {item.label} - - - {item.value} - -
- ) : null -const PropertyList = ({ - properties, - cellStyles = { labelWidth: '100px', height: 'auto', color: 'inherit' }, -}: { - properties: Property[] - cellStyles?: CellStyles -}) => { - return ({ ...prop, ...cellStyles }))} onRenderCell={onRenderCell} /> -} - -PropertyList.displayName = 'PropertyList' - -export default PropertyList diff --git a/packages/neuron-ui/src/widgets/TextField/textField.module.scss b/packages/neuron-ui/src/widgets/TextField/textField.module.scss index 6d499cde94..946f553e6a 100644 --- a/packages/neuron-ui/src/widgets/TextField/textField.module.scss +++ b/packages/neuron-ui/src/widgets/TextField/textField.module.scss @@ -3,13 +3,12 @@ $error-red: #d50000; .textField { - @include RegularText; + @include regular-text; display: grid; - grid-template: - 'label input'auto 'blank message'minmax(0.6875rem, auto) / auto 1fr; + grid-template: 'label input' auto 'blank message' minmax(0.6875rem, auto) / auto 1fr; grid-gap: 4px; - &[data-required="true"] { + &[data-required='true'] { label::after { display: inline; content: '*'; @@ -17,7 +16,7 @@ $error-red: #d50000; } } - &[data-has-error="true"] { + &[data-has-error='true'] { .input { border-color: $error-red !important; } @@ -59,7 +58,6 @@ $error-red: #d50000; } } - .suffix { font-family: inherit; display: flex; @@ -74,12 +72,10 @@ $error-red: #d50000; border-color: var(--nervos-green); } } - } .stack { - grid-template: - 'label'auto 'input'auto 'message'minmax(0.6875rem, auto) /auto; + grid-template: 'label' auto 'input' auto 'message' minmax(0.6875rem, auto) / auto; } .hint { diff --git a/packages/neuron-wallet/.eslintignore b/packages/neuron-wallet/.eslintignore index ed6c873a2d..6a3af16c06 100644 --- a/packages/neuron-wallet/.eslintignore +++ b/packages/neuron-wallet/.eslintignore @@ -2,5 +2,4 @@ src/migration dist src/database/**/migrations jest.config.js -jest.e2e.config.js scripts/notarize.js diff --git a/packages/neuron-wallet/assets/icons/icon.icns b/packages/neuron-wallet/assets/icons/icon.icns index 3293e9d070..abfbdf45cd 100644 Binary files a/packages/neuron-wallet/assets/icons/icon.icns and b/packages/neuron-wallet/assets/icons/icon.icns differ diff --git a/packages/neuron-wallet/assets/icons/icon.ico b/packages/neuron-wallet/assets/icons/icon.ico index d51a526d29..9b60cd0b22 100644 Binary files a/packages/neuron-wallet/assets/icons/icon.ico and b/packages/neuron-wallet/assets/icons/icon.ico differ diff --git a/packages/neuron-wallet/assets/icons/icon.png b/packages/neuron-wallet/assets/icons/icon.png index e132bc4987..c17497370b 100644 Binary files a/packages/neuron-wallet/assets/icons/icon.png and b/packages/neuron-wallet/assets/icons/icon.png differ diff --git a/packages/neuron-wallet/electron-builder.yml b/packages/neuron-wallet/electron-builder.yml index b1b8817627..d79a897c4e 100644 --- a/packages/neuron-wallet/electron-builder.yml +++ b/packages/neuron-wallet/electron-builder.yml @@ -13,7 +13,7 @@ afterSign: scripts/notarize.js files: - from: "../.." to: "." - filter: ["!**/*", "ormconfig.json"] + filter: ["!**/*", ".ckb-version", "ormconfig.json"] - package.json - dist - "!**/*.map" diff --git a/packages/neuron-wallet/jest.e2e.config.js b/packages/neuron-wallet/jest.e2e.config.js deleted file mode 100644 index ee66890097..0000000000 --- a/packages/neuron-wallet/jest.e2e.config.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - displayName: "E2E Tests", - testRegex: "(/tests-e2e/.*.(test|spec))\\.(ts?|js?)$", - transform: { - "^.+\\.ts?$": "ts-jest" - }, - roots: [ - "/src/", - "/tests-e2e/" - ], - moduleDirectories: [ - "node_modules", - "src" - ], - moduleFileExtensions: [ - "ts", - "js", - "json", - "node" - ], - setupFilesAfterEnv: [ - "/setup-e2e-tests.ts" - ], -}; diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index 7bfb81b292..f226c1d5dd 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -3,7 +3,7 @@ "productName": "Neuron", "description": "CKB Neuron Wallet", "homepage": "https://www.nervos.org/", - "version": "0.30.0", + "version": "0.31.0-rc1", "private": true, "author": { "name": "Nervos Core Dev", @@ -22,7 +22,6 @@ "build": "ttsc && ncp ./src/block-sync-renderer/index.html ./dist/block-sync-renderer/index.html", "clean": "rimraf dist/*", "test": "jest --runInBand", - "test:e2e": "jest --config jest.e2e.config.js --runInBand", "lint": "eslint --fix --ext .ts,.js src", "precommit": "lint-staged", "rebuild:nativemodules": "electron-builder install-app-deps" @@ -63,7 +62,6 @@ "@types/levelup": "4.3.0", "@types/sqlite3": "3.1.5", "@types/uuid": "3.4.5", - "@types/webdriverio": "4.13.0", "axios": "0.19.0", "devtron": "1.4.0", "electron": "7.1.14", @@ -71,9 +69,8 @@ "electron-devtools-installer": "2.2.4", "electron-notarize": "0.2.1", "lint-staged": "9.2.5", - "neuron-ui": "0.30.0", + "neuron-ui": "0.31.0-rc1", "rimraf": "3.0.0", - "spectron": "9.0.0", "ts-transformer-imports": "0.4.3", "ttypescript": "1.5.10" } diff --git a/packages/neuron-wallet/setup-e2e-tests.ts b/packages/neuron-wallet/setup-e2e-tests.ts deleted file mode 100644 index 94d0f3fed1..0000000000 --- a/packages/neuron-wallet/setup-e2e-tests.ts +++ /dev/null @@ -1,3 +0,0 @@ -jest.setTimeout(30_000) - -export default undefined diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 5ed225ceb0..72019ee895 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -8,7 +8,8 @@ import { NetworkType, Network } from 'models/network' import { ConnectionStatusSubject } from 'models/subjects/node' import NetworksService from 'services/networks' import WalletsService from 'services/wallets' -import { ResponseCode } from 'utils/const' +import SettingsService, { Locale } from 'services/settings' +import { ResponseCode, SETTINGS_WINDOW_TITLE } from 'utils/const' import WalletsController from 'controllers/wallets' import TransactionsController from 'controllers/transactions' @@ -58,6 +59,11 @@ export default class ApiController { private registerHandlers() { const handle = this.handleChannel + // sync messages + ipcMain.on('get-locale', e => { + e.returnValue = SettingsService.getInstance().locale + }) + // App handle('get-system-codehash', async () => { return { @@ -137,6 +143,10 @@ export default class ApiController { } }) + handle('set-locale', async (_, locale: Locale) => { + return SettingsService.getInstance().locale = locale + }) + // Wallets handle('get-all-wallets', async () => { @@ -226,7 +236,13 @@ export default class ApiController { }) handle('show-transaction-details', async (_, hash: string) => { - showWindow(`#/transaction/${hash}`, i18n.t(`messageBox.transaction.title`, { hash })) + showWindow(`#/transaction/${hash}`, i18n.t(`messageBox.transaction.title`, { hash }), { + height: 750 + }) + }) + + handle('export-transactions', async (_, params: { walletID: string }) => { + return this.transactionsController.exportTransactions(params) }) // Dao @@ -302,6 +318,10 @@ export default class ApiController { // Settings + handle('show-settings', (_, params: Controller.Params.ShowSettings) => { + showWindow(`#/settings/${params.tab}`, i18n.t(SETTINGS_WINDOW_TITLE)) + }) + handle('clear-cache', async () => { return new SyncController().clearCache() }) diff --git a/packages/neuron-wallet/src/controllers/app/index.ts b/packages/neuron-wallet/src/controllers/app/index.ts index 523001273a..6297dd6a25 100644 --- a/packages/neuron-wallet/src/controllers/app/index.ts +++ b/packages/neuron-wallet/src/controllers/app/index.ts @@ -2,6 +2,7 @@ import path from 'path' import { app as electronApp, remote, BrowserWindow } from 'electron' import windowStateKeeper from 'electron-window-state' +import i18n from 'locales/i18n' import env from 'env' import { updateApplicationMenu } from './menu' import logger from 'utils/logger' @@ -11,6 +12,7 @@ import WalletsService from 'services/wallets' import ApiController from 'controllers/api' import NodeController from 'controllers/node' import SyncApiController from 'controllers/sync-api' +import { SETTINGS_WINDOW_TITLE } from 'utils/const' const app = electronApp || (remote && remote.app) @@ -116,10 +118,7 @@ export default class AppController { if (process.platform !== 'darwin') { app.quit() } - if (this.mainWindow) { - this.mainWindow.removeAllListeners() - this.mainWindow = null - } + this.clearOnClosed() }) this.mainWindow.on('focus', () => { @@ -133,4 +132,18 @@ export default class AppController { this.mainWindow.loadURL(env.mainURL) this.updateMenu() } + + private clearOnClosed = () => { + const windowsToClose = [i18n.t(SETTINGS_WINDOW_TITLE)] + BrowserWindow.getAllWindows().forEach(bw => { + if (windowsToClose.includes(bw.getTitle())) { + bw.close() + } + }) + + if (this.mainWindow) { + this.mainWindow.removeAllListeners() + this.mainWindow = null + } + } } diff --git a/packages/neuron-wallet/src/controllers/app/menu.ts b/packages/neuron-wallet/src/controllers/app/menu.ts index 9dd17e3f5e..4f6a517af1 100644 --- a/packages/neuron-wallet/src/controllers/app/menu.ts +++ b/packages/neuron-wallet/src/controllers/app/menu.ts @@ -1,3 +1,5 @@ +import fs from 'fs' +import path from 'path' import { app, shell, @@ -13,9 +15,11 @@ import ExportDebugController from 'controllers/export-debug' import { showWindow } from 'controllers/app/show-window' import WalletsService from 'services/wallets' import CommandSubject from 'models/subjects/command' +import logger from 'utils/logger' +import { SETTINGS_WINDOW_TITLE } from 'utils/const' enum URL { - Preference = '/settings/general', + Settings = '/settings/general', CreateWallet = '/wizard/mnemonic/create', ImportMnemonic = '/wizard/mnemonic/import', ImportKeystore = '/keystore/import', @@ -34,15 +38,36 @@ const separator: MenuItemConstructorOptions = { } const showAbout = () => { - const options = { - type: 'info', - title: app.name, - message: app.name, - detail: app.getVersion(), - buttons: ['OK'], - cancelId: 0, + let applicationVersion = i18n.t('about.app-version', { name: app.name, version: app.getVersion() }) + + const appPath = app.isPackaged ? app.getAppPath() : path.join(__dirname, '../../../../..') + const ckbVersionPath = path.join(appPath, '.ckb-version') + if (fs.existsSync(ckbVersionPath)) { + try { + const ckbVersion = fs.readFileSync(ckbVersionPath, 'utf8') + applicationVersion += `\n${i18n.t('about.ckb-client-version', { version: ckbVersion })}` + } catch (err) { + logger.error(`[Menu]: `, err) + } + } + + const isWin = process.platform === 'win32' + + if (isWin) { + const options = { + type: 'info', + title: app.name, + message: app.name, + detail: applicationVersion, + buttons: ['OK'], + cancelId: 0, + } + dialog.showMessageBox(options) + return } - dialog.showMessageBox(options) + + app.setAboutPanelOptions({ applicationVersion, version: '' }) + app.showAboutPanel() } const navigateTo = (url: string) => { @@ -52,6 +77,10 @@ const navigateTo = (url: string) => { } } +const showSettings = () => { + showWindow(`#${URL.Settings}`, i18n.t(SETTINGS_WINDOW_TITLE)) +} + const requestPassword = (walletID: string, actionType: 'delete-wallet' | 'backup-wallet') => { const window = BrowserWindow.getFocusedWindow() if (window) { @@ -77,16 +106,15 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => { label: i18n.t('application-menu.neuron.about', { app: app.name, }), - role: 'about', click: () => { showAbout() }, }, { label: i18n.t('application-menu.neuron.check-updates'), enabled: isMainWindow && !UpdateController.isChecking, click: () => { - new UpdateController().checkUpdates() - navigateTo(URL.Preference) - } + new UpdateController().checkUpdates() + showSettings() + } }, separator, { @@ -94,7 +122,7 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => { enabled: isMainWindow, label: i18n.t('application-menu.neuron.preferences'), accelerator: 'CmdOrCtrl+,', - click: () => { navigateTo(URL.Preference) } + click: showSettings }, separator, { @@ -279,15 +307,15 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => { helpSubmenu.push(separator) helpSubmenu.push({ id: 'preference', - label: i18n.t('application-menu.help.settings'), - click: () => { navigateTo(URL.Preference) } + label: i18n.t(SETTINGS_WINDOW_TITLE), + click: showSettings, }) helpSubmenu.push({ label: i18n.t('application-menu.neuron.check-updates'), enabled: isMainWindow && !UpdateController.isChecking, click: () => { new UpdateController().checkUpdates() - navigateTo(URL.Preference) + showSettings() } }) helpSubmenu.push({ @@ -295,7 +323,6 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => { label: i18n.t('application-menu.neuron.about', { app: app.name }), - role: 'about', click: () => { showAbout() } }) } diff --git a/packages/neuron-wallet/src/controllers/app/show-window.ts b/packages/neuron-wallet/src/controllers/app/show-window.ts index b64a20cb49..4e6d3c794e 100644 --- a/packages/neuron-wallet/src/controllers/app/show-window.ts +++ b/packages/neuron-wallet/src/controllers/app/show-window.ts @@ -2,25 +2,40 @@ import { BrowserWindow } from 'electron' import path from 'path' import env from 'env' -const showWindow = (url: string, title: string): BrowserWindow => { - const win = new BrowserWindow({ - width: 1200, - minWidth: 900, - minHeight: 600, - show: false, - webPreferences: { - preload: path.join(__dirname, './preload.js'), - }, - }) - const fmtUrl = url.startsWith('http') || url.startsWith('file:') ? url : env.mainURL + url - win.loadURL(fmtUrl) - win.on('ready-to-show', () => { - win.setTitle(title) - win.show() - win.focus() - }) - - return win +const showWindow = (url: string, title: string, options?: Electron.BrowserWindowConstructorOptions): BrowserWindow => { + const opened = BrowserWindow.getAllWindows().find(bw => bw.getTitle() === title) + if (opened) { + opened.webContents.send('navigation', url.replace(/^#/, '')) + opened.focus() + return opened + } else { + const win = new BrowserWindow({ + width: 1200, + minWidth: 900, + minHeight: 600, + show: false, + resizable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: true, + autoHideMenuBar: true, + webPreferences: { + devTools: env.isDevMode, + nodeIntegration: false, + preload: path.join(__dirname, './preload.js'), + }, + ...options, + }) + const fmtUrl = url.startsWith('http') || url.startsWith('file:') ? url : env.mainURL + url + win.loadURL(fmtUrl) + win.on('ready-to-show', () => { + win.setTitle(title) + win.show() + win.focus() + }) + return win + } } export { showWindow } diff --git a/packages/neuron-wallet/src/controllers/app/subscribe.ts b/packages/neuron-wallet/src/controllers/app/subscribe.ts index f529d81f58..f0eaa37377 100644 --- a/packages/neuron-wallet/src/controllers/app/subscribe.ts +++ b/packages/neuron-wallet/src/controllers/app/subscribe.ts @@ -1,3 +1,4 @@ +import { BrowserWindow } from 'electron' import { debounceTime, sampleTime } from 'rxjs/operators' import CommandSubject from 'models/subjects/command' @@ -7,6 +8,8 @@ import SyncedBlockNumberSubject, { ConnectionStatusSubject } from 'models/subjec import { WalletListSubject, CurrentWalletSubject } from 'models/subjects/wallets' import dataUpdateSubject from 'models/subjects/data-update' import AppUpdaterSubject from 'models/subjects/app-updater' +import i18n from 'locales/i18n' +import { SETTINGS_WINDOW_TITLE } from 'utils/const' interface AppResponder { sendMessage: (channel: string, arg: any) => void @@ -34,7 +37,7 @@ export const subscribe = (dispatcher: AppResponder) => { CommandSubject.subscribe(params => { if (params.dispatchToUI) { - dispatcher.sendMessage('command', params) + BrowserWindow.getFocusedWindow()?.webContents.send('command', params) } else { dispatcher.runCommand(params.type, params.payload) } @@ -60,6 +63,9 @@ export const subscribe = (dispatcher: AppResponder) => { AppUpdaterSubject.subscribe(params => { dispatcher.updateMenu() - dispatcher.sendMessage('app-updater-updated', params) + BrowserWindow + .getAllWindows() + .find(bw => bw.getTitle() === i18n.t(SETTINGS_WINDOW_TITLE))?.webContents + .send('app-updater-updated', params) }) } diff --git a/packages/neuron-wallet/src/controllers/export-debug.ts b/packages/neuron-wallet/src/controllers/export-debug.ts index 1e35d5a06a..6e5df9fb16 100644 --- a/packages/neuron-wallet/src/controllers/export-debug.ts +++ b/packages/neuron-wallet/src/controllers/export-debug.ts @@ -6,12 +6,13 @@ import CKB from '@nervosnetwork/ckb-sdk-core' import { app, dialog } from 'electron' import logger from 'electron-log' import i18n from 'locales/i18n' +import { ckbDataPath } from 'services/ckb-runner' import NetworksService from 'services/networks' import SyncedBlockNumber from 'models/synced-block-number' export default class ExportDebugController { archive: archiver.Archiver - constructor() { + constructor () { this.archive = archiver('zip', { zlib: { level: 9 } }) @@ -30,7 +31,7 @@ export default class ExportDebugController { return } this.archive.pipe(fs.createWriteStream(filePath)) - await this.addStatusFile() + await Promise.all([this.addStatusFile(), this.addBundledCKBLog()]) this.addLogFiles() await this.archive.finalize() dialog.showMessageBox({ @@ -47,7 +48,7 @@ export default class ExportDebugController { const url = NetworksService.getInstance().getCurrent().remote const ckb = new CKB(url) - const [syncedBlockNumber, ckbVersion, tipBlockNumber] = await Promise.all([ + const [syncedBlockNumber, ckbVersion, tipBlockNumber, peers] = await Promise.all([ new SyncedBlockNumber() .getNextBlock() .then(n => n.toString()) @@ -59,7 +60,10 @@ export default class ExportDebugController { ckb.rpc .getTipBlockNumber() .then(n => BigInt(n).toString()) - .catch(() => '') + .catch(() => ''), + ckb.rpc + .getPeers() + .catch(() => []) ]) const { platform, arch } = process const release = os.release() @@ -71,7 +75,8 @@ export default class ExportDebugController { ckb: { url: /https?:\/\/(localhost|127.0.0.1)/.test(url) ? url : 'http://****:port', version: ckbVersion, - blockNumber: tipBlockNumber + blockNumber: tipBlockNumber, + peers, }, client: { platform, @@ -84,6 +89,39 @@ export default class ExportDebugController { }) } + private addBundledCKBLog = () => { + const name = 'bundled-ckb-log.json' + const SIZE_TO_READ = 32_000 + + return new Promise((resolve, reject) => { + const logPath = path.resolve(ckbDataPath(), 'data', 'logs', 'run.log') + if (!fs.existsSync(logPath)) { return reject(new Error("File not found")) } + + const fileStats = fs.statSync(logPath) + const position = fileStats.size - SIZE_TO_READ + + fs.open(logPath, 'r', (openErr, fd) => { + if (openErr) { return reject(openErr) } + fs.read(fd, Buffer.alloc(SIZE_TO_READ), 0, SIZE_TO_READ, position, (readErr, _, buffer) => { + if (readErr) { + reject(readErr) + } else { + resolve(buffer.toString('utf8')) + } + fs.close(fd, closeErr => { + logger.error(closeErr) + }) + return + }) + }) + + }).then((log: string) => { + this.archive.append(log, { name }) + }).catch(err => { + this.archive.append(err.message, { name }) + }) + } + private addLogFiles = (files = ['main.log', 'renderer.log']) => { const logFile = logger.transports.file.getFile() if (!logFile?.path) { diff --git a/packages/neuron-wallet/src/controllers/transactions.ts b/packages/neuron-wallet/src/controllers/transactions.ts index 366edd3020..90da5ceba5 100644 --- a/packages/neuron-wallet/src/controllers/transactions.ts +++ b/packages/neuron-wallet/src/controllers/transactions.ts @@ -1,7 +1,9 @@ +import { dialog } from 'electron' import { TransactionsService, PaginationResult } from 'services/tx' import AddressesService from 'services/addresses' import WalletsService from 'services/wallets' +import i18n from 'locales/i18n' import { ResponseCode } from 'utils/const' import { TransactionNotFound, CurrentWalletNotSet } from 'exceptions' import Transaction from 'models/chain/transaction' @@ -97,4 +99,34 @@ export default class TransactionsController { result: { hash, description } } } + + public async exportTransactions({ walletID }: { walletID: string }) { + const wallet = WalletsService.getInstance().get(walletID) + + if (!wallet) { + throw new CurrentWalletNotSet() + } + + try { + const { canceled, filePath } = await dialog.showSaveDialog({ + title: i18n.t('export-transactions.export-transactions'), + defaultPath: `transactions_${Date.now()}.csv` + }) + if (canceled || !filePath) { + return + } + const total = await TransactionsService.exportTransactions({ walletID, filePath }) + dialog.showMessageBox({ + type: 'info', + message: i18n.t('export-transactions.transactions-exported', { file: filePath, total }) + }) + return { + status: ResponseCode.Success, + result: total + } + } catch (err) { + dialog.showErrorBox(i18n.t('common.error'), err.message) + throw err + } + } } diff --git a/packages/neuron-wallet/src/locales/en.ts b/packages/neuron-wallet/src/locales/en.ts index a8e9153039..6b9b2785d1 100644 --- a/packages/neuron-wallet/src/locales/en.ts +++ b/packages/neuron-wallet/src/locales/en.ts @@ -127,7 +127,7 @@ export default { transaction: { title: 'Transaction: {{hash}}', }, - 'sign-and-verify':{ + 'sign-and-verify': { title: 'Sign/verify message' }, }, @@ -147,9 +147,35 @@ export default { ok: 'OK', error: 'Error', }, - 'export-debug-info':{ + 'export-debug-info': { 'export-debug-info': 'Export Debug Information', 'debug-info-exported': 'Debug information has been exported to {{ file }}' + }, + about: { + "app-version": "{{name}} Version: {{version}}", + "ckb-client-version": "CKB Client Version: {{version}}" + }, + settings: { + title: { + normal: 'Settings', + mac: 'Preference' + } + }, + 'export-transactions': { + 'export-transactions': 'Export Transaction History', + 'transactions-exported': '{{total}} transaction records have been exported to {{file}}', + column: { + "time": "Time", + "block-number": "Block Number", + "tx-hash": "Transaction Hash", + "tx-type": "Transaction Type", + "amount": "Amount", + "description": "Description" + }, + "tx-type": { + "send": "Send", + "receive": "Receive" + } } }, } diff --git a/packages/neuron-wallet/src/locales/zh-tw.ts b/packages/neuron-wallet/src/locales/zh-tw.ts index 6f57d11695..d996f3d4c4 100644 --- a/packages/neuron-wallet/src/locales/zh-tw.ts +++ b/packages/neuron-wallet/src/locales/zh-tw.ts @@ -34,7 +34,7 @@ export default { }, tools: { label: "工具", - "sign-and-verify": "簽名/驗證信息", + "sign-and-verify": "簽名/驗簽信息", }, window: { label: '視窗', @@ -125,8 +125,8 @@ export default { transaction: { title: '交易: {{hash}}', }, - 'sign-and-verify':{ - title: '簽名/驗證信息' + 'sign-and-verify': { + title: '簽名/驗簽信息' }, }, prompt: { @@ -145,9 +145,36 @@ export default { ok: '確定', error: '錯誤' }, + 'export-debug-info': { + 'export-debug-info': '導出調試信息', + 'debug-info-exported': '調試信息已被導出至 {{ file }}' + }, + about: { + "app-version": "{{name}} 版本: {{version}}", + "ckb-client-version": "CKB 節點版本: {{version}}" + }, + settings: { + title: { + normal: '設置', + mac: '偏好設置' + } + }, + 'export-transactions': { + 'export-transactions': '導出交易歷史', + 'transactions-exported': '{{total}} 條交易記錄已被導出至 {{file}}', + column: { + "time": "時間", + "block-number": "區塊高度", + "tx-hash": "交易哈希", + "tx-type": "交易類型", + "amount": "金額", + "description": "備註" + }, + "tx-type": { + "send": "轉賬", + "receive": "收款" + } + } }, - 'export-debug-info':{ - 'export-debug-info': '導出調試信息', - 'debug-info-exported': '調試信息已被導出至 {{ file }}' - } } + diff --git a/packages/neuron-wallet/src/locales/zh.ts b/packages/neuron-wallet/src/locales/zh.ts index 7bbedee8fc..2e640f5aeb 100644 --- a/packages/neuron-wallet/src/locales/zh.ts +++ b/packages/neuron-wallet/src/locales/zh.ts @@ -32,9 +32,9 @@ export default { paste: '粘贴', selectall: '全部选中', }, - tools:{ + tools: { label: "工具", - "sign-and-verify": "签名/验证信息", + "sign-and-verify": "签名/验签信息", }, window: { label: '窗口', @@ -126,8 +126,8 @@ export default { transaction: { title: '交易: {{hash}}', }, - 'sign-and-verify':{ - title: '签名/验证信息' + 'sign-and-verify': { + title: '签名/验签信息' }, }, prompt: { @@ -146,9 +146,35 @@ export default { ok: '确定', error: '错误' }, - 'export-debug-info':{ + 'export-debug-info': { 'export-debug-info': '导出调试信息', 'debug-info-exported': '调试信息已被导出至 {{ file }}' + }, + about: { + "app-version": "{{name}} 版本: {{version}}", + "ckb-client-version": "CKB 节点版本: {{version}}" + }, + settings: { + title: { + normal: '设置', + mac: '偏好设置' + } + }, + 'export-transactions': { + 'export-transactions': '导出交易历史', + 'transactions-exported': '{{total}} 条交易记录已被导出至 {{file}}', + column: { + "time": "时间", + "block-number": "区块高度", + "tx-hash": "交易哈希", + "tx-type": "交易类型", + "amount": "金额", + "description": "备注" + }, + "tx-type": { + "send": "转账", + "receive": "收款" + } } }, } diff --git a/packages/neuron-wallet/src/main.ts b/packages/neuron-wallet/src/main.ts index 6749ee72b3..db36c176d6 100644 --- a/packages/neuron-wallet/src/main.ts +++ b/packages/neuron-wallet/src/main.ts @@ -1,6 +1,7 @@ import { app } from 'electron' import AppController from 'controllers/app' +import SettingsService from 'services/settings' import { changeLanguage } from 'locales/i18n' const appController = new AppController() @@ -8,7 +9,7 @@ const appController = new AppController() const singleInstanceLock = app.requestSingleInstanceLock() if (singleInstanceLock) { app.on('ready', async () => { - changeLanguage(app.getLocale()) + changeLanguage(SettingsService.getInstance().locale) appController.start() }) diff --git a/packages/neuron-wallet/src/services/ckb-runner.ts b/packages/neuron-wallet/src/services/ckb-runner.ts index c2251eb994..04bc11bd5a 100644 --- a/packages/neuron-wallet/src/services/ckb-runner.ts +++ b/packages/neuron-wallet/src/services/ckb-runner.ts @@ -33,7 +33,7 @@ const ckbBinary = (): string => { return platform() === 'win' ? binary + '.exe' : binary } -const ckbDataPath = (): string => { +export const ckbDataPath = (): string => { return path.resolve(app.getPath('userData',), 'chains/mainnet') } diff --git a/packages/neuron-wallet/src/services/settings.ts b/packages/neuron-wallet/src/services/settings.ts new file mode 100644 index 0000000000..2e79bf7681 --- /dev/null +++ b/packages/neuron-wallet/src/services/settings.ts @@ -0,0 +1,42 @@ +import { BrowserWindow } from 'electron' +import env from 'env' +import Store from 'models/store' +import { changeLanguage } from 'locales/i18n' +import { updateApplicationMenu } from 'controllers/app/menu' + +export const locales = ['zh', 'zh-TW', 'en', 'en-US'] as const +export type Locale = typeof locales[number] + +export default class SettingsService extends Store { + private static instance: SettingsService | null = null + + public static getInstance() { + if (!SettingsService.instance) { + SettingsService.instance = new SettingsService() + } + return SettingsService.instance + } + + get locale() { + return this.readSync('locale') + } + + set locale(lng: Locale) { + if (locales.includes(lng)) { + this.writeSync('locale', lng) + changeLanguage(lng) + this.onLocaleChanged(lng) + } else { + throw new Error(`Locale ${lng} not supported`) + } + } + + constructor () { + super('', 'settings.json', JSON.stringify({ locale: env.app.getLocale() })) + } + + private onLocaleChanged = (lng: Locale) => { + BrowserWindow.getAllWindows().forEach(bw => bw.webContents.send('set-locale', lng)) + updateApplicationMenu(null) + } +} diff --git a/packages/neuron-wallet/src/services/settings/base.ts b/packages/neuron-wallet/src/services/settings/base.ts deleted file mode 100644 index 10ebce5bcf..0000000000 --- a/packages/neuron-wallet/src/services/settings/base.ts +++ /dev/null @@ -1,48 +0,0 @@ -import FileService from '../file' - -export default class BaseSettings { - private static moduleName = '' - private static fileName = 'settings.json' - - private static instance: BaseSettings - - public static getInstance(): BaseSettings { - if (!BaseSettings.instance) { - BaseSettings.instance = new BaseSettings() - } - - return BaseSettings.instance - } - - public updateSetting = (key: string, value: any) => { - let settings = this.read() - if (settings === undefined) { - settings = {} - } - Object.assign(settings, { [key]: value }) - FileService.getInstance().writeFileSync(BaseSettings.moduleName, BaseSettings.fileName, JSON.stringify(settings)) - } - - public getSetting = (key: string) => { - const info = this.read() - - if (info) { - return info[key] - } - - return undefined - } - - public read = () => { - const fileService = FileService.getInstance() - const { moduleName, fileName } = BaseSettings - - if (fileService.hasFile(moduleName, fileName)) { - const info = FileService.getInstance().readFileSync(moduleName, fileName) - const value = JSON.parse(info) - return value - } - - return undefined - } -} diff --git a/packages/neuron-wallet/src/services/tx/transaction-service.ts b/packages/neuron-wallet/src/services/tx/transaction-service.ts index 460fa5bf21..11c9661a94 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-service.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-service.ts @@ -1,10 +1,12 @@ import { getConnection, ObjectLiteral } from 'typeorm' +import AddressesService from 'services/addresses' import { pubkeyToAddress } from '@nervosnetwork/ckb-sdk-utils' import TransactionEntity from 'database/chain/entities/transaction' import OutputEntity from 'database/chain/entities/output' import Transaction, { TransactionStatus } from 'models/chain/transaction' import InputEntity from 'database/chain/entities/input' import AddressParser from 'models/address-parser' +import exportTransactions from 'utils/export-history' export interface TransactionsByAddressesParam { pageNo: number @@ -368,6 +370,15 @@ export class TransactionsService { return getConnection().manager.save(transactionEntity) } + public static async exportTransactions({ walletID, filePath }: { walletID: string, filePath: string }) { + const addresses = AddressesService.allAddressesByWalletId(walletID).map(addr => addr.address) + const lockHashList = AddressParser.batchToLockHash(addresses) + const connection = getConnection() + const dbPath = connection.options.database as string + const total = await exportTransactions({ walletID, dbPath, lockHashList, filePath }) + return total + } + // only deal with address / txHash / Date private static async searchSQL(params: TransactionsByLockHashesParam, type: SearchType, value: string = '') { const base = [ diff --git a/packages/neuron-wallet/src/types/controller.d.ts b/packages/neuron-wallet/src/types/controller.d.ts index d947c56342..f7f95eece6 100644 --- a/packages/neuron-wallet/src/types/controller.d.ts +++ b/packages/neuron-wallet/src/types/controller.d.ts @@ -66,6 +66,10 @@ declare module Controller { signature: string message: string } + + interface ShowSettings { + tab: 'general' | 'wallets' | 'networks' + } } interface Wallet { diff --git a/packages/neuron-wallet/src/utils/const.ts b/packages/neuron-wallet/src/utils/const.ts index 3c6bdb1c9d..a65a5d7167 100644 --- a/packages/neuron-wallet/src/utils/const.ts +++ b/packages/neuron-wallet/src/utils/const.ts @@ -1,6 +1,7 @@ export const MIN_PASSWORD_LENGTH = 8 export const MAX_PASSWORD_LENGTH = 50 export const BUNDLED_CKB_URL = 'http://localhost:8114' +export const SETTINGS_WINDOW_TITLE = process.platform === 'darwin' ? 'settings.title.mac' : 'settings.title.normal' export enum ResponseCode { Fail, diff --git a/packages/neuron-wallet/src/utils/export-history.ts b/packages/neuron-wallet/src/utils/export-history.ts new file mode 100644 index 0000000000..04df6eb5d7 --- /dev/null +++ b/packages/neuron-wallet/src/utils/export-history.ts @@ -0,0 +1,152 @@ +import fs from 'fs' +import sqlite3 from 'sqlite3' +import { get as getDescription } from 'database/leveldb/transaction-description' +import i18n from 'locales/i18n' +import shannonToCKB from 'utils/shannonToCKB' + + +interface ExportHistoryParms { + walletID: string + dbPath: string + lockHashList?: string[] + filePath: string +} +const exportHistory = ({ + walletID, + dbPath, + lockHashList = [], + filePath +}: ExportHistoryParms): Promise => { + return new Promise((resolve, reject) => { + if (!dbPath) { + return reject(new Error(`Database is required`)) + } + + if (!Array.isArray(lockHashList)) { + return reject(new Error(`Lock hash list is expected to be an array`)) + } + + if (!filePath) { + return reject(new Error(`File Path is required`)) + } + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath) + } + + let total: number | undefined + let inserted = 0 + + const SEND_TYPE = i18n.t('export-transactions.tx-type.send') + const RECEIVE_TYPE = i18n.t('export-transactions.tx-type.receive') + + const writeStream = fs.createWriteStream(filePath) + writeStream.write( + `${['time', 'block-number', 'tx-hash', 'tx-type', 'amount', 'description'] + .map(label => i18n.t(`export-transactions.column.${label}`))}\n` + ) + + const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY) + const serializedlockHashList = lockHashList.map(l => `'${l}'`).join(`,`) + + const onRowLoad = (err: Error, row: { + hash: string, + inputShannon: string | null, + timestamp: string, + blockNumber: string, + description: string + }) => { + db.serialize(() => { + if (err) { + return reject(err) + } + db.get( + ` + SELECT + CAST(SUM(CAST(output.capacity AS UNSIGNED BIG ING)) AS VARCHAR) outputShannon + FROM + output + WHERE + (output.transactionHash = ? AND output.lockHash IN (${serializedlockHashList})) + GROUP BY + output.transactionHash + `, + [row.hash], + async (err, { outputShannon } = { outputShannon: '0' }) => { + if (err) { + return reject(err) + } + const description = await getDescription(walletID, row.hash) + const totalInput = BigInt(row.inputShannon || `0`) + const totalOutput = BigInt(outputShannon || `0`) + let txType = `-` + if (totalInput > totalOutput) { + txType = SEND_TYPE + } else if (totalInput < totalOutput) { + txType = RECEIVE_TYPE + } + const amount = shannonToCKB(totalOutput - totalInput) + writeStream.write( + `${new Date(+row.timestamp).toISOString()},${row.blockNumber},${ + row.hash + },${txType},${amount},"${description}"\n`, + err => { + if (err) { + return reject(err) + } + if (++inserted === total) { + writeStream.end() + return resolve(total) + } + } + ) + } + ) + }) + } + + const onCompleted = (err: Error, retrieved: number) => { + if (err) { + return reject(err) + } + db.close() + if (!retrieved) { + writeStream.end() + return resolve(0) + } + total = retrieved + } + + db.serialize(() => { + db.each( + ` + SELECT + tx.hash, + tx.timestamp, + tx.blockNumber, + CAST(SUM(CAST(input.capacity AS UNSIGNED BIG INT)) AS VARCHAR) inputShannon + FROM 'transaction' AS tx + LEFT JOIN + input ON (input.transactionHash = tx.hash AND input.lockHash in (${serializedlockHashList})) + WHERE + tx.hash IN ( + SELECT output.transactionHash FROM output WHERE output.lockHash IN (${serializedlockHashList}) + UNION + SELECT input.transactionHash FROM input WHERE input.lockHash IN (${serializedlockHashList}) + ) + AND + tx.timestamp IS NOT NULL + GROUP BY + tx.hash + ORDER BY + CAST(tx.timestamp AS UNSIGNED BIG INT) + `, + onRowLoad, + onCompleted + ) + }) + + }) +} + +export default exportHistory diff --git a/packages/neuron-wallet/src/utils/shannonToCKB.ts b/packages/neuron-wallet/src/utils/shannonToCKB.ts new file mode 100644 index 0000000000..672bfc8080 --- /dev/null +++ b/packages/neuron-wallet/src/utils/shannonToCKB.ts @@ -0,0 +1,19 @@ + +const DECIMAL = 8 + +const shannonToCKB = (shannon: bigint) => { + if (shannon === BigInt(0)) { + return `0.${'0'.repeat(DECIMAL)}` + } + + const isNegative = shannon < 0 + const absStr = isNegative ? `${shannon}`.slice(1) : `${shannon}` + if (absStr.length <= DECIMAL) { + return `${isNegative ? '-' : '+'}0.${absStr.padStart(DECIMAL, '0')}` + } + const int = absStr.slice(0, -1 * DECIMAL) + const dec = absStr.slice(-1 * DECIMAL) + return `${isNegative ? '-' : '+'}${int}.${dec}` +} + +export default shannonToCKB diff --git a/packages/neuron-wallet/tests-e2e/address-book.test.ts b/packages/neuron-wallet/tests-e2e/address-book.test.ts deleted file mode 100644 index 6b114ce187..0000000000 --- a/packages/neuron-wallet/tests-e2e/address-book.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import Application from './application' - -/** - * 1. navigate to the address book - * 2. verify the count of receiving/changing addresses - * 3. verify default data of the first address - * 4. update description of the first address - * 5. refresh the view and verify the description - */ -describe('Address book tests', () => { - const app = new Application() - beforeEach(() => app.start()) - afterEach(() => app.stop()) - - beforeEach(async () => { - await app.spectron.client.waitUntilWindowLoaded() - await app.createWalletFromWizard() - - app.spectron.client.click('button[name=Addresses]') - await app.spectron.client.waitUntilWindowLoaded() - }) - - app.test('Address book should have 20 receiving addresses and 10 change addresses', async () => { - const { client } = app.spectron - await client.pause(1000) - const countOfReceivingAddresses = await client - .elements('//SPAN[text()="Receiving Address"]') - .then(res => res.value.length) - expect(countOfReceivingAddresses).toBe(20) - await client.pause(1000) - const countOfChangeAddresses = await client - .elements('//SPAN[text()="Change Address"]') - .then(res => res.value.length) - expect(countOfChangeAddresses).toBe(10) - }) - - app.test('Update description', async () => { - const newDescription = 'new description' - const descriptionCellSelector = 'div[data-automation-key=description]' - const editButtonSelector = 'i[data-icon-name=Edit]' - const inputSelector = 'div[role=row] input' - const { client } = app.spectron - - const descriptionBeforeUpdate = await client.$(inputSelector).getValue() - expect(descriptionBeforeUpdate).toBe('') - - client.moveToObject(descriptionCellSelector) - await client.waitUntilWindowLoaded() - client.click(editButtonSelector) - await client.waitUntilWindowLoaded() - - client.setValue(inputSelector, newDescription) - await client.waitUntilWindowLoaded() - await client.pause(1000) - - client.click('button[name=Overview]') - await client.waitUntilWindowLoaded() - client.click('button[name=Addresses]') - await client.waitUntilWindowLoaded() - - const descriptionAfterUpdate = await client.$(inputSelector).getValue() - expect(descriptionAfterUpdate).toBe(newDescription) - }) -} diff --git a/packages/neuron-wallet/tests-e2e/application/env.ts b/packages/neuron-wallet/tests-e2e/application/env.ts deleted file mode 100644 index e555fbff65..0000000000 --- a/packages/neuron-wallet/tests-e2e/application/env.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface TransactionSendTo { - address: string - amount: number -} - -export interface TransactionTestEnv { - // Used to test sending transactions, please make sure there is enough balance. - mnemonic: string, - sendTo: TransactionSendTo[] -} - -export interface TestEnv { - transaction: TransactionTestEnv -} - -const env: TestEnv = { - transaction: { - mnemonic: '', - sendTo: [] - } -} - -export default env; \ No newline at end of file diff --git a/packages/neuron-wallet/tests-e2e/application/index.ts b/packages/neuron-wallet/tests-e2e/application/index.ts deleted file mode 100644 index 73e5d813c2..0000000000 --- a/packages/neuron-wallet/tests-e2e/application/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Application as SpectronApplication } from 'spectron'; -import { Element, RawResult } from 'webdriverio'; -import { clickMenu, deleteNetwork, editNetwork, editWallet } from './utils'; -import { createWallet } from '../operations/create-wallet' - -export default class Application { - spectron: SpectronApplication - - constructor() { - let electronPath = path.join(__dirname, '../..', 'node_modules', '.bin', 'electron') - if (process.platform === 'win32') { - electronPath += '.cmd' - } - this.spectron = new SpectronApplication({ - args: [ - '--require', - path.join(__dirname, 'preload.js'), - path.join(__dirname, '../..', 'dist', 'main.js'), - '--lang=en', - ], - path: electronPath, - env: { - // NODE_ENV: 'test' - }, - }) - } - - async start() { - if (this.spectron.isRunning()) { - return - } - await this.spectron.start() - await this.spectron.client.waitUntilWindowLoaded(10000) - } - - async stop() { - if (!this.spectron.isRunning()) { - return - } - await this.spectron.stop() - } - - async createWalletFromWizard() { - this.spectron.client.click('button[name=create-a-wallet]') - await this.spectron.client.waitUntilWindowLoaded() - await createWallet(this) - await this.spectron.client.waitUntilWindowLoaded() - } - - async createWalletFromSettings() { - await this.gotoSettingsView() - await this.spectron.client.waitUntilWindowLoaded() - this.spectron.client.click('button[name=Wallets]') - await this.spectron.client.waitUntilWindowLoaded() - this.spectron.client.click('button[name=create-a-wallet]') - await this.spectron.client.waitUntilWindowLoaded() - await createWallet(this) - await this.spectron.client.waitUntilWindowLoaded() - } - - test(name: string, func: () => void, timeout: number = 2000 * 10 * 1) { - it(name, async () => { - try { - await func() - } catch (error) { - const errorsPath = path.join(__dirname, '../errors') - if (!fs.existsSync(errorsPath)) { - fs.mkdirSync(errorsPath) - } - const errorFileName = `${name.replace(/ /g, '_')}-${new Date().getTime()}` - - // save error log - fs.writeFileSync(path.join(errorsPath, `${errorFileName}.txt`), error.stack) - - // save screenshot - const imageBuffer = await this.spectron.browserWindow.capturePage() - fs.writeFileSync(path.join(errorsPath, `${errorFileName}.png`), imageBuffer) - - throw error - } - }, timeout) - } - - // ipc - - editWallet(walletId: string) { - return editWallet(this.spectron.electron, walletId) - } - - clickMenu(labels: string[]) { - return clickMenu(this.spectron.electron, labels) - } - - editNetwork(networkId: string) { - return editNetwork(this.spectron.electron, networkId) - } - - deleteNetwork(networkId: string) { - return deleteNetwork(this.spectron.electron, networkId) - } - - - async elements(selector: string): Promise> { - const { client } = this.spectron - let result: RawResult | undefined - let error: Error | undefined - try { - result = await client.elements(selector) - } catch (_error) { - error = _error - } - - return new Promise((resolve, reject) => { - if (error) { - reject(error) - } else { - resolve(result) - } - }) - } - - async setElementValue(selector: string, text: string) { - const { client } = this.spectron - await client.selectorExecute(selector, (elements: any, args) => { - const element = elements[0] - var event = new Event('input', { bubbles: true}) as any; - event.simulated = true; - element.value = args; - element.dispatchEvent(event); - return `${element} ${args}` - }, text) - } - - async gotoSettingsView() { - this.spectron.client.click('button[name=Settings]') - } -} diff --git a/packages/neuron-wallet/tests-e2e/application/preload.js b/packages/neuron-wallet/tests-e2e/application/preload.js deleted file mode 100644 index 32f1fc91d2..0000000000 --- a/packages/neuron-wallet/tests-e2e/application/preload.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const electron = require("electron"); -const CommandSubject = require("../../dist/models/subjects/command").default; -const servicesNetworks = require("../../dist/services/networks").default; - -electron.ipcMain.on('E2E_EDIT_WALLET', function (event, arg) { - const walletId = arg[0]; - const window = electron.BrowserWindow.getFocusedWindow() - CommandSubject.next({ winID: window.id, type: 'navigate-to-url', payload: `/editwallet/${walletId}`, dispatchToUI: true }) -}); - -electron.ipcMain.on('E2E_EDIT_NETWORK', function (event, arg) { - const networkId = arg[0]; - const window = electron.BrowserWindow.getFocusedWindow() - CommandSubject.next({ winID: window.id, type: 'navigate-to-url', payload: `/network/${networkId}`, dispatchToUI: true }) -}); - -electron.ipcMain.on('E2E_DELETE_NETWORK', function (event, arg) { - const networkId = arg[0]; - const networksService = servicesNetworks.getInstance(); - networksService.delete(networkId); -}) - -function findItem(menuItems, labels) { - var target = labels[0]; - var rest = labels.slice(1); - var foundItem = menuItems.find(function (item) { return item.label === target; }); - if (rest.length === 0) { - return foundItem; - } - return findItem(foundItem.submenu.items, rest); -} - -electron.ipcMain.on('E2E_GET_MENU_ITEM', function (e, labels) { - var menuItem = findItem(electron.Menu.getApplicationMenu().items, labels); - if (menuItem) { - e.returnValue = new electron.MenuItem({ - checked: menuItem.checked, - enabled: menuItem.enabled, - label: menuItem.label, - visible: menuItem.visible - }); - } - else { - e.returnValue = ({ - label: '' - }); - } -}); - -electron.ipcMain.on('E2E_CLICK_MENU_ITEM', function (e, labels) { - var item = findItem(electron.Menu.getApplicationMenu().items, labels); - item.click(); -}); - -electron.ipcMain.on('E2E_QUIT_APP', function () { - electron.app.quit() -}) diff --git a/packages/neuron-wallet/tests-e2e/application/utils.ts b/packages/neuron-wallet/tests-e2e/application/utils.ts deleted file mode 100644 index 31ef925119..0000000000 --- a/packages/neuron-wallet/tests-e2e/application/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AllElectron } from 'electron' - -// TODO: Refactor this! Integration tests should reply on UI operation, not IPC communications! -export const clickMenu = (electron: AllElectron, labels: string[]) => { - return electron.ipcRenderer.send('E2E_CLICK_MENU_ITEM', labels) -} - -export const getMenuItem = (electron: AllElectron, labels: string[]) => { - return electron.ipcRenderer.sendSync('E2E_GET_MENU_ITEM', labels) -} - -export const editWallet = (electron: AllElectron, walletId: string) => { - return electron.ipcRenderer.send('E2E_EDIT_WALLET', [walletId]) -} - -export const quitApp = (electron: AllElectron) => { - return electron.ipcRenderer.send('E2E_QUIT_APP') -} - -export const editNetwork = (electron: AllElectron, networkId: string) => { - return electron.ipcRenderer.send('E2E_EDIT_NETWORK', [networkId]) -} - -export const deleteNetwork = (electron: AllElectron, networkId: string) => { - return electron.ipcRenderer.send('E2E_DELETE_NETWORK', [networkId]) -} diff --git a/packages/neuron-wallet/tests-e2e/network.test.ts b/packages/neuron-wallet/tests-e2e/network.test.ts deleted file mode 100644 index 9bcff1db92..0000000000 --- a/packages/neuron-wallet/tests-e2e/network.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import Application from './application'; - -describe('Wallet tests', () => { - const app = new Application() - beforeEach(() => app.start()) - afterEach(() => app.stop()) - - const addNetwork = async () => { - const { client } = app.spectron - const newNodeName = 'Node-2233' - const newNodeRpcUrl = 'http://localhost:8114' - - client.click('button[name=add-network]') - await client.waitUntilWindowLoaded() - - // Setup Network - const inputElements = await client.elements('') - expect(inputElements.value).not.toBeUndefined() - expect(inputElements.value.length).toBe(2) - await app.setElementValue('//MAIN/DIV/DIV/DIV[1]//INPUT', newNodeRpcUrl) - await app.setElementValue('//MAIN/DIV/DIV/DIV[2]//INPUT', newNodeName) - await client.waitUntilWindowLoaded() - // Save - client.click('button[name=save-network]') - await client.waitUntilWindowLoaded() - await client.pause(1000) - - // Check network name - const title = `${newNodeName}: ${newNodeRpcUrl}` - const newNetworkItemElement = await client.element("//MAIN//LABEL/DIV[@title='" + title + "']") - expect(newNetworkItemElement.value).not.toBeUndefined() - } - - beforeEach(async () => { - await app.spectron.client.waitUntilWindowLoaded() - await app.createWalletFromWizard() - - await app.gotoSettingsView() - await app.spectron.client.waitUntilWindowLoaded() - - app.spectron.client.click('button[name=Network]') - await app.spectron.client.waitUntilWindowLoaded() - }) - - app.test('add network', async () => { - await addNetwork() - }) - - app.test('edit network', async () => { - await addNetwork() - - const { client } = app.spectron - await client.waitUntilWindowLoaded() - - // Get network id - const inputs = await client.elements("//MAIN//INPUT") - const networkItemElement = inputs.value[1] - expect(networkItemElement).not.toBeUndefined() - const networkItemElementId = await client.elementIdAttribute(networkItemElement.ELEMENT, 'id') - const networkItemElementName = await client.elementIdAttribute(networkItemElement.ELEMENT, 'name') - const networkId = networkItemElementId.value.slice(networkItemElementName.value.length + 1) - - // Go to edit network page - await app.editNetwork(networkId) - await client.waitUntilWindowLoaded() - - // Setup Network - const inputElements = await client.elements('') - expect(inputElements.value).not.toBeUndefined() - expect(inputElements.value.length).toBe(2) - const networkRpcUrlInputText = await client.elementIdAttribute(inputElements.value[0].ELEMENT, 'value') - const networkNameInputText = await client.elementIdAttribute(inputElements.value[1].ELEMENT, 'value') - const newRpcUrl = `${networkRpcUrlInputText.value}22` - const newName = `${networkNameInputText.value}33` - await app.setElementValue('//MAIN/DIV/DIV/DIV[1]//INPUT', newRpcUrl) - await app.setElementValue('//MAIN/DIV/DIV/DIV[2]//INPUT', newName) - await client.waitUntilWindowLoaded() - - // Save - client.click('button[name=save-network]') - await client.waitUntilWindowLoaded() - await client.pause(1000) - - // Check network name - const title = `${newName}: ${newRpcUrl}` - const newNetworkItemElement = await client.element("//MAIN//LABEL/DIV[@title='" + title + "']") - expect(newNetworkItemElement.value).not.toBeUndefined() - }) - - app.test('switch network', async () => { - await addNetwork() - - const { client } = app.spectron - - // Get target network name - const labels = await app.elements('//MAIN//LABEL//SPAN') - const targetNetworkNameElement = labels.value[3] - expect(targetNetworkNameElement).not.toBeUndefined() - const targetNetowrkName = await client.elementIdText(targetNetworkNameElement.ELEMENT) - - // switch network - const inputs = await app.elements("//MAIN//INPUT") - const targetNetworkElement = inputs.value[1].ELEMENT - await client.elementIdClick(targetNetworkElement) - await client.waitUntilWindowLoaded() - await client.pause(3000) - - // Check network name - const networkElement = await client.element('[id=connected-network-name]') - expect(networkElement).not.toBeUndefined() - const networkName = await client.elementIdText(networkElement.value.ELEMENT) - expect(networkName.value).toBe(targetNetowrkName.value) - }) - - app.test('delete network', async () => { - await addNetwork() - - const { client } = app.spectron - - // Get network name - const labels = await app.elements('//MAIN//LABEL//SPAN') - const networkNameElement = labels.value[3] - expect(networkNameElement).not.toBeUndefined() - const netowrkName = await client.elementIdText(networkNameElement.ELEMENT) - - // Get network id - const inputs = await app.elements("//MAIN//INPUT") - const networkItemElement = inputs.value[1].ELEMENT - expect(networkItemElement).not.toBeUndefined() - const networkItemElementId = await client.elementIdAttribute(networkItemElement, 'id') - const networkItemElementName = await client.elementIdAttribute(networkItemElement, 'name') - const networkId = networkItemElementId.value.slice(networkItemElementName.value.length + 1) - - // Delete network - app.deleteNetwork(networkId) - await client.waitUntilWindowLoaded() - await client.pause(3000) - - // Check network name - const networkElement = await client.element('[id=connected-network-name]') - expect(networkElement).not.toBeUndefined() - const newNetworkName = await client.elementIdText(networkElement.value.ELEMENT) - expect(newNetworkName.value).not.toBe(netowrkName.value) - }) -}) diff --git a/packages/neuron-wallet/tests-e2e/notification.test.ts b/packages/neuron-wallet/tests-e2e/notification.test.ts deleted file mode 100644 index 7d7f206bb4..0000000000 --- a/packages/neuron-wallet/tests-e2e/notification.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import Application from './application' - -/** - * 1. check the alert, it should be disconnected to the network - * 2. navigate to wallet settingsState - * 3. delete a wallet - * 4. input a wrong password - * 5. check the alert, it should be incorrect password - * 6. check the notification, it should have two messages - * 1. incorrect PasswordRequest - * 2. disconnected to the network - * 7. password-incorrect alerts should be dismissed once a correct one is inputted - */ -describe('Notification tests', () => { - const app = new Application() - beforeEach(() => app.start()) - afterEach(() => app.stop()) - - beforeEach(async () => { - await app.spectron.client.waitUntilWindowLoaded() - await app.createWalletFromWizard() - - app.spectron.client.click('button[name=Addresses]') - await app.spectron.client.waitUntilWindowLoaded() - await app.gotoSettingsView() - - app.spectron.client.click('button[name=Wallets]') - await app.spectron.client.waitUntilWindowLoaded() - }) - - describe('Test alert message and notification', () => { - const messages = { - disconnected: 'Fail to connect to the node', - incorrectPassword: 'Password is incorrect', - } - - app.test('It should have an alert message of disconnection', async () => { - const { client } = app.spectron - const alertComponent = await client.$('.ms-MessageBar-text') - const msg = await client.elementIdText(alertComponent.value.ELEMENT) - expect(msg.value).toBe(messages.disconnected) - }) - - app.test('It should have an alert message of incorrect password', async () => { - const { client } = app.spectron - await app.clickMenu(['Wallet', 'Delete Current Wallet']) - await client.waitUntilWindowLoaded() - const inputElement = await client.$('input') - await client.elementIdValue(inputElement.value.ELEMENT, 'Invalid Password') - client.click('button[type=submit]') - await client.waitUntilWindowLoaded() - await client.pause(3000) - const alertComponent = await client.$('.ms-MessageBar-text') - const msg = await client.elementIdText(alertComponent.value.ELEMENT) - expect(msg.value).toBe(messages.incorrectPassword) - }) - - app.test('It should have two messages in the notification', async () => { - const { client } = app.spectron - const messageComponents = await client.$$('.ms-Panel-content p') - expect(messageComponents.length).toBe(Object.keys(messages).length) - const incorrectPasswordMsg = await client.element(`//P[text()="${messages.incorrectPassword}"]`) - const disconnectMsg = await client.element(`//P[text()="${messages.disconnected}"]`) - expect(incorrectPasswordMsg.state).not.toBe('failure') - expect(disconnectMsg.state).not.toBe('failed') - }) - - app.test('Password-incorrect alerts should be dismissed once a correct one is inputted', async () => { - const { client } = app.spectron - await app.clickMenu(['Wallet', 'Delete Current Wallet']) - await client.waitUntilWindowLoaded() - const inputElement = await client.$('input') - await client.elementIdValue(inputElement.value.ELEMENT, 'Azusa2233') - client.click('button[type=submit]') - await client.waitUntilWindowLoaded() - await client.pause(3000) - const alertComponent = await client.$('.ms-MessageBar--error') - const msg = await client.elementIdText(alertComponent.value.ELEMENT) - expect(msg.value).toBe(messages.disconnected) - }) - }) -} diff --git a/packages/neuron-wallet/tests-e2e/operations/check-network-status.ts b/packages/neuron-wallet/tests-e2e/operations/check-network-status.ts deleted file mode 100644 index 2ff50c8625..0000000000 --- a/packages/neuron-wallet/tests-e2e/operations/check-network-status.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Application from '../application' - -export const checkNetworkStatus = async (app: Application) => { - const { client } = app.spectron - let retryCount = 5 - let connected = false - while (!connected && retryCount > 0) { - retryCount -= 1 - const networkStateElement = await client.element('//FOOTER/DIV/DIV[2]//I') - expect(networkStateElement.value).not.toBeNull() - const state = await client.elementIdAttribute(networkStateElement.value.ELEMENT, 'data-icon-name') - console.log(`network state ${state.value}`); - if (state.value === 'Disconnected') { - connected = false - await client.pause(1000) - } else if (state.value === 'Connected') { - connected = true - break - } - } - return connected -} diff --git a/packages/neuron-wallet/tests-e2e/operations/create-wallet.ts b/packages/neuron-wallet/tests-e2e/operations/create-wallet.ts deleted file mode 100644 index 84f4a95f36..0000000000 --- a/packages/neuron-wallet/tests-e2e/operations/create-wallet.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Application from '../application' -import { importWallet } from './import-wallet' - -export const createWallet = async (app: Application, name: string | undefined = undefined, password: string = 'Azusa2233') => { - const { client } = app.spectron - - // Copy mnemonic - const mnemonicTextarea = await client.element('