From 4d98f374f8b36900e4167e40c4a1bc7163685c19 Mon Sep 17 00:00:00 2001 From: Keith Date: Fri, 23 Aug 2019 11:10:37 +0800 Subject: [PATCH 01/12] feat(neuron-ui): add error codes --- packages/neuron-ui/src/utils/const.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 1dfa352b08..6e9ff3378d 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -77,3 +77,23 @@ export const FULL_SCREENS = [ `${Routes.WalletEditor}/`, `${Routes.NetworkEditor}/`, ] + +export enum ErrorCode { + // Errors from RPC + ErrorFromRPC = -3, + // Errors from neuron-wallet + CapacityNotEnough = 100, + CapacityTooSmall = 101, + FieldIsInvalid = 102, + // Parameter validation errors from neuron-ui + FieldRequired = 201, + FieldUsed = 202, + FieldTooLong = 203, + FieldTooShort = 204, + FieldInvalid = 205, + // Other errors + FieldIrremovable = 301, + FailToLaunch = 302, + FieldNotFound = 303, + CameraUnavailable = 304, +} From 9acd82170fc76af76e81f1c0dbc36ce2396632f3 Mon Sep 17 00:00:00 2001 From: Keith Date: Fri, 23 Aug 2019 16:55:04 +0800 Subject: [PATCH 02/12] refactor(neuron-ui): use error codes instead of error messages --- .../src/components/ImportKeystore/index.tsx | 19 ++- .../src/components/NetworkEditor/hooks.ts | 118 ++++++++++++------ .../src/components/Overview/index.tsx | 9 +- .../neuron-ui/src/components/Send/hooks.ts | 83 ++++++++---- .../src/components/Transaction/index.tsx | 11 +- .../src/components/WalletEditor/index.tsx | 4 +- .../src/components/WalletWizard/index.tsx | 4 +- .../src/containers/Notification/index.tsx | 12 +- packages/neuron-ui/src/locales/en.json | 36 +++++- packages/neuron-ui/src/locales/zh.json | 8 +- .../remote/controllerMethodWrapper.ts | 26 ++-- .../stateProvider/actionCreators/app.ts | 12 +- .../stateProvider/actionCreators/settings.ts | 12 +- .../actionCreators/transactions.ts | 16 ++- .../stateProvider/actionCreators/wallets.ts | 96 +++++++++++--- packages/neuron-ui/src/types/App/index.d.ts | 10 +- packages/neuron-ui/src/utils/const.ts | 24 ++-- .../neuron-ui/src/widgets/QRScanner/index.tsx | 3 +- .../neuron-wallet/src/decorators/errors.ts | 2 +- 19 files changed, 350 insertions(+), 155 deletions(-) diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index 8361bb9ff4..a9cbc8b3fe 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -7,10 +7,17 @@ import { importWalletWithKeystore } from 'states/stateProvider/actionCreators' import { StateWithDispatch } from 'states/stateProvider/reducer' import { useGoBack } from 'utils/hooks' import generateWalletName from 'utils/generateWalletName' +import { ErrorCode } from 'utils/const' -const defaultFields = { +interface KeystoreFields { + path: string + name: string | undefined + password: string +} + +const defaultFields: KeystoreFields = { path: '', - name: '', + name: undefined, password: '', } @@ -25,7 +32,7 @@ const ImportKeystore = (props: React.PropsWithoutRef { - if (fields.name === '') { + if (fields.name === undefined) { const name = generateWalletName(wallets, wallets.length + 1, t) setFields({ ...fields, @@ -60,7 +67,7 @@ const ImportKeystore = (props: React.PropsWithoutRef { importWalletWithKeystore({ - name: fields.name, + name: fields.name || '', keystorePath: fields.path, password: fields.password, })(dispatch, history) @@ -82,10 +89,10 @@ const ImportKeystore = (props: React.PropsWithoutRef { if (text === '') { - return t('messages.is-required', { field: t(`import-keystore.label.${key}`) }) + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: `keystore-${key}` }) } if (key === 'name' && isNameUsed) { - return t('messages.is-used', { field: t(`import-keystore.label.${key}`) }) + return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: `name`, fieldValue: text }) } return '' }} diff --git a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts index f65f54ee02..d432d5e565 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts +++ b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts @@ -3,7 +3,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import { StateDispatch } from 'states/stateProvider/reducer' import { createNetwork, updateNetwork, addNotification } from 'states/stateProvider/actionCreators' -import { Message, MAX_NETWORK_NAME_LENGTH } from 'utils/const' +import { MAX_NETWORK_NAME_LENGTH, ErrorCode } from 'utils/const' import i18n from 'utils/i18n' @@ -69,8 +69,12 @@ export const useInitialize = ( initialize(network) } else { addNotification({ - type: 'warning', - content: i18n.t('messages.network-is-not-found'), + type: 'warning' as State.MessageType, + timestamp: +new Date(), + code: ErrorCode.FieldNotFound, + meta: { + fieldName: 'network', + }, }) } } @@ -87,13 +91,13 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any placeholder: PlaceHolder.URL, onGetErrorMessage: (url: string) => { if (!url) { - return t('messages.url-required') + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'remote' }) } if (!/^https?:\/\//.test(url)) { - return t('messages.rpc-url-should-have-protocol') + return t(`messages.codes.${ErrorCode.ProtocolRequired}`, { fieldName: 'remote', fieldValue: url }) } if (/\s/.test(url)) { - return t('messages.rpc-url-should-have-no-whitespaces') + return t(`messages.codes.${ErrorCode.NoWhiteSpaces}`, { fieldName: 'remote' }) } return '' }, @@ -105,10 +109,17 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any placeholder: PlaceHolder.Name, onGetErrorMessage: (name: string) => { if (!name) { - return t('messages.name-required') + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'name' }) } if (usedNetworkNames.includes(name)) { - return t('messages.network-name-used') + return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: 'name', fieldValue: name }) + } + if (name.length > MAX_NETWORK_NAME_LENGTH) { + return t(`messages.codes.${ErrorCode.FieldTooLong}`, { + fieldName: 'name', + fieldValue: name, + length: MAX_NETWORK_NAME_LENGTH, + }) } return '' }, @@ -136,44 +147,67 @@ export const useHandleSubmit = ( dispatch: StateDispatch ) => useCallback(async () => { - const warning = { - type: 'warning' as 'warning', - timestamp: Date.now(), - content: '', - } + let errorMessage: State.Message | undefined if (!name) { - return addNotification({ - ...warning, - content: i18n.t(Message.NameRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldRequired, + meta: { + fieldName: 'name', + }, + } + return addNotification(errorMessage)(dispatch) } if (name.length > MAX_NETWORK_NAME_LENGTH) { - return addNotification({ - ...warning, - content: i18n.t(Message.LengthOfNameShouldBeLessThanOrEqualTo, { - length: MAX_NETWORK_NAME_LENGTH, - }), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldTooLong, + meta: { + fieldName: 'name', + fieldValue: name, + length: `${MAX_NETWORK_NAME_LENGTH}`, + }, + } + return addNotification(errorMessage)(dispatch) } if (!remote) { - return addNotification({ - ...warning, - content: i18n.t(Message.URLRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldRequired, + meta: { + fieldName: 'remote', + }, + } + return addNotification(errorMessage)(dispatch) } if (!remote.startsWith('http')) { - return addNotification({ - ...warning, - content: i18n.t(Message.ProtocolRequired), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.ProtocolRequired, + meta: { + fieldName: 'remote', + fieldValue: remote, + }, + } + return addNotification(errorMessage)(dispatch) } // verification, for now, only name is unique if (id === 'new') { if (networks.some(network => network.name === name)) { - return addNotification({ - ...warning, - content: i18n.t(Message.NetworkNameUsed), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldUsed, + meta: { + fieldName: 'name', + fieldValue: name, + }, + } + return addNotification(errorMessage)(dispatch) } return createNetwork({ name, @@ -181,10 +215,16 @@ export const useHandleSubmit = ( })(dispatch, history) } if (networks.some(network => network.name === name && network.id !== id)) { - return addNotification({ - ...warning, - content: i18n.t(Message.NetworkNameUsed), - })(dispatch) + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldUsed, + meta: { + fieldName: 'name', + fieldValue: name, + }, + } + return addNotification(errorMessage)(dispatch) } return updateNetwork({ networkID: id!, diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index f004c09610..cc7eca0a0d 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -28,7 +28,7 @@ import { updateTransactionList, addPopup } from 'states/stateProvider/actionCrea import { showTransactionDetails, showErrorMessage } from 'services/remote' import { localNumberFormatter, shannonToCKBFormatter, uniformTimeFormatter as timeFormatter } from 'utils/formatters' -import { PAGE_SIZE, Routes, CONFIRMATION_THRESHOLD } from 'utils/const' +import { PAGE_SIZE, Routes, CONFIRMATION_THRESHOLD, ErrorCode } from 'utils/const' import { backToTop } from 'utils/animations' const TITLE_FONT_SIZE = 'xxLarge' @@ -284,7 +284,10 @@ const Overview = ({ hideMinerInfo() addPopup('lock-arg-copied')(dispatch) } else { - showErrorMessage(t('messages.error'), t('messages.can-not-find-the-default-address')) + showErrorMessage( + t(`messages.error`), + t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: `default-address` }) + ) } }, [defaultAddress, t, hideMinerInfo, dispatch]) @@ -409,7 +412,7 @@ const Overview = ({ ) : ( - {t('messages.can-not-find-the-default-address')} + {t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: `default-address` })} )} diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 0d14c1955e..eaa00296c7 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -2,61 +2,90 @@ import React, { useCallback, useEffect } from 'react' import { IDropdownOption } from 'office-ui-fabric-react' import { AppActions, StateDispatch } from 'states/stateProvider/reducer' +import { addNotification } from 'states/stateProvider/actionCreators' import { calculateCycles } from 'services/remote/wallets' -import { Message, MAX_DECIMAL_DIGITS } from 'utils/const' +import { MAX_DECIMAL_DIGITS, ErrorCode } from 'utils/const' import { verifyAddress, verifyAmountRange } from 'utils/validators' import { outputsToTotalCapacity } from 'utils/formatters' import { TransactionOutput } from '.' let cyclesTimer: ReturnType -const validateTransactionParams = ({ items, dispatch }: { items: TransactionOutput[]; dispatch?: StateDispatch }) => { - const errorAction = { - type: AppActions.AddNotification, - payload: { - type: 'warning', - timestamp: Date.now(), - content: Message.AtLeastOneAddressNeeded, - meta: {}, - }, - } - if (!items.length || !items[0].address) { - if (dispatch) { - dispatch(errorAction) - } - return false - } +const validateTransactionParams = ({ + items = [], + dispatch, +}: { + items: TransactionOutput[] + dispatch?: StateDispatch +}) => { + let errorMessage: State.Message | undefined + const invalid = items.some( (item): boolean => { + if (!item.address) { + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.AddressIsEmpty, + } + return true + } const isAddressValid = verifyAddress(item.address) if (typeof isAddressValid === 'string') { - errorAction.payload.content = Message.InvalidAddress - errorAction.payload.meta = { address: item.address } + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.FieldInvalid, + meta: { + fieldName: 'address', + fieldValue: item.address, + }, + } return true } if (Number.isNaN(+item.amount) || +item.amount < 0) { - errorAction.payload.content = Message.InvalidAmount - errorAction.payload.meta = { amount: item.amount } + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.NotNegative, + meta: { + fieldName: 'amount', + fieldValue: item.amount || '0', + }, + } return true } const [, decimal = ''] = item.amount.split('.') if (decimal.length > MAX_DECIMAL_DIGITS) { - errorAction.payload.content = Message.DecimalExceed - errorAction.payload.meta = { amount: item.amount } + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.DecimalExceed, + meta: { + fieldName: 'amount', + fieldValue: item.amount, + }, + } return true } if (!verifyAmountRange(item.amount)) { - errorAction.payload.content = Message.AmountTooSmall - errorAction.payload.meta = { amount: item.amount } + errorMessage = { + type: 'warning', + timestamp: +new Date(), + code: ErrorCode.AmountTooSmall, + meta: { + amount: item.amount || '0', + }, + } return true } return false } ) - if (invalid) { + if (invalid && errorMessage) { if (dispatch) { - dispatch(errorAction) + addNotification(errorMessage)(dispatch) } return false } diff --git a/packages/neuron-ui/src/components/Transaction/index.tsx b/packages/neuron-ui/src/components/Transaction/index.tsx index 1655afa835..1cd2edc30e 100644 --- a/packages/neuron-ui/src/components/Transaction/index.tsx +++ b/packages/neuron-ui/src/components/Transaction/index.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Stack, DetailsList, Text, CheckboxVisibility, IColumn } from 'office-ui-fabric-react' import { currentWallet as currentWalletCache } from 'utils/localCache' -import { getTransaction } from 'services/remote' +import { getTransaction, showErrorMessage } from 'services/remote' import { transactionState } from 'states/initStates/chain' import { localNumberFormatter, uniformTimeFormatter, shannonToCKBFormatter } from 'utils/formatters' +import { ErrorCode } from 'utils/const' const MIN_CELL_WIDTH = 70 @@ -103,7 +104,9 @@ const Transaction = () => { if (res.status) { setTransaction(res.result) } else { - throw new Error(res.message.title) + // TODO: use error code + showErrorMessage(t(`messages.error`), t(`messages.transaction-not-found`)) + window.close() } }) .catch((err: Error) => { @@ -113,7 +116,7 @@ const Transaction = () => { }) }) } - }, []) + }, [t]) useEffect(() => { window.addEventListener('storage', (e: StorageEvent) => { @@ -147,7 +150,7 @@ const Transaction = () => { if (error.code) { return ( - {error.message || t('messages.transaction-not-found')} + {error.message || t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' })} ) } diff --git a/packages/neuron-ui/src/components/WalletEditor/index.tsx b/packages/neuron-ui/src/components/WalletEditor/index.tsx index ecc6844f07..ab0bca302f 100644 --- a/packages/neuron-ui/src/components/WalletEditor/index.tsx +++ b/packages/neuron-ui/src/components/WalletEditor/index.tsx @@ -5,7 +5,7 @@ import { Stack, TextField, PrimaryButton, DefaultButton } from 'office-ui-fabric import { StateWithDispatch } from 'states/stateProvider/reducer' -import { Routes } from 'utils/const' +import { Routes, ErrorCode } from 'utils/const' import { useGoBack } from 'utils/hooks' import { useAreParamsValid, useOnConfirm, useInputs, useWalletEditor } from './hooks' @@ -14,7 +14,7 @@ const WalletNotFound = () => { const [t] = useTranslation() return (
-

{t('messages.wallet-is-not-found')}

+

{t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'wallet' })}

{`${t('navbar.settings')}-${t('settings.setting-tabs.wallets')}`} diff --git a/packages/neuron-ui/src/components/WalletWizard/index.tsx b/packages/neuron-ui/src/components/WalletWizard/index.tsx index e8119349b5..7979c29818 100644 --- a/packages/neuron-ui/src/components/WalletWizard/index.tsx +++ b/packages/neuron-ui/src/components/WalletWizard/index.tsx @@ -16,7 +16,7 @@ import withWizard, { WizardElementProps, WithWizardState } from 'components/with import { generateMnemonic, validateMnemonic, showErrorMessage } from 'services/remote' import { createWalletWithMnemonic, importWalletWithMnemonic } from 'states/stateProvider/actionCreators' -import { Routes, MnemonicAction } from 'utils/const' +import { Routes, MnemonicAction, ErrorCode } from 'utils/const' import { buttonGrommetIconStyles } from 'utils/icons' import { verifyPasswordComplexity } from 'utils/validators' import generateWalletName from 'utils/generateWalletName' @@ -160,7 +160,7 @@ const Mnemonic = ({ }` ) } else { - showErrorMessage(t('messages.error'), t('messages.invalid-mnemonic')) + showErrorMessage(t(`messages.error`), t(`messages.codes.${ErrorCode.FieldInvalid}`, { fieldName: 'mnemonic' })) } } }, [isCreate, history, rootPath, type, imported, t, dispatch]) diff --git a/packages/neuron-ui/src/containers/Notification/index.tsx b/packages/neuron-ui/src/containers/Notification/index.tsx index e7e5c83341..72b0afe3dc 100644 --- a/packages/neuron-ui/src/containers/Notification/index.tsx +++ b/packages/neuron-ui/src/containers/Notification/index.tsx @@ -83,7 +83,7 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef - {showTopAlert && notificationsInDesc.length ? ( + {showTopAlert && notification ? ( } > - {t(notification.content, notification.meta)} + {notification.code + ? t(`messages.codes.${notification.code}`, notification.meta) + : notification.content || 'Unknonw'} ) : null} @@ -154,7 +156,11 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef - {t(n.content, n.meta)} + + {notification.code + ? t(`messages.codes.${notification.code}`, notification.meta) + : notification.content || 'Unknonw'} + ) })} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 434e14f877..5c245793d9 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -278,7 +278,41 @@ "invalid-address": "{{address}} is an invalid address", "amount-decimal-exceed": "The amount {{amount}} CKB is invalid, please enter an amount with no more than 8 decimal places", "invalid-amount": "The amount {{amount}} CKB is invalid", - "amount-too-small": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB" + "amount-too-small": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB", + "fields": { + "wallet": "Wallet", + "name": "Name", + "remote": "RPC URL", + "network": "Network", + "address": "Address", + "amount": "Amount", + "transaction": "Transaction", + "default-address": "Default Address", + "mnemonic": "Mnemonic", + "keystore-path": "Keystore File", + "keystore-name": "Wallet name", + "keystore-password": "Password" + }, + "codes": { + "-3": "", + "100": "Amount is not enough", + "101": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB", + "102": "$t(messages.fields.{{fieldName}}) is invalid", + "201": "$t(messages.fields.{{fieldName}}) is required", + "202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is used", + "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is too long, it should be shorter than or equal to {{length}}", + "204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is too short, it should be longer than or equal to {{length}}", + "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid", + "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, please enter the $t(messages.fields.{{fieldName}}) with no more than {{length}} decimal places", + "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, it cannot be negative", + "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is invalid, it should start with http(s)://", + "209": "$t(messages.fields.{{fieldName}}) should have no whitespaces", + "301": "$t(messages.fields.{{fieldName}}) {{fieldValue}} is irremovable", + "302": "Fail to launch the app", + "303": "$t(messages.fields.{{fieldName}}) is not found", + "304": "Camera is unavailable or disabled", + "305": "$t(messages.fields.address) cannot be empty" + } }, "sync": { "syncing": "Syncing", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 507f6ead45..10b5931495 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -278,7 +278,13 @@ "invalid-address": "{{address}} 是无效的地址", "amount-decimal-exceed": "金额 {{amount}} 是个无效的值, 其小数位应不超过 8 位", "invalid-amount": "金额 {{amount} 是无效的数字", - "amount-too-small": "金额 {{amount}} 太小, 请输入一个不小于 61 CKB 的值" + "amount-too-small": "金额 {{amount}} 太小, 请输入一个不小于 61 CKB 的值", + "fields": { + "name": "Name" + }, + "codes": { + "201": "缺少{{fieldName}} $t('messages.fields.name')" + } }, "sync": { "syncing": "同步中", diff --git a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts index 6bef71266c..8f9cfa13bb 100644 --- a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts +++ b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts @@ -6,8 +6,9 @@ interface SuccessFromController { interface FailureFromController { status: 0 message: { - title: string + code?: number content?: string + meta?: { [key: string]: string } } } export type ControllerResponse = SuccessFromController | FailureFromController @@ -15,19 +16,27 @@ export type ControllerResponse = SuccessFromController | FailureFromController export const RemoteNotLoadError = { status: 0 as 0, message: { - title: 'remote is not supported', + content: 'remote is not supported', }, } export const controllerNotLoaded = (controllerName: string) => ({ status: 0 as 0, message: { - title: `${controllerName} controller not loaded`, + content: `${controllerName} controller not loaded`, }, }) export const controllerMethodWrapper = (controllerName: string) => ( - callControllerMethod: (controller: any) => (params: any) => Promise<{ status: any; result: any; msg: string }> + callControllerMethod: ( + controller: any + ) => ( + params: any + ) => Promise<{ + status: any + result: any + message: { code?: number; content?: string; meta?: { [key: string]: string } } + }> ) => async (realParams?: any): Promise => { if (!window.remote) { return RemoteNotLoadError @@ -57,16 +66,9 @@ export const controllerMethodWrapper = (controllerName: string) => ( } } - let title = '' - - if (typeof res === 'string') { - title = res - } else if (typeof res.msg === 'string') { - title = res.msg - } return { status: 0, - message: { title }, + message: res.message || {}, } } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts index ff5aa97ce5..d7f6c4ffef 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/app.ts @@ -1,7 +1,7 @@ import { NeuronWalletActions, AppActions, StateDispatch } from 'states/stateProvider/reducer' import { getNeuronWalletState } from 'services/remote' import initStates from 'states/initStates' -import { Routes } from 'utils/const' +import { Routes, ErrorCode } from 'utils/const' import { WalletWizardPath } from 'components/WalletWizard' import { addressesToBalance } from 'utils/formatters' import { @@ -73,16 +73,10 @@ export const addPopup = (text: string) => (dispatch: StateDispatch) => { }, 8000) } -export const addNotification = ({ type, content }: { type: 'alert' | 'warning'; content: string }) => ( - dispatch: StateDispatch -) => { +export const addNotification = (message: State.Message) => (dispatch: StateDispatch) => { dispatch({ type: AppActions.AddNotification, - payload: { - type, - content, - timestamp: Date.now(), - }, + payload: message, }) } export const dismissNotification = (timestamp: number) => (dispatch: StateDispatch) => { diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts index 2fc4283507..b571e2a3e0 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts @@ -16,14 +16,12 @@ export const toggleAddressBook = () => { export const createNetwork = (params: Controller.CreateNetworkParams) => (dispatch: StateDispatch, history: any) => { createRemoteNetwork(params).then(res => { if (res.status) { - dispatch({ - type: AppActions.Ignore, - payload: null, - }) addPopup('create-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ type: 'alert', timestamp: +new Date(), code: res.message.code, content: res.message.content })( + dispatch + ) } }) } @@ -34,7 +32,9 @@ export const updateNetwork = (params: Controller.UpdateNetworkParams) => (dispat addPopup('update-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ type: 'alert', timestamp: +new Date(), code: res.message.code, content: res.message.content })( + dispatch + ) } }) } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts index 8aed69669c..0cdf49e7c3 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts @@ -14,7 +14,13 @@ export const updateTransactionList = (params: GetTransactionListParams) => (disp payload: res.result, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -39,7 +45,13 @@ export const updateTransactionDescription = (params: Controller.UpdateTransactio }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) .finally(() => { diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts index 3b5f3c6753..bb8ffa6fda 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts @@ -36,7 +36,13 @@ export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) }) currentWalletCache.save(payload) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -48,8 +54,12 @@ export const createWalletWithMnemonic = (params: Controller.ImportMnemonicParams createWallet(params).then(res => { if (res.status) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.message) { + if (res.message.code) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) + } else if (res.message.content) { + showErrorMessage(i18n.t(`messages.error`), res.message.content) + } } }) } @@ -61,8 +71,12 @@ export const importWalletWithMnemonic = (params: Controller.ImportMnemonicParams importMnemonic(params).then(res => { if (res.status) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.message) { + if (res.message.code) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) + } else if (res.message.content) { + showErrorMessage(i18n.t(`messages.error`), res.message.content) + } } }) } @@ -74,8 +88,12 @@ export const importWalletWithKeystore = (params: Controller.ImportKeystoreParams importKeystore(params).then(res => { if (res.status) { history.push(Routes.Overview) - } else { - showErrorMessage(i18n.t('messages.error'), i18n.t(res.message.title)) + } else if (res.message) { + if (res.message.code) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) + } else if (res.message.content) { + showErrorMessage(i18n.t(`messages.error`), res.message.content) + } } }) } @@ -93,7 +111,13 @@ export const updateWalletList = () => (dispatch: StateDispatch, history: any) => }) walletsCache.save(payload) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -109,7 +133,13 @@ export const updateWalletProperty = (params: Controller.UpdateWalletParams) => ( history.push(Routes.SettingsWallets) } } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -121,7 +151,13 @@ export const setCurrentWallet = (id: string) => (dispatch: StateDispatch) => { payload: null, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -139,7 +175,13 @@ export const sendTransaction = (params: Controller.SendTransaction) => (dispatch history.push(Routes.History) } else { // TODO: the pretreatment is unnecessary once the error code is implemented - addNotification({ type: 'alert', content: res.message.title.replace(/(\b"|"\b)/g, '') })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: (res.message.content || '').replace(/(\b"|"\b)/g, ''), + meta: res.message.meta, + })(dispatch) } dispatch({ type: AppActions.DismissPasswordRequest, @@ -168,7 +210,13 @@ export const updateAddressListAndBalance = (params: Controller.GetAddressesByWal payload: { addresses, balance }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -193,7 +241,13 @@ export const updateAddressDescription = (params: Controller.UpdateAddressDescrip }, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) .finally(() => { @@ -215,7 +269,13 @@ export const deleteWallet = (params: Controller.DeleteWalletParams) => (dispatch if (res.status) { addPopup('delete-wallet-successfully')(dispatch) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } @@ -232,7 +292,13 @@ export const backupWallet = (params: Controller.BackupWalletParams) => (dispatch payload: null, }) } else { - addNotification({ type: 'alert', content: res.message.title })(dispatch) + addNotification({ + type: 'alert', + timestamp: +new Date(), + code: res.message.code, + content: res.message.content, + meta: res.message.meta, + })(dispatch) } }) } diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 2cfb486745..35964b6440 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -47,11 +47,13 @@ declare namespace State { amount: string unit: any } - interface Message { - type: 'success' | 'warning' | 'alert' + type MessageType = 'success' | 'warning' | 'alert' + interface Message { + type: MessageType timestamp: number - content: string - meta?: { [key: string]: string } + code?: Code + content?: string + meta?: Meta } interface Send { txID: string diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 6e9ff3378d..0efad87278 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -49,20 +49,6 @@ export const PlaceHolders = { }, } -export enum Message { - NameRequired = 'messages.name-required', - URLRequired = 'messages.url-required', - LengthOfNameShouldBeLessThanOrEqualTo = 'messages.length-of-name-should-be-less-than-or-equal-to', - NetworkNameUsed = 'messages.network-name-used', - AtLeastOneAddressNeeded = 'messages.at-least-one-address-needed', - InvalidAddress = 'messages.invalid-address', - InvalidAmount = 'messages.invalid-amount', - DecimalExceed = 'messages.amount-decimal-exceed', - IsUnremovable = 'messages.is-unremovable', - ProtocolRequired = 'messages.protocol-required', - AmountTooSmall = 'messages.amount-too-small', -} - export enum MnemonicAction { Create = 'create', Verify = 'verify', @@ -82,18 +68,22 @@ export enum ErrorCode { // Errors from RPC ErrorFromRPC = -3, // Errors from neuron-wallet - CapacityNotEnough = 100, - CapacityTooSmall = 101, - FieldIsInvalid = 102, + AmountNotEnough = 100, + AmountTooSmall = 101, // Parameter validation errors from neuron-ui FieldRequired = 201, FieldUsed = 202, FieldTooLong = 203, FieldTooShort = 204, FieldInvalid = 205, + DecimalExceed = 206, + NotNegative = 207, + ProtocolRequired = 208, + NoWhiteSpaces = 209, // Other errors FieldIrremovable = 301, FailToLaunch = 302, FieldNotFound = 303, CameraUnavailable = 304, + AddressIsEmpty = 305, } diff --git a/packages/neuron-ui/src/widgets/QRScanner/index.tsx b/packages/neuron-ui/src/widgets/QRScanner/index.tsx index e2b3fcc942..067963af32 100644 --- a/packages/neuron-ui/src/widgets/QRScanner/index.tsx +++ b/packages/neuron-ui/src/widgets/QRScanner/index.tsx @@ -14,6 +14,7 @@ import jsQR from 'jsqr' import { showErrorMessage } from 'services/remote' import { drawPolygon } from 'utils/canvasActions' import { verifyAddress } from 'utils/validators' +import { ErrorCode } from 'utils/const' interface QRScannerProps { title: string @@ -100,7 +101,7 @@ const QRScanner = ({ title, label, onConfirm, styles }: QRScannerProps) => { requestAnimationFrame(tick) }) .catch((err: Error) => { - showErrorMessage(t('messages.camera-not-available-or-disabled'), err.message) + showErrorMessage(t(`messages.codes.${ErrorCode.CameraUnavailable}`), err.message) setOpen(false) }) }, [video, t, onConfirm, onDismiss]) diff --git a/packages/neuron-wallet/src/decorators/errors.ts b/packages/neuron-wallet/src/decorators/errors.ts index 93df8d62b0..fd41954793 100644 --- a/packages/neuron-wallet/src/decorators/errors.ts +++ b/packages/neuron-wallet/src/decorators/errors.ts @@ -10,7 +10,7 @@ export const CatchControllerError = (_target: any, _name: string, descriptor: Pr } catch (err) { return { status: ResponseCode.Fail, - msg: err.message, + message: { content: err.message }, } } }, From 6e8cd17c983efd03550283794073aeee44f6ae76 Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 16:23:14 +0800 Subject: [PATCH 03/12] feat(neuron-ui): add error messages on the Send View --- .../src/components/ImportKeystore/index.tsx | 9 +- .../src/components/NetworkEditor/index.tsx | 4 +- .../neuron-ui/src/components/Send/hooks.ts | 186 +++++++----------- .../neuron-ui/src/components/Send/index.tsx | 39 ++-- .../src/components/WalletEditor/hooks.ts | 17 +- .../src/components/WalletEditor/index.tsx | 13 +- .../src/components/WalletWizard/index.tsx | 21 +- .../remote/controllerMethodWrapper.ts | 15 +- .../stateProvider/actionCreators/settings.ts | 13 +- .../actionCreators/transactions.ts | 17 +- .../stateProvider/actionCreators/wallets.ts | 117 ++++------- packages/neuron-ui/src/utils/const.ts | 1 + packages/neuron-ui/src/utils/formatters.ts | 11 ++ packages/neuron-ui/src/utils/validators.ts | 34 +++- .../src/controllers/wallets/index.ts | 4 +- .../neuron-wallet/src/decorators/errors.ts | 2 +- .../src/types/controller/index.d.ts | 7 +- 17 files changed, 249 insertions(+), 261 deletions(-) diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index a9cbc8b3fe..bf094012f6 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -7,7 +7,7 @@ import { importWalletWithKeystore } from 'states/stateProvider/actionCreators' import { StateWithDispatch } from 'states/stateProvider/reducer' import { useGoBack } from 'utils/hooks' import generateWalletName from 'utils/generateWalletName' -import { ErrorCode } from 'utils/const' +import { ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' interface KeystoreFields { path: string @@ -77,6 +77,12 @@ const ImportKeystore = (props: React.PropsWithoutRef {Object.entries(fields).map(([key, value]) => { + let maxLength: number | undefined + if (key === 'name') { + maxLength = MAX_WALLET_NAME_LENGTH + } else if (key === 'password') { + maxLength = MAX_PASSWORD_LENGTH + } return ( { diff --git a/packages/neuron-ui/src/components/NetworkEditor/index.tsx b/packages/neuron-ui/src/components/NetworkEditor/index.tsx index d7b0db0889..e61ae39b3e 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/index.tsx +++ b/packages/neuron-ui/src/components/NetworkEditor/index.tsx @@ -41,9 +41,9 @@ const NetworkEditor = ({ key={inputProps.label} required validateOnLoad={false} - onNotifyValidationResult={(msg: any) => { + onNotifyValidationResult={(message: any) => { const errs = [...errors] - errs.splice(idx, 1, msg !== '') + errs.splice(idx, 1, message !== '') setErrors(errs) }} /> diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index eaa00296c7..4dfe9d2437 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -1,95 +1,25 @@ -import React, { useCallback, useEffect } from 'react' -import { IDropdownOption } from 'office-ui-fabric-react' +import React, { useState, useCallback, useEffect, useMemo } from 'react' import { AppActions, StateDispatch } from 'states/stateProvider/reducer' -import { addNotification } from 'states/stateProvider/actionCreators' import { calculateCycles } from 'services/remote/wallets' -import { MAX_DECIMAL_DIGITS, ErrorCode } from 'utils/const' -import { verifyAddress, verifyAmountRange } from 'utils/validators' -import { outputsToTotalCapacity } from 'utils/formatters' +import { outputsToTotalCapacity, priceToFee } from 'utils/formatters' +import { verifyAddress, verifyAmount, verifyAmountRange } from 'utils/validators' +import { ErrorCode } from 'utils/const' import { TransactionOutput } from '.' let cyclesTimer: ReturnType -const validateTransactionParams = ({ - items = [], - dispatch, -}: { - items: TransactionOutput[] - dispatch?: StateDispatch -}) => { - let errorMessage: State.Message | undefined - - const invalid = items.some( - (item): boolean => { - if (!item.address) { - errorMessage = { - type: 'warning', - timestamp: +new Date(), - code: ErrorCode.AddressIsEmpty, - } - return true - } - const isAddressValid = verifyAddress(item.address) - if (typeof isAddressValid === 'string') { - errorMessage = { - type: 'warning', - timestamp: +new Date(), - code: ErrorCode.FieldInvalid, - meta: { - fieldName: 'address', - fieldValue: item.address, - }, - } - return true - } - if (Number.isNaN(+item.amount) || +item.amount < 0) { - errorMessage = { - type: 'warning', - timestamp: +new Date(), - code: ErrorCode.NotNegative, - meta: { - fieldName: 'amount', - fieldValue: item.amount || '0', - }, - } - return true - } - const [, decimal = ''] = item.amount.split('.') - if (decimal.length > MAX_DECIMAL_DIGITS) { - errorMessage = { - type: 'warning', - timestamp: +new Date(), - code: ErrorCode.DecimalExceed, - meta: { - fieldName: 'amount', - fieldValue: item.amount, - }, - } - return true - } - if (!verifyAmountRange(item.amount)) { - errorMessage = { - type: 'warning', - timestamp: +new Date(), - code: ErrorCode.AmountTooSmall, - meta: { - amount: item.amount || '0', - }, - } - return true - } - return false +const verifyTransactionParams = (items: TransactionOutput[] = []) => { + return !items.some(item => { + if (item.address === '' || verifyAddress(item.address) !== true) { + return true } - ) - if (invalid && errorMessage) { - if (dispatch) { - addNotification(errorMessage)(dispatch) + if (Number.isNaN(+item.amount) || verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { + return true } return false - } - return true + }) } const useUpdateTransactionOutput = (dispatch: StateDispatch) => @@ -100,7 +30,7 @@ const useUpdateTransactionOutput = (dispatch: StateDispatch) => payload: { idx, item: { - [field]: value.trim(), + [field]: value.replace(/\s/, ''), }, }, }) @@ -127,11 +57,17 @@ const useRemoveTransactionOutput = (dispatch: StateDispatch) => [dispatch] ) -const useOnTransactionChange = (walletID: string, items: TransactionOutput[], dispatch: StateDispatch) => { +const useOnTransactionChange = ( + walletID: string, + items: TransactionOutput[], + dispatch: StateDispatch, + setIsTransactionValid: Function +) => { useEffect(() => { clearTimeout(cyclesTimer) cyclesTimer = setTimeout(() => { - if (validateTransactionParams({ items })) { + if (verifyTransactionParams(items)) { + setIsTransactionValid(true) calculateCycles({ walletID, capacities: outputsToTotalCapacity(items), @@ -156,6 +92,7 @@ const useOnTransactionChange = (walletID: string, items: TransactionOutput[], di }) }) } else { + setIsTransactionValid(false) dispatch({ type: AppActions.UpdateSendCycles, payload: '0', @@ -165,10 +102,10 @@ const useOnTransactionChange = (walletID: string, items: TransactionOutput[], di }, [walletID, items, dispatch]) } -const useOnSubmit = (items: TransactionOutput[], dispatch: StateDispatch) => +const useOnSubmit = (items: TransactionOutput[], balance: string, fee: string, dispatch: StateDispatch) => useCallback( (walletID: string = '') => () => { - if (validateTransactionParams({ items, dispatch })) { + if (verifyTransactionParams(items)) { dispatch({ type: AppActions.UpdateTransactionID, payload: null, @@ -182,7 +119,7 @@ const useOnSubmit = (items: TransactionOutput[], dispatch: StateDispatch) => }) } }, - [dispatch, items] + [dispatch, items, balance, fee] ) const useOnItemChange = (updateTransactionOutput: Function) => @@ -193,7 +130,7 @@ const useOnItemChange = (updateTransactionOutput: Function) => ) => { if (undefined !== value) { if (field === 'amount') { - if (Number.isNaN(+value) || /[^\d.]/.test(value)) { + if (Number.isNaN(+value) || /[^\d.]/.test(value) || +value < 0) { return } updateTransactionOutput(field)(idx)(value) @@ -205,16 +142,6 @@ const useOnItemChange = (updateTransactionOutput: Function) => [updateTransactionOutput] ) -const useCapacityUnitChange = (updateTransactionOutput: Function) => - useCallback( - (idx: number = -1) => (_e: React.FormEvent, option?: IDropdownOption) => { - if (option) { - updateTransactionOutput('unit')(idx)(option.key) - } - }, - [updateTransactionOutput] - ) - const useUpdateTransactionPrice = (dispatch: StateDispatch) => useCallback( (_e: React.FormEvent, value?: string) => { @@ -252,40 +179,73 @@ const clear = (dispatch: StateDispatch) => { const useClear = (dispatch: StateDispatch) => useCallback(() => clear(dispatch), [dispatch]) export const useInitialize = ( - address: string, items: TransactionOutput[], + price: string, + cycles: string, + balance: string, dispatch: React.Dispatch, - history: any + t: any ) => { + const fee = useMemo(() => priceToFee(price, cycles), [price, cycles]) // in shannon + const [isTransactionValid, setIsTransactionValid] = useState(false) + const updateTransactionOutput = useUpdateTransactionOutput(dispatch) const onItemChange = useOnItemChange(updateTransactionOutput) - const onCapacityUnitChange = useCapacityUnitChange(updateTransactionOutput) - const onSubmit = useOnSubmit(items, dispatch) const addTransactionOutput = useAddTransactionOutput(dispatch) const removeTransactionOutput = useRemoveTransactionOutput(dispatch) const updateTransactionPrice = useUpdateTransactionPrice(dispatch) const onDescriptionChange = useSendDescriptionChange(dispatch) + const onSubmit = useOnSubmit(items, balance, fee, dispatch) const onClear = useClear(dispatch) - useEffect(() => { - if (address) { - updateTransactionOutput('address')(0)(address) - } - return () => { - clear(dispatch) - } - }, [address, dispatch, history, updateTransactionOutput]) + const onGetAddressErrorMessage = useCallback( + (addr: string) => { + if (addr === '') { + return t(`messages.codes.${ErrorCode.AddressIsEmpty}`) + } + if (!verifyAddress(addr)) { + return t(`messages.codes.${ErrorCode.FieldInvalid}`, { + fieldName: 'address', + fieldValue: addr, + }) + } + return '' + }, + [t] + ) + + const onGetAmountErrorMessage = useCallback( + (text: string) => { + const amount = text || '0' + + const msg = verifyAmount(amount) + if (typeof msg === 'object') { + return t(`messages.codes.${msg.code}`, msg.meta) + } + if (!verifyAmountRange(amount)) { + return t(`messages.codes.${ErrorCode.AmountTooSmall}`, { + amount, + }) + } + + return undefined + }, + [t] + ) return { + fee, + isTransactionValid, + setIsTransactionValid, useOnTransactionChange, - updateTransactionOutput, onItemChange, - onCapacityUnitChange, - onSubmit, addTransactionOutput, removeTransactionOutput, updateTransactionPrice, onDescriptionChange, + onGetAddressErrorMessage, + onGetAmountErrorMessage, + onSubmit, onClear, } } diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index 6f77bee3ee..de66491834 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -21,7 +21,7 @@ import { StateWithDispatch } from 'states/stateProvider/reducer' import appState from 'states/initStates/app' import { PlaceHolders, CapacityUnit } from 'utils/const' -import { shannonToCKBFormatter, priceToFee } from 'utils/formatters' +import { shannonToCKBFormatter } from 'utils/formatters' import { useInitialize } from './hooks' @@ -38,24 +38,24 @@ const Send = ({ }, wallet: { id: walletID = '', balance = '' }, dispatch, - history, - match: { - params: { address = '' }, - }, }: React.PropsWithoutRef>) => { const { t } = useTranslation() const { + fee, + isTransactionValid, + setIsTransactionValid, useOnTransactionChange, - updateTransactionOutput, onItemChange, onSubmit, addTransactionOutput, removeTransactionOutput, updateTransactionPrice, onDescriptionChange, + onGetAddressErrorMessage, + onGetAmountErrorMessage, onClear, - } = useInitialize(address, send.outputs, dispatch, history) - useOnTransactionChange(walletID, send.outputs, dispatch) + } = useInitialize(send.outputs, send.price, send.cycles, balance, dispatch, t) + useOnTransactionChange(walletID, send.outputs, dispatch, setIsTransactionValid) const leftStackWidth = '70%' const labelWidth = '140px' const actionSpacer = ( @@ -78,7 +78,7 @@ const Send = ({ @@ -91,15 +91,17 @@ const Send = ({ value={item.address || ''} onChange={onItemChange('address', idx)} required + validateOnLoad={false} + onGetErrorMessage={onGetAddressErrorMessage} /> - + updateTransactionOutput('address')(idx)(data)} + onConfirm={(data: string) => onItemChange('address', idx)(undefined as any, data)} /> - + @@ -116,7 +118,7 @@ const Send = ({ @@ -130,6 +132,8 @@ const Send = ({ onChange={onItemChange('amount', idx)} disabled={sending} required + validateOnLoad={false} + onGetErrorMessage={onGetAmountErrorMessage} /> @@ -168,7 +172,7 @@ const Send = ({ ) : ( - + )} diff --git a/packages/neuron-ui/src/components/WalletEditor/hooks.ts b/packages/neuron-ui/src/components/WalletEditor/hooks.ts index 38190028cb..8dd4bce01d 100644 --- a/packages/neuron-ui/src/components/WalletEditor/hooks.ts +++ b/packages/neuron-ui/src/components/WalletEditor/hooks.ts @@ -1,6 +1,7 @@ import { useState, useMemo, useCallback } from 'react' import { updateWalletProperty } from 'states/stateProvider/actionCreators' import { StateDispatch } from 'states/stateProvider/reducer' +import { ErrorCode, MAX_WALLET_NAME_LENGTH } from 'utils/const' import i18n from 'utils/i18n' export const useWalletEditor = () => { @@ -28,7 +29,7 @@ export const useInputs = ({ name }: ReturnType) => { ...name, label: i18n.t('settings.wallet-manager.edit-wallet.wallet-name'), placeholder: i18n.t('settings.wallet-manager.edit-wallet.wallet-name'), - maxLength: 20, + maxLength: MAX_WALLET_NAME_LENGTH, }, ], [name] @@ -44,15 +45,21 @@ export const useOnConfirm = (name: string = '', id: string = '', history: any, d }, [name, id, history, dispatch]) } -export const useAreParamsValid = (name: string) => { +export const useHint = (name: string, usedNames: string[], t: Function): string | null => { return useMemo(() => { - return !(name === '') - }, [name]) + if (name === '') { + return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'name' }) + } + if (usedNames.includes(name)) { + return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: 'name', fieldValue: name }) + } + return null + }, [name, usedNames, t]) } export default { useWalletEditor, useInputs, useOnConfirm, - useAreParamsValid, + useHint, } diff --git a/packages/neuron-ui/src/components/WalletEditor/index.tsx b/packages/neuron-ui/src/components/WalletEditor/index.tsx index ab0bca302f..09c71077c1 100644 --- a/packages/neuron-ui/src/components/WalletEditor/index.tsx +++ b/packages/neuron-ui/src/components/WalletEditor/index.tsx @@ -8,7 +8,7 @@ import { StateWithDispatch } from 'states/stateProvider/reducer' import { Routes, ErrorCode } from 'utils/const' import { useGoBack } from 'utils/hooks' -import { useAreParamsValid, useOnConfirm, useInputs, useWalletEditor } from './hooks' +import { useHint, useOnConfirm, useInputs, useWalletEditor } from './hooks' const WalletNotFound = () => { const [t] = useTranslation() @@ -33,6 +33,7 @@ const WalletEditor = ({ const [t] = useTranslation() const wallet = useMemo(() => wallets.find(w => w.id === id), [id, wallets]) || { id: '', name: '' } + const usedNames = wallets.map(w => w.name).filter(n => n !== wallet.name) const editor = useWalletEditor() const { initialize } = editor @@ -42,7 +43,7 @@ const WalletEditor = ({ }, [id, initialize, wallet.name]) const inputs = useInputs(editor) - const areParamsValid = useAreParamsValid(editor.name.value) + const hint = useHint(editor.name.value, usedNames, t) const onConfirm = useOnConfirm(editor.name.value, wallet.id, history, dispatch) const goBack = useGoBack(history) @@ -56,13 +57,17 @@ const WalletEditor = ({ {inputs.map(inputProps => ( - + ))} - + ) diff --git a/packages/neuron-ui/src/components/WalletWizard/index.tsx b/packages/neuron-ui/src/components/WalletWizard/index.tsx index 7979c29818..4de577f8de 100644 --- a/packages/neuron-ui/src/components/WalletWizard/index.tsx +++ b/packages/neuron-ui/src/components/WalletWizard/index.tsx @@ -16,7 +16,7 @@ import withWizard, { WizardElementProps, WithWizardState } from 'components/with import { generateMnemonic, validateMnemonic, showErrorMessage } from 'services/remote' import { createWalletWithMnemonic, importWalletWithMnemonic } from 'states/stateProvider/actionCreators' -import { Routes, MnemonicAction, ErrorCode } from 'utils/const' +import { Routes, MnemonicAction, ErrorCode, MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } from 'utils/const' import { buttonGrommetIconStyles } from 'utils/icons' import { verifyPasswordComplexity } from 'utils/validators' import generateWalletName from 'utils/generateWalletName' @@ -36,15 +36,29 @@ const initState: WithWizardState = { } const submissionInputs = [ - { label: 'name', key: 'name', type: 'text', hint: 'wizard.set-wallet-name', autoFocus: false }, + { + label: 'name', + key: 'name', + type: 'text', + hint: 'wizard.set-wallet-name', + autoFocus: false, + maxLength: MAX_WALLET_NAME_LENGTH, + }, { label: 'password', key: 'password', type: 'password', hint: 'wizard.set-a-strong-password-to-protect-your-wallet', autoFocus: true, + maxLength: MAX_PASSWORD_LENGTH, + }, + { + label: 'confirm-password', + key: 'confirmPassword', + type: 'password', + autoFocus: false, + maxLength: MAX_PASSWORD_LENGTH, }, - { label: 'confirm-password', key: 'confirmPassword', type: 'password', autoFocus: false }, ] const Welcome = ({ rootPath = '/wizard', wallets = [], history }: WizardElementProps<{ rootPath: string }>) => { @@ -271,6 +285,7 @@ const Submission = ({ value={state[input.key]} onChange={onChange(input.key)} description={t(input.hint || '')} + maxLength={input.maxLength} />
))} diff --git a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts index 8f9cfa13bb..b44a7f8a6b 100644 --- a/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts +++ b/packages/neuron-ui/src/services/remote/controllerMethodWrapper.ts @@ -5,11 +5,12 @@ interface SuccessFromController { } interface FailureFromController { status: 0 - message: { - code?: number - content?: string - meta?: { [key: string]: string } - } + message: + | string + | { + content?: string + meta?: { [key: string]: string } + } } export type ControllerResponse = SuccessFromController | FailureFromController @@ -53,12 +54,14 @@ export const controllerMethodWrapper = (controllerName: string) => ( console.groupEnd() /* eslint-enable no-console */ } + if (!res) { return { status: 1, result: null, } } + if (res.status) { return { status: 1, @@ -68,7 +71,7 @@ export const controllerMethodWrapper = (controllerName: string) => ( return { status: 0, - message: res.message || {}, + message: typeof res.message === 'string' ? { content: res.message } : res.message || '', } } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts index b571e2a3e0..6279aa3103 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/settings.ts @@ -1,6 +1,7 @@ import { createNetwork as createRemoteNetwork, updateNetwork as updateRemoteNetwork } from 'services/remote' import { addressBook } from 'utils/localCache' import { Routes } from 'utils/const' +import { failureResToNotification } from 'utils/formatters' import { addNotification, addPopup } from './app' import { AppActions, StateDispatch } from '../reducer' @@ -15,26 +16,22 @@ export const toggleAddressBook = () => { export const createNetwork = (params: Controller.CreateNetworkParams) => (dispatch: StateDispatch, history: any) => { createRemoteNetwork(params).then(res => { - if (res.status) { + if (res.status === 1) { addPopup('create-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', timestamp: +new Date(), code: res.message.code, content: res.message.content })( - dispatch - ) + addNotification(failureResToNotification(res))(dispatch) } }) } export const updateNetwork = (params: Controller.UpdateNetworkParams) => (dispatch: StateDispatch, history: any) => { updateRemoteNetwork(params).then(res => { - if (res.status) { + if (res.status === 1) { addPopup('update-network-successfully')(dispatch) history.push(Routes.SettingsNetworks) } else { - addNotification({ type: 'alert', timestamp: +new Date(), code: res.message.code, content: res.message.content })( - dispatch - ) + addNotification(failureResToNotification(res))(dispatch) } }) } diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts index 0cdf49e7c3..491e0c12e4 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/transactions.ts @@ -4,6 +4,7 @@ import { getTransactionList, updateTransactionDescription as updateRemoteTransactionDescription, } from 'services/remote' +import { failureResToNotification } from 'utils/formatters' import { addNotification } from './app' export const updateTransactionList = (params: GetTransactionListParams) => (dispatch: StateDispatch) => { @@ -14,13 +15,7 @@ export const updateTransactionList = (params: GetTransactionListParams) => (disp payload: res.result, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -45,13 +40,7 @@ export const updateTransactionDescription = (params: Controller.UpdateTransactio }, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) .finally(() => { diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts index bb8ffa6fda..23febb7f63 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts @@ -19,13 +19,13 @@ import { WalletWizardPath } from 'components/WalletWizard' import i18n from 'utils/i18n' import { wallets as walletsCache, currentWallet as currentWalletCache } from 'utils/localCache' import { Routes } from 'utils/const' -import { addressesToBalance } from 'utils/formatters' +import { addressesToBalance, failureResToNotification } from 'utils/formatters' import { NeuronWalletActions } from '../reducer' import { addNotification, addPopup } from './app' export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) => { getCurrentWallet().then(res => { - if (res.status) { + if (res.status === 1) { const payload = res.result || initStates.wallet if (!payload || !payload.id) { history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) @@ -36,13 +36,7 @@ export const updateCurrentWallet = () => (dispatch: StateDispatch, history: any) }) currentWalletCache.save(payload) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -52,13 +46,14 @@ export const createWalletWithMnemonic = (params: Controller.ImportMnemonicParams history: any ) => { createWallet(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { - if (res.message.code) { - showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) - } else if (res.message.content) { - showErrorMessage(i18n.t(`messages.error`), res.message.content) + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) } } }) @@ -69,13 +64,14 @@ export const importWalletWithMnemonic = (params: Controller.ImportMnemonicParams history: any ) => { importMnemonic(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { - if (res.message.code) { - showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) - } else if (res.message.content) { - showErrorMessage(i18n.t(`messages.error`), res.message.content) + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) } } }) @@ -86,13 +82,14 @@ export const importWalletWithKeystore = (params: Controller.ImportKeystoreParams history: any ) => { importKeystore(params).then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.Overview) + } else if (res.status > 0) { + showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.status}`)) } else if (res.message) { - if (res.message.code) { - showErrorMessage(i18n.t(`messages.error`), i18n.t(`messages.codes.${res.message.code}`)) - } else if (res.message.content) { - showErrorMessage(i18n.t(`messages.error`), res.message.content) + const msg = typeof res.message === 'string' ? res.message : res.message.content || '' + if (msg) { + showErrorMessage(i18n.t(`messages.error`), msg) } } }) @@ -100,7 +97,7 @@ export const importWalletWithKeystore = (params: Controller.ImportKeystoreParams export const updateWalletList = () => (dispatch: StateDispatch, history: any) => { getWalletList().then(res => { - if (res.status) { + if (res.status === 1) { const payload = res.result || [] if (!payload.length) { history.push(`${Routes.WalletWizard}${WalletWizardPath.Welcome}`) @@ -111,13 +108,7 @@ export const updateWalletList = () => (dispatch: StateDispatch, history: any) => }) walletsCache.save(payload) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -133,13 +124,7 @@ export const updateWalletProperty = (params: Controller.UpdateWalletParams) => ( history.push(Routes.SettingsWallets) } } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -151,13 +136,7 @@ export const setCurrentWallet = (id: string) => (dispatch: StateDispatch) => { payload: null, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -171,16 +150,19 @@ export const sendTransaction = (params: Controller.SendTransaction) => (dispatch }) sendCapacity(params) .then(res => { - if (res.status) { + if (res.status === 1) { history.push(Routes.History) } else { // TODO: the pretreatment is unnecessary once the error code is implemented addNotification({ type: 'alert', timestamp: +new Date(), - code: res.message.code, - content: (res.message.content || '').replace(/(\b"|"\b)/g, ''), - meta: res.message.meta, + 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({ @@ -188,6 +170,9 @@ export const sendTransaction = (params: Controller.SendTransaction) => (dispatch payload: null, }) }) + .catch(err => { + console.warn(err) + }) .finally(() => { dispatch({ type: AppActions.UpdateLoadings, @@ -210,13 +195,7 @@ export const updateAddressListAndBalance = (params: Controller.GetAddressesByWal payload: { addresses, balance }, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -241,13 +220,7 @@ export const updateAddressDescription = (params: Controller.UpdateAddressDescrip }, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) .finally(() => { @@ -269,13 +242,7 @@ export const deleteWallet = (params: Controller.DeleteWalletParams) => (dispatch if (res.status) { addPopup('delete-wallet-successfully')(dispatch) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } @@ -292,13 +259,7 @@ export const backupWallet = (params: Controller.BackupWalletParams) => (dispatch payload: null, }) } else { - addNotification({ - type: 'alert', - timestamp: +new Date(), - code: res.message.code, - content: res.message.content, - meta: res.message.meta, - })(dispatch) + addNotification(failureResToNotification(res))(dispatch) } }) } diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 0efad87278..4939e49f91 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -1,4 +1,5 @@ export const MAX_NETWORK_NAME_LENGTH = 28 +export const MAX_WALLET_NAME_LENGTH = 20 export const ADDRESS_LENGTH = 46 export const MIN_PASSWORD_LENGTH = 8 export const MAX_PASSWORD_LENGTH = 50 diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index e178b94cd5..bc4dfaadcc 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -173,6 +173,16 @@ export const outputsToTotalCapacity = (outputs: { amount: string; unit: Capacity return totalCapacity.toString() } +export const failureResToNotification = (res: any): State.Message => { + return { + type: 'alert', + timestamp: +new Date(), + code: res.status, + content: typeof res.message !== 'string' ? res.message.content : res.message, + meta: typeof res.message !== 'string' ? res.message.meta : undefined, + } +} + export default { queryFormatter, currencyFormatter, @@ -183,4 +193,5 @@ export default { priceToFee, addressesToBalance, outputsToTotalCapacity, + failureResToNotification, } diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index 0f9667d913..b209d8b593 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -1,23 +1,40 @@ +/* global BigInt */ import { ckbCore } from 'services/chain' -import { ADDRESS_LENGTH, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, MIN_AMOUNT } from './const' +import { outputsToTotalCapacity } from 'utils/formatters' +import { MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, MIN_AMOUNT, MAX_DECIMAL_DIGITS, ErrorCode } from './const' -export const verifyAddress = (address: string): boolean | string => { - // TODO: verify address, prd required +export const verifyAddress = (address: string): boolean => { try { - if (address.length !== ADDRESS_LENGTH) { - throw new Error('Address length is incorrect') - } ckbCore.utils.parseAddress(address) return true } catch (err) { - return err.message + return false } } -export const verifyAmountRange = (amount: string) => { +export const verifyAmountRange = (amount: string = '') => { return +amount >= MIN_AMOUNT } +export const verifyAmount = (amount: string = '0') => { + if (Number.isNaN(+amount) || +amount < 0) { + return { code: ErrorCode.NotNegative, meta: { fieldName: 'amount', fieldValue: amount } } + } + const [, decimal = ''] = amount.split('.') + if (decimal.length > MAX_DECIMAL_DIGITS) { + return { + code: ErrorCode.DecimalExceed, + meta: { fieldName: 'amount', fieldValue: amount }, + } + } + return true +} + +export const verifyTotalAmount = (items: any, fee: string, balance: string) => { + const totalAmount = outputsToTotalCapacity(items) + return BigInt(totalAmount) + BigInt(fee) <= BigInt(balance) +} + export const verifyPasswordComplexity = (password: string) => { if (!password) { return 'password-is-empty' @@ -54,5 +71,6 @@ export const verifyPasswordComplexity = (password: string) => { export default { verifyAddress, verifyAmountRange, + verifyTotalAmount, verifyPasswordComplexity, } diff --git a/packages/neuron-wallet/src/controllers/wallets/index.ts b/packages/neuron-wallet/src/controllers/wallets/index.ts index 6fc3c95328..0f98884eb9 100644 --- a/packages/neuron-wallet/src/controllers/wallets/index.ts +++ b/packages/neuron-wallet/src/controllers/wallets/index.ts @@ -371,7 +371,7 @@ export default class WalletsController { } catch (err) { return { status: ResponseCode.Fail, - msg: `Error: "${err.message}"`, + message: `Error: "${err.message}"`, } } } @@ -391,7 +391,7 @@ export default class WalletsController { } catch (err) { return { status: ResponseCode.Fail, - msg: `Error: "${err.message}"`, + message: `Error: "${err.message}"`, } } } diff --git a/packages/neuron-wallet/src/decorators/errors.ts b/packages/neuron-wallet/src/decorators/errors.ts index fd41954793..efaa1b3ffe 100644 --- a/packages/neuron-wallet/src/decorators/errors.ts +++ b/packages/neuron-wallet/src/decorators/errors.ts @@ -10,7 +10,7 @@ export const CatchControllerError = (_target: any, _name: string, descriptor: Pr } catch (err) { return { status: ResponseCode.Fail, - message: { content: err.message }, + message: typeof err.message === 'string' ? { content: err.message } : err.message, } } }, diff --git a/packages/neuron-wallet/src/types/controller/index.d.ts b/packages/neuron-wallet/src/types/controller/index.d.ts index c6d318a9c0..43b39ce24a 100644 --- a/packages/neuron-wallet/src/types/controller/index.d.ts +++ b/packages/neuron-wallet/src/types/controller/index.d.ts @@ -1,7 +1,12 @@ declare module Controller { interface Response { status: number - msg?: string + message?: + | string + | { + content?: string + meta?: { [key: string]: string } + } result?: T } From 617a762fae2ac18fbb56f35f065d1dafb75792fd Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 16:45:43 +0800 Subject: [PATCH 04/12] feat(neuron-ui): add more Chinese for the i18n --- packages/neuron-ui/src/locales/zh.json | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 10b5931495..01202ff38a 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -280,10 +280,38 @@ "invalid-amount": "金额 {{amount} 是无效的数字", "amount-too-small": "金额 {{amount}} 太小, 请输入一个不小于 61 CKB 的值", "fields": { - "name": "Name" + "wallet": "钱包", + "name": "名称", + "remote": "RPC URL", + "network": "网络", + "address": "地址", + "amount": "金额", + "transaction": "交易", + "default-address": "默认地址", + "mnemonic": "助记词", + "keystore-path": "Keystore 文件", + "keystore-name": "钱包名称", + "keystore-password": "密码" }, "codes": { - "201": "缺少{{fieldName}} $t('messages.fields.name')" + "-3": "", + "100": "余额不足", + "101": "金额 {{amount}} CKB 太小, 请输入一个不小于 61 CKB 的值", + "102": "$t(messages.fields.{{fieldName}})无效", + "201": "缺少$t(messages.fields.{{fieldName}})", + "202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 已被使用", + "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太长, 其长度应不超过 {{length}}", + "204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太短, 其长度应不小于 {{length}}", + "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效", + "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 请输入小数位不超过 {{length}} 的$t(messages.fields.{{fieldName}})", + "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值不能为负数", + "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值应以 http(s):// 开始", + "209": "$t(messages.fields.{{fieldName}})不能包含空格", + "301": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 不可删除", + "302": "无法启动应用", + "303": "未找到$t(messages.fields.{{fieldName}})", + "304": "摄像头不可用或被禁用", + "305": "$t(messages.fields.address)不能为空" } }, "sync": { From 2a25e1944914ae4ec8f8ede409f4d5e1d3412bd9 Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 17:07:45 +0800 Subject: [PATCH 05/12] refactor(neuron-ui): remove useless texts for the i18n --- .../src/components/Transaction/index.tsx | 6 ++-- packages/neuron-ui/src/locales/en.json | 31 ----------------- packages/neuron-ui/src/locales/zh.json | 33 +------------------ 3 files changed, 5 insertions(+), 65 deletions(-) diff --git a/packages/neuron-ui/src/components/Transaction/index.tsx b/packages/neuron-ui/src/components/Transaction/index.tsx index 1cd2edc30e..ecd72826d1 100644 --- a/packages/neuron-ui/src/components/Transaction/index.tsx +++ b/packages/neuron-ui/src/components/Transaction/index.tsx @@ -104,8 +104,10 @@ const Transaction = () => { if (res.status) { setTransaction(res.result) } else { - // TODO: use error code - showErrorMessage(t(`messages.error`), t(`messages.transaction-not-found`)) + showErrorMessage( + t(`messages.error`), + t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' }) + ) window.close() } }) diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 5c245793d9..3eb6c2a076 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -240,45 +240,14 @@ "success": "success" }, "messages": { - "at-least-one-address-needed": "At least one address needed", - "name-required": "Name is required", - "url-required": "URL is required", - "protocol-required": "Protocol is required", - "length-of-name-should-be-less-than-or-equal-to": "Length of name should be less than or equal to {{length}}", - "network-name-used": "Network name is used", - "is-unremovable": "{{target}} is unremovable", - "create-wallet-success": "You have created wallet '{{name}}' successfully", - "network-is-not-found": "Network is not found", - "failed-to-initiate,-please-reopen-Neuron": "Failed to initiate, please reopen Neuron", - "no-wallet": "No Wallet", - "wallet-imported-successfully": "{{name}} imported successfully", - "wallet-created-successfully": "{{name}} created successfully", - "wallet-updated-successfully": "{{name}} updated successfully", - "wallet-not-found": "Wallet not found", - "no-transactions": "No transactions", "error": "Error", - "invalid-mnemonic": "Invalid mnemonic words", - "camera-not-available-or-disabled": "Camera is unavailable or disabled", - "can-not-find-the-default-address": "Cannot find the default address", - "create-wallet-successfully": "Create a wallet successfully", - "import-wallet-successfully": "Import a wallet successfully", "update-wallet-successfully": "Update the wallet successfully", "delete-wallet-successfully": "Delete the wallet successfully", "create-network-successfully": "Create a network successfully", "update-network-successfully": "Update the network successfully", - "delete-network-successfully": "Delete the network successfully", "addr-copied": "Address has been copied to the clipboard", "qrcode-copied": "QR Code has been copied to the clipboard", "lock-arg-copied": "Lock Arg has been copied to the clipboard", - "transaction-not-found": "The transaction is not found", - "rpc-url-should-have-protocol": "The RPC URL should start with http(s)://", - "rpc-url-should-have-no-whitespaces": "The RPC URL should have no whitespaces", - "is-required": "{{field}} is required", - "is-used": "{{field}} is used", - "invalid-address": "{{address}} is an invalid address", - "amount-decimal-exceed": "The amount {{amount}} CKB is invalid, please enter an amount with no more than 8 decimal places", - "invalid-amount": "The amount {{amount}} CKB is invalid", - "amount-too-small": "The amount {{amount}} CKB is too small, please enter an amount no less than 61 CKB", "fields": { "wallet": "Wallet", "name": "Name", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 01202ff38a..6ff67e4ae3 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -240,45 +240,14 @@ "success": "成功" }, "messages": { - "at-least-one-address-needed": "需要至少一个地址", - "name-required": "缺少名称", - "url-required": "缺少 URL", - "protocol-required": "请指定 URL 协议", - "length-of-name-should-be-less-than-or-equal-to": "名称长度应不大于 {{length}}", - "network-name-used": "节点名称已存在", - "is-unremovable": "{{target}}不可删除", - "create-wallet-success": "您已成功创建钱包 '{{name}}'", - "network-is-not-found": "未找到节点信息", - "failed-to-initiate,-please-reopen-Neuron": "初始化失败, 请新打开 Neuron", - "no-wallet": "没有钱包", - "wallet-imported-successfully": "{{name}} 导入成功", - "wallet-created-successfully": "{{name}} 创建成功", - "wallet-updated-successfully": "{{name}} 更新成功", - "wallet-not-found": "未找到钱包", - "no-transactions": "没有交易", "error": "错误", - "invalid-mnemonic": "助记词不合法", - "camera-not-available-or-disabled": "摄像头不可用或被禁用", - "can-not-find-the-default-address": "未获得默认地址", - "create-wallet-successfully": "新建钱包成功", - "import-wallet-successfully": "导入钱包成功", "update-wallet-successfully": "已更新钱包信息", "delete-wallet-successfully": "已删除钱包", "create-network-successfully": "新节点已添加", "update-network-successfully": "已更新节点信息", - "delete-network-successfully": "节点已删除", "addr-copied": "地址已复制到剪贴板", "qrcode-copied": "二维码已复制到剪贴板", "lock-arg-copied": "Lock Arg 已复制到剪贴板", - "transaction-not-found": "未找到交易", - "network-address-should-have-protocol": "RPC 地址应以 http(s)//: 开始", - "network-address-should-have-no-whitespaces": "RPC 地址不能包含空格", - "is-required": "{{field}}是必须的", - "is-used": "{{field}}已使用", - "invalid-address": "{{address}} 是无效的地址", - "amount-decimal-exceed": "金额 {{amount}} 是个无效的值, 其小数位应不超过 8 位", - "invalid-amount": "金额 {{amount} 是无效的数字", - "amount-too-small": "金额 {{amount}} 太小, 请输入一个不小于 61 CKB 的值", "fields": { "wallet": "钱包", "name": "名称", @@ -303,7 +272,7 @@ "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太长, 其长度应不超过 {{length}}", "204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太短, 其长度应不小于 {{length}}", "205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效", - "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 请输入小数位不超过 {{length}} 的$t(messages.fields.{{fieldName}})", + "206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其小数位应不超过 {{length}} 位", "207": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值不能为负数", "208": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其值应以 http(s):// 开始", "209": "$t(messages.fields.{{fieldName}})不能包含空格", From 9d4cc3c3a7e2291749e5763891435eda7435e912 Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 17:55:08 +0800 Subject: [PATCH 06/12] refactor(neuron-ui): simplify verify transaction outputs --- .../neuron-ui/src/components/Send/hooks.ts | 27 +++----- .../neuron-ui/src/components/Send/index.tsx | 2 +- .../tests/{ => formatters}/formatters.test.ts | 0 .../verifyTransactionOutputs/fixture.ts | 63 +++++++++++++++++++ .../verifyTransactionOutputs/index.test.ts | 9 +++ packages/neuron-ui/src/utils/validators.ts | 13 ++++ 6 files changed, 93 insertions(+), 21 deletions(-) rename packages/neuron-ui/src/tests/{ => formatters}/formatters.test.ts (100%) create mode 100644 packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 4dfe9d2437..17b8ec2ec8 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -4,24 +4,12 @@ import { AppActions, StateDispatch } from 'states/stateProvider/reducer' import { calculateCycles } from 'services/remote/wallets' import { outputsToTotalCapacity, priceToFee } from 'utils/formatters' -import { verifyAddress, verifyAmount, verifyAmountRange } from 'utils/validators' +import { verifyAddress, verifyAmount, verifyAmountRange, verifyTransactionOutputs } from 'utils/validators' import { ErrorCode } from 'utils/const' import { TransactionOutput } from '.' let cyclesTimer: ReturnType -const verifyTransactionParams = (items: TransactionOutput[] = []) => { - return !items.some(item => { - if (item.address === '' || verifyAddress(item.address) !== true) { - return true - } - if (Number.isNaN(+item.amount) || verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { - return true - } - return false - }) -} - const useUpdateTransactionOutput = (dispatch: StateDispatch) => useCallback( (field: string) => (idx: number) => (value: string) => { @@ -66,7 +54,7 @@ const useOnTransactionChange = ( useEffect(() => { clearTimeout(cyclesTimer) cyclesTimer = setTimeout(() => { - if (verifyTransactionParams(items)) { + if (verifyTransactionOutputs(items)) { setIsTransactionValid(true) calculateCycles({ walletID, @@ -99,13 +87,13 @@ const useOnTransactionChange = ( }) } }, 300) - }, [walletID, items, dispatch]) + }, [walletID, items, dispatch, setIsTransactionValid]) } -const useOnSubmit = (items: TransactionOutput[], balance: string, fee: string, dispatch: StateDispatch) => +const useOnSubmit = (items: TransactionOutput[], dispatch: StateDispatch) => useCallback( (walletID: string = '') => () => { - if (verifyTransactionParams(items)) { + if (verifyTransactionOutputs(items)) { dispatch({ type: AppActions.UpdateTransactionID, payload: null, @@ -119,7 +107,7 @@ const useOnSubmit = (items: TransactionOutput[], balance: string, fee: string, d }) } }, - [dispatch, items, balance, fee] + [dispatch, items] ) const useOnItemChange = (updateTransactionOutput: Function) => @@ -182,7 +170,6 @@ export const useInitialize = ( items: TransactionOutput[], price: string, cycles: string, - balance: string, dispatch: React.Dispatch, t: any ) => { @@ -195,7 +182,7 @@ export const useInitialize = ( const removeTransactionOutput = useRemoveTransactionOutput(dispatch) const updateTransactionPrice = useUpdateTransactionPrice(dispatch) const onDescriptionChange = useSendDescriptionChange(dispatch) - const onSubmit = useOnSubmit(items, balance, fee, dispatch) + const onSubmit = useOnSubmit(items, dispatch) const onClear = useClear(dispatch) const onGetAddressErrorMessage = useCallback( diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index de66491834..0beca4d7b2 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -54,7 +54,7 @@ const Send = ({ onGetAddressErrorMessage, onGetAmountErrorMessage, onClear, - } = useInitialize(send.outputs, send.price, send.cycles, balance, dispatch, t) + } = useInitialize(send.outputs, send.price, send.cycles, dispatch, t) useOnTransactionChange(walletID, send.outputs, dispatch, setIsTransactionValid) const leftStackWidth = '70%' const labelWidth = '140px' diff --git a/packages/neuron-ui/src/tests/formatters.test.ts b/packages/neuron-ui/src/tests/formatters/formatters.test.ts similarity index 100% rename from packages/neuron-ui/src/tests/formatters.test.ts rename to packages/neuron-ui/src/tests/formatters/formatters.test.ts diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts new file mode 100644 index 0000000000..fcdbd14dda --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts @@ -0,0 +1,63 @@ +const fixtures: { + [title: string]: { + outputs: { address: string; amount: string }[] + expected: boolean + } +} = { + 'valid address and valid amount': { + outputs: [ + { + address: 'ckt1qyqg5w7emdntvnnk7utzqkz3kx276um0j4qs525t0y', + amount: '100', + }, + ], + expected: true, + }, + 'empty address and valid amount': { + outputs: [ + { + address: '', + amount: '100', + }, + ], + expected: false, + }, + 'invalid address and valid amount': { + outputs: [ + { + address: 'abcdefg', + amount: '100', + }, + ], + expected: false, + }, + 'valid address and amount of invalid number': { + outputs: [ + { + address: 'abcdefg', + amount: 'invalid number', + }, + ], + expected: false, + }, + 'valid address and negative amount': { + outputs: [ + { + address: 'abcdefg', + amount: '-1', + }, + ], + expected: false, + }, + 'valid address and amount less than 61': { + outputs: [ + { + address: 'abcdefg', + amount: '60', + }, + ], + expected: false, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts new file mode 100644 index 0000000000..4176725b0c --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts @@ -0,0 +1,9 @@ +import { verifyTransactionOutputs } from '../../../utils/validators' +import fixtures from './fixture' + +describe('test verify transaction outputs', () => { + const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) + test.each(fixtureTable)('%s, outputs: %j, expected: %s', (title, outputs, expected) => { + expect(verifyTransactionOutputs(outputs)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index b209d8b593..03b44dca49 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -68,9 +68,22 @@ export const verifyPasswordComplexity = (password: string) => { return true } +export const verifyTransactionOutputs = (items: { address: string; amount: string }[] = []) => { + return !items.some(item => { + if (item.address === '' || verifyAddress(item.address) !== true) { + return true + } + if (Number.isNaN(+item.amount) || verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { + return true + } + return false + }) +} + export default { verifyAddress, verifyAmountRange, verifyTotalAmount, verifyPasswordComplexity, + verifyTransactionOutputs, } From 1698e6ad907321f82cd4441d80663d4ad3480933 Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 18:11:24 +0800 Subject: [PATCH 07/12] test(neuron-ui): add tests of verify amount range --- .../validators/verifyAmountRange/fixtures.ts | 22 +++++++++++++++++++ .../verifyAmountRange/index.test.ts | 10 +++++++++ .../verifyTransactionOutputs/index.test.ts | 14 +++++++----- 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts new file mode 100644 index 0000000000..626d7def9e --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts @@ -0,0 +1,22 @@ +export default { + 'amount of 0': { + amount: '0', + expected: false, + }, + 'amount of 60.99999999': { + amount: '60.99999999', + expected: false, + }, + 'amount equals to 61': { + amount: '61', + expected: true, + }, + 'amount close to 61.00000001': { + amount: '61.00000001', + expected: true, + }, + 'amount far away from 61': { + amount: '6100000001', + expected: true, + }, +} diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts new file mode 100644 index 0000000000..fb07a1a619 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts @@ -0,0 +1,10 @@ +import { verifyAmountRange } from '../../../utils/validators' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) + +describe('verify amount range', () => { + test.each(fixtureTable)(`%s, amount: %s, expected: %s`, (_title: string, amount: string, expected: boolean) => { + expect(verifyAmountRange(amount)).toBe(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts index 4176725b0c..be3b414170 100644 --- a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts @@ -1,9 +1,13 @@ import { verifyTransactionOutputs } from '../../../utils/validators' import fixtures from './fixture' -describe('test verify transaction outputs', () => { - const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) - test.each(fixtureTable)('%s, outputs: %j, expected: %s', (title, outputs, expected) => { - expect(verifyTransactionOutputs(outputs)).toBe(expected) - }) +const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) + +describe(`test verify transaction outputs`, () => { + test.each(fixtureTable)( + `%s, outputs: %j, expected: %s`, + (_title: string, outputs: { address: string; amount: string }[], expected: boolean) => { + expect(verifyTransactionOutputs(outputs)).toBe(expected) + } + ) }) From d5d2c6808a15d782d06ecdffbe0806a8fa24992c Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 18:28:08 +0800 Subject: [PATCH 08/12] test(neuron-ui): add tests of verify amount --- .../neuron-ui/src/components/Send/hooks.ts | 5 ++- .../tests/validators/verifyAmount/fixtures.ts | 37 +++++++++++++++++++ .../validators/verifyAmount/index.test.ts | 10 +++++ .../validators/verifyAmountRange/fixtures.ts | 10 ++--- .../verifyAmountRange/index.test.ts | 4 +- .../verifyTransactionOutputs/fixture.ts | 12 +++--- .../verifyTransactionOutputs/index.test.ts | 4 +- packages/neuron-ui/src/utils/validators.ts | 10 +++-- 8 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 17b8ec2ec8..2d36ff9cf0 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -207,7 +207,10 @@ export const useInitialize = ( const msg = verifyAmount(amount) if (typeof msg === 'object') { - return t(`messages.codes.${msg.code}`, msg.meta) + return t(`messages.codes.${msg.code}`, { + fieldName: 'amount', + fieldValue: amount, + }) } if (!verifyAmountRange(amount)) { return t(`messages.codes.${ErrorCode.AmountTooSmall}`, { diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts new file mode 100644 index 0000000000..80e76662cf --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts @@ -0,0 +1,37 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + amount: string + expected: + | boolean + | { + code: ErrorCode + } + } +} = { + 'Valid Amount of 100000000000000000000000000000000000000000000000.00000001': { + amount: '100000000000000000000000000000000000000000000000.00000001', + expected: true, + }, + 'Amount should be number': { + amount: 'not a number', + expected: { + code: ErrorCode.FieldInvalid, + }, + }, + 'Amount should not be negative': { + amount: '-1', + expected: { + code: ErrorCode.NotNegative, + }, + }, + 'Amount should have no more than 8 decimal places': { + amount: '0.000000001', + expected: { + code: ErrorCode.DecimalExceed, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts new file mode 100644 index 0000000000..93c962ef5b --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts @@ -0,0 +1,10 @@ +import { verifyAmount } from 'utils/validators' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) + +describe(`Verify amount`, () => { + test.each(fixtureTable)(`%s, amount: %s, expected: %s`, (_title: string, amount: string, expected: boolean) => { + expect(verifyAmount(amount)).toEqual(expected) + }) +}) diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts index 626d7def9e..240dd21e44 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts @@ -1,21 +1,21 @@ export default { - 'amount of 0': { + 'Amount of 0': { amount: '0', expected: false, }, - 'amount of 60.99999999': { + 'Amount of 60.99999999': { amount: '60.99999999', expected: false, }, - 'amount equals to 61': { + 'Amount equals to 61': { amount: '61', expected: true, }, - 'amount close to 61.00000001': { + 'Amount close to 61.00000001': { amount: '61.00000001', expected: true, }, - 'amount far away from 61': { + 'Amount far away from 61': { amount: '6100000001', expected: true, }, diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts index fb07a1a619..46f30e5c85 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts @@ -1,9 +1,9 @@ -import { verifyAmountRange } from '../../../utils/validators' +import { verifyAmountRange } from 'utils/validators' import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) -describe('verify amount range', () => { +describe('Verify amount range', () => { test.each(fixtureTable)(`%s, amount: %s, expected: %s`, (_title: string, amount: string, expected: boolean) => { expect(verifyAmountRange(amount)).toBe(expected) }) diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts index fcdbd14dda..d7dce7db2f 100644 --- a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts @@ -4,7 +4,7 @@ const fixtures: { expected: boolean } } = { - 'valid address and valid amount': { + 'Valid address and valid amount': { outputs: [ { address: 'ckt1qyqg5w7emdntvnnk7utzqkz3kx276um0j4qs525t0y', @@ -13,7 +13,7 @@ const fixtures: { ], expected: true, }, - 'empty address and valid amount': { + 'Empty address and valid amount': { outputs: [ { address: '', @@ -22,7 +22,7 @@ const fixtures: { ], expected: false, }, - 'invalid address and valid amount': { + 'Invalid address and valid amount': { outputs: [ { address: 'abcdefg', @@ -31,7 +31,7 @@ const fixtures: { ], expected: false, }, - 'valid address and amount of invalid number': { + 'Valid address and amount of invalid number': { outputs: [ { address: 'abcdefg', @@ -40,7 +40,7 @@ const fixtures: { ], expected: false, }, - 'valid address and negative amount': { + 'Valid address and negative amount': { outputs: [ { address: 'abcdefg', @@ -49,7 +49,7 @@ const fixtures: { ], expected: false, }, - 'valid address and amount less than 61': { + 'Valid address and amount less than 61': { outputs: [ { address: 'abcdefg', diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts index be3b414170..e5a60f594e 100644 --- a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts @@ -1,9 +1,9 @@ -import { verifyTransactionOutputs } from '../../../utils/validators' +import { verifyTransactionOutputs } from 'utils/validators' import fixtures from './fixture' const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) -describe(`test verify transaction outputs`, () => { +describe(`Verify transaction outputs`, () => { test.each(fixtureTable)( `%s, outputs: %j, expected: %s`, (_title: string, outputs: { address: string; amount: string }[], expected: boolean) => { diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index 03b44dca49..ccdc8169b1 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -17,14 +17,16 @@ export const verifyAmountRange = (amount: string = '') => { } export const verifyAmount = (amount: string = '0') => { - if (Number.isNaN(+amount) || +amount < 0) { - return { code: ErrorCode.NotNegative, meta: { fieldName: 'amount', fieldValue: amount } } + if (Number.isNaN(+amount)) { + return { code: ErrorCode.FieldInvalid } + } + if (+amount < 0) { + return { code: ErrorCode.NotNegative } } const [, decimal = ''] = amount.split('.') if (decimal.length > MAX_DECIMAL_DIGITS) { return { code: ErrorCode.DecimalExceed, - meta: { fieldName: 'amount', fieldValue: amount }, } } return true @@ -73,7 +75,7 @@ export const verifyTransactionOutputs = (items: { address: string; amount: strin if (item.address === '' || verifyAddress(item.address) !== true) { return true } - if (Number.isNaN(+item.amount) || verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { + if (verifyAmount(item.amount) !== true || verifyAmountRange(item.amount) !== true) { return true } return false From 09cc871b92345f8f728b1f1fc85defcc8bd2b54f Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 20:31:04 +0800 Subject: [PATCH 09/12] test(neuron-ui): add tests of the verify network name method --- .../src/components/NetworkEditor/hooks.ts | 38 ++++++++-------- .../src/components/NetworkEditor/index.tsx | 18 ++------ .../validators/verifyAmount/index.test.ts | 10 +++-- .../validators/verifyNetworkName/fixtures.ts | 43 +++++++++++++++++++ .../verifyNetworkName/index.test.ts | 19 ++++++++ packages/neuron-ui/src/utils/validators.ts | 40 +++++++++++++++++ 6 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts diff --git a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts index d432d5e565..172926852c 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/hooks.ts +++ b/packages/neuron-ui/src/components/NetworkEditor/hooks.ts @@ -6,6 +6,7 @@ import { createNetwork, updateNetwork, addNotification } from 'states/stateProvi import { MAX_NETWORK_NAME_LENGTH, ErrorCode } from 'utils/const' import i18n from 'utils/i18n' +import { verifyNetworkName, verifyURL } from 'utils/validators' enum PlaceHolder { Name = 'My Custom Node', @@ -90,14 +91,9 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any tooltip: TooltipText.URL, placeholder: PlaceHolder.URL, onGetErrorMessage: (url: string) => { - if (!url) { - return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'remote' }) - } - if (!/^https?:\/\//.test(url)) { - return t(`messages.codes.${ErrorCode.ProtocolRequired}`, { fieldName: 'remote', fieldValue: url }) - } - if (/\s/.test(url)) { - return t(`messages.codes.${ErrorCode.NoWhiteSpaces}`, { fieldName: 'remote' }) + const res = verifyURL(url) + if (typeof res === 'object') { + return t(`messages.codes.${res.code}`, { fieldName: 'remote', fieldValue: url }) } return '' }, @@ -108,14 +104,9 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any tooltip: TooltipText.Name, placeholder: PlaceHolder.Name, onGetErrorMessage: (name: string) => { - if (!name) { - return t(`messages.codes.${ErrorCode.FieldRequired}`, { fieldName: 'name' }) - } - if (usedNetworkNames.includes(name)) { - return t(`messages.codes.${ErrorCode.FieldUsed}`, { fieldName: 'name', fieldValue: name }) - } - if (name.length > MAX_NETWORK_NAME_LENGTH) { - return t(`messages.codes.${ErrorCode.FieldTooLong}`, { + const res = verifyNetworkName(name, usedNetworkNames) + if (typeof res === 'object') { + return t(`messages.codes.${res.code}`, { fieldName: 'name', fieldValue: name, length: MAX_NETWORK_NAME_LENGTH, @@ -129,13 +120,21 @@ export const useInputs = (editor: EditorType, usedNetworkNames: string[], t: any ) } -export const useIsInputsValid = (editor: EditorType, cachedNetwork: State.Network | undefined) => { - const [errors, setErrors] = useState([!cachedNetwork && !editor.name.value, !cachedNetwork && !editor.remote.value]) +export const useIsInputsValid = ( + editor: EditorType, + usedNetworkNames: string[], + cachedNetwork: State.Network | undefined +) => { + const hasError = useMemo(() => { + const nameRes = verifyNetworkName(editor.name.value, usedNetworkNames) + const URLRes = verifyURL(editor.remote.value) + return !(nameRes === true && URLRes === true) + }, [editor.name.value, editor.remote.value, usedNetworkNames]) const notModified = useMemo( () => cachedNetwork && (cachedNetwork.name === editor.name.value && cachedNetwork.remote === editor.remote.value), [cachedNetwork, editor.name.value, editor.remote.value] ) - return { errors, setErrors, notModified } + return { hasError, notModified } } export const useHandleSubmit = ( @@ -214,6 +213,7 @@ export const useHandleSubmit = ( remote, })(dispatch, history) } + if (networks.some(network => network.name === name && network.id !== id)) { errorMessage = { type: 'warning', diff --git a/packages/neuron-ui/src/components/NetworkEditor/index.tsx b/packages/neuron-ui/src/components/NetworkEditor/index.tsx index e61ae39b3e..6b84cead1d 100644 --- a/packages/neuron-ui/src/components/NetworkEditor/index.tsx +++ b/packages/neuron-ui/src/components/NetworkEditor/index.tsx @@ -27,32 +27,22 @@ const NetworkEditor = ({ const goBack = useGoBack(history) useInitialize(id, networks, editor.initialize, dispatch) - const { errors, setErrors, notModified } = useIsInputsValid(editor, cachedNetwork) + const { hasError, notModified } = useIsInputsValid(editor, usedNetworkNames, cachedNetwork) const handleSubmit = useHandleSubmit(id, editor.name.value, editor.remote.value, networks, history, dispatch) return (

{t('settings.network.edit-network.title')}

- {inputs.map((inputProps, idx) => ( + {inputs.map(inputProps => ( - { - const errs = [...errors] - errs.splice(idx, 1, message !== '') - setErrors(errs) - }} - /> + ))} - +
) 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 93c962ef5b..f92700f714 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts @@ -1,10 +1,14 @@ import { verifyAmount } from 'utils/validators' +import { ErrorCode } from '../../../utils/const' import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) describe(`Verify amount`, () => { - test.each(fixtureTable)(`%s, amount: %s, expected: %s`, (_title: string, amount: string, expected: boolean) => { - expect(verifyAmount(amount)).toEqual(expected) - }) + test.each(fixtureTable)( + `%s, amount: %s, expected: %s`, + (_title: string, amount: string, expected: boolean | { code: ErrorCode }) => { + expect(verifyAmount(amount)).toEqual(expected) + } + ) }) diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts new file mode 100644 index 0000000000..6906bf0827 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts @@ -0,0 +1,43 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + name: string + usedNames: string[] + expected: boolean | { code: ErrorCode } + } +} = { + 'Valid name': { + name: 'Testnet', + usedNames: ['Local'], + expected: true, + }, + 'Name cannot be empty': { + name: '', + usedNames: ['Local'], + expected: { + code: ErrorCode.FieldRequired, + }, + }, + 'Name consists of 28 charcters': { + name: '1234567890123456789012345678', + usedNames: ['Local'], + expected: true, + }, + 'Name is too long': { + name: '12345678901234567890123456789', + usedNames: ['Local'], + expected: { + code: ErrorCode.FieldTooLong, + }, + }, + 'Name is used': { + name: 'Testnet', + usedNames: ['Testnet', 'Local'], + expected: { + code: ErrorCode.FieldUsed, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts new file mode 100644 index 0000000000..402253c0cf --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts @@ -0,0 +1,19 @@ +import { verifyNetworkName } from 'utils/validators' +import { ErrorCode } from 'utils/const' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { name, usedNames, expected }]) => [ + title, + name, + usedNames, + expected, +]) + +describe(`Verify network name`, () => { + test.each(fixtureTable)( + `%s, name: %s, used names: %j, expected: %s`, + (_title: string, name: string, usedNames: string[], expected: boolean | { code: ErrorCode }) => { + expect(verifyNetworkName(name, usedNames)).toEqual(expected) + } + ) +}) diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index ccdc8169b1..51b9632c80 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -1,3 +1,4 @@ +import { MAX_NETWORK_NAME_LENGTH } from 'utils/const' /* global BigInt */ import { ckbCore } from 'services/chain' import { outputsToTotalCapacity } from 'utils/formatters' @@ -82,10 +83,49 @@ export const verifyTransactionOutputs = (items: { address: string; amount: strin }) } +export const verifyNetworkName = (name: string, usedNames: string[]) => { + if (!name) { + return { + code: ErrorCode.FieldRequired, + } + } + if (usedNames.includes(name)) { + return { + code: ErrorCode.FieldUsed, + } + } + if (name.length > MAX_NETWORK_NAME_LENGTH) { + return { + code: ErrorCode.FieldTooLong, + } + } + return true +} + +export const verifyURL = (url: string) => { + if (!url) { + return { + code: ErrorCode.FieldRequired, + } + } + if (!/^https?:\/\//.test(url)) { + return { + code: ErrorCode.ProtocolRequired, + } + } + if (/\s/.test(url)) { + return { + code: ErrorCode.NoWhiteSpaces, + } + } + return true +} + export default { verifyAddress, verifyAmountRange, verifyTotalAmount, verifyPasswordComplexity, verifyTransactionOutputs, + verifyNetworkName, } From f03728e74d3c37140cdd06c3d11a5f1e67181159 Mon Sep 17 00:00:00 2001 From: Keith Date: Mon, 26 Aug 2019 20:46:35 +0800 Subject: [PATCH 10/12] test(neuron-ui): add tests of the verify url method --- .../tests/validators/verifyAmount/fixtures.ts | 6 +-- .../validators/verifyAmount/index.test.ts | 9 ++-- .../validators/verifyAmountRange/fixtures.ts | 10 ++-- .../verifyAmountRange/index.test.ts | 2 +- .../validators/verifyNetworkName/fixtures.ts | 6 +-- .../verifyNetworkName/index.test.ts | 2 +- .../verifyTransactionOutputs/fixture.ts | 10 ++-- .../verifyTransactionOutputs/index.test.ts | 9 ++-- .../tests/validators/verifyURL/fixtures.ts | 49 +++++++++++++++++++ .../tests/validators/verifyURL/index.test.ts | 11 +++++ 10 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts diff --git a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts index 80e76662cf..9dcd4ac08c 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/fixtures.ts @@ -14,19 +14,19 @@ const fixtures: { amount: '100000000000000000000000000000000000000000000000.00000001', expected: true, }, - 'Amount should be number': { + 'Amount which is not a number should fail': { amount: 'not a number', expected: { code: ErrorCode.FieldInvalid, }, }, - 'Amount should not be negative': { + 'Negative amount should fail': { amount: '-1', expected: { code: ErrorCode.NotNegative, }, }, - 'Amount should have no more than 8 decimal places': { + 'Amount has more than 8 decimal places should fail': { amount: '0.000000001', expected: { code: ErrorCode.DecimalExceed, 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 f92700f714..9efb3336d9 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmount/index.test.ts @@ -5,10 +5,7 @@ import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) describe(`Verify amount`, () => { - test.each(fixtureTable)( - `%s, amount: %s, expected: %s`, - (_title: string, amount: string, expected: boolean | { code: ErrorCode }) => { - expect(verifyAmount(amount)).toEqual(expected) - } - ) + test.each(fixtureTable)(`%s`, (_title: string, amount: string, expected: boolean | { code: ErrorCode }) => { + expect(verifyAmount(amount)).toEqual(expected) + }) }) diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts index 240dd21e44..b7d6801a72 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/fixtures.ts @@ -1,21 +1,21 @@ export default { - 'Amount of 0': { + 'Amount of 0 should fail': { amount: '0', expected: false, }, - 'Amount of 60.99999999': { + 'Amount of 60.99999999 should fail': { amount: '60.99999999', expected: false, }, - 'Amount equals to 61': { + 'Amount equals to 61 should pass': { amount: '61', expected: true, }, - 'Amount close to 61.00000001': { + 'Amount close to 61.00000001 should pass': { amount: '61.00000001', expected: true, }, - 'Amount far away from 61': { + 'Amount far away from 61 should pass': { amount: '6100000001', expected: true, }, diff --git a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts index 46f30e5c85..1138435193 100644 --- a/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyAmountRange/index.test.ts @@ -4,7 +4,7 @@ import fixtures from './fixtures' const fixtureTable = Object.entries(fixtures).map(([title, { amount, expected }]) => [title, amount, expected]) describe('Verify amount range', () => { - test.each(fixtureTable)(`%s, amount: %s, expected: %s`, (_title: string, amount: string, expected: boolean) => { + test.each(fixtureTable)(`%s`, (_title: string, amount: string, expected: boolean) => { expect(verifyAmountRange(amount)).toBe(expected) }) }) diff --git a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts index 6906bf0827..ea59d674e6 100644 --- a/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/fixtures.ts @@ -12,7 +12,7 @@ const fixtures: { usedNames: ['Local'], expected: true, }, - 'Name cannot be empty': { + 'Empty name should fail': { name: '', usedNames: ['Local'], expected: { @@ -24,14 +24,14 @@ const fixtures: { usedNames: ['Local'], expected: true, }, - 'Name is too long': { + 'Name consists of more than 28 characters should fail': { name: '12345678901234567890123456789', usedNames: ['Local'], expected: { code: ErrorCode.FieldTooLong, }, }, - 'Name is used': { + 'Name which is used should fail': { name: 'Testnet', usedNames: ['Testnet', 'Local'], expected: { 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 402253c0cf..43b350166a 100644 --- a/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyNetworkName/index.test.ts @@ -11,7 +11,7 @@ const fixtureTable = Object.entries(fixtures).map(([title, { name, usedNames, ex describe(`Verify network name`, () => { test.each(fixtureTable)( - `%s, name: %s, used names: %j, expected: %s`, + `%s`, (_title: string, name: string, usedNames: string[], expected: boolean | { code: ErrorCode }) => { expect(verifyNetworkName(name, usedNames)).toEqual(expected) } diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts index d7dce7db2f..91f12e4987 100644 --- a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/fixture.ts @@ -13,7 +13,7 @@ const fixtures: { ], expected: true, }, - 'Empty address and valid amount': { + 'Empty address and valid amount should fail': { outputs: [ { address: '', @@ -22,7 +22,7 @@ const fixtures: { ], expected: false, }, - 'Invalid address and valid amount': { + 'Invalid address and valid amount should fail': { outputs: [ { address: 'abcdefg', @@ -31,7 +31,7 @@ const fixtures: { ], expected: false, }, - 'Valid address and amount of invalid number': { + 'Valid address and amount of invalid number should fail': { outputs: [ { address: 'abcdefg', @@ -40,7 +40,7 @@ const fixtures: { ], expected: false, }, - 'Valid address and negative amount': { + 'Valid address and negative amount should fail': { outputs: [ { address: 'abcdefg', @@ -49,7 +49,7 @@ const fixtures: { ], expected: false, }, - 'Valid address and amount less than 61': { + 'Valid address and amount less than 61 should fail': { outputs: [ { address: 'abcdefg', diff --git a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts index e5a60f594e..b4f07c7b7f 100644 --- a/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts +++ b/packages/neuron-ui/src/tests/validators/verifyTransactionOutputs/index.test.ts @@ -4,10 +4,7 @@ import fixtures from './fixture' const fixtureTable = Object.entries(fixtures).map(([title, { outputs, expected }]) => [title, outputs, expected]) describe(`Verify transaction outputs`, () => { - test.each(fixtureTable)( - `%s, outputs: %j, expected: %s`, - (_title: string, outputs: { address: string; amount: string }[], expected: boolean) => { - expect(verifyTransactionOutputs(outputs)).toBe(expected) - } - ) + test.each(fixtureTable)(`%s`, (_title: string, outputs: { address: string; amount: string }[], expected: boolean) => { + expect(verifyTransactionOutputs(outputs)).toBe(expected) + }) }) diff --git a/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts new file mode 100644 index 0000000000..7a2b71fea9 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyURL/fixtures.ts @@ -0,0 +1,49 @@ +import { ErrorCode } from 'utils/const' + +const fixtures: { + [title: string]: { + url: string + expected: boolean | { code: ErrorCode } + } +} = { + 'URL starts with http://': { + url: 'http://localhost', + expected: true, + }, + 'URL starts with https://': { + url: 'https://localhost', + expected: true, + }, + 'URL starts with http should fail': { + url: 'http hello', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL starts with https should fail': { + url: 'https hello', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL start with ws:// should fail': { + url: 'ws://localhost', + expected: { + code: ErrorCode.ProtocolRequired, + }, + }, + 'URL contains whitespaces should fail': { + url: 'http:// localhost', + expected: { + code: ErrorCode.NoWhiteSpaces, + }, + }, + 'Empty URL should fail': { + url: '', + expected: { + code: ErrorCode.FieldRequired, + }, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts new file mode 100644 index 0000000000..04e325918c --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyURL/index.test.ts @@ -0,0 +1,11 @@ +import { verifyURL } from 'utils/validators' +import { ErrorCode } from 'utils/const' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { url, expected }]) => [title, url, expected]) + +describe('Verify URL', () => { + test.each(fixtureTable)(`%s`, (_title: string, url: string, expected: boolean | { code: ErrorCode }) => { + expect(verifyURL(url)).toEqual(expected) + }) +}) From d06d366f65ca01e4115409751beb642fdad10b9e Mon Sep 17 00:00:00 2001 From: Keith Date: Tue, 27 Aug 2019 11:41:28 +0800 Subject: [PATCH 11/12] feat(neuron-ui): add total amount verification on the send view --- .../neuron-ui/src/components/Send/hooks.ts | 10 +++- .../neuron-ui/src/components/Send/index.tsx | 55 +++++++++++++++---- packages/neuron-ui/src/locales/en.json | 1 + packages/neuron-ui/src/locales/zh.json | 1 + .../validators/verifyTotalAmount/fixtures.ts | 35 ++++++++++++ .../verifyTotalAmount/index.test.ts | 19 +++++++ packages/neuron-ui/src/utils/validators.ts | 7 ++- 7 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts create mode 100644 packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index 2d36ff9cf0..e752b88ec3 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -49,16 +49,19 @@ const useOnTransactionChange = ( walletID: string, items: TransactionOutput[], dispatch: StateDispatch, - setIsTransactionValid: Function + setIsTransactionValid: Function, + setTotalAmount: Function ) => { useEffect(() => { clearTimeout(cyclesTimer) cyclesTimer = setTimeout(() => { if (verifyTransactionOutputs(items)) { setIsTransactionValid(true) + const totalAmount = outputsToTotalCapacity(items) + setTotalAmount(totalAmount) calculateCycles({ walletID, - capacities: outputsToTotalCapacity(items), + capacities: totalAmount, }) .then(response => { if (response.status) { @@ -175,6 +178,7 @@ export const useInitialize = ( ) => { const fee = useMemo(() => priceToFee(price, cycles), [price, cycles]) // in shannon const [isTransactionValid, setIsTransactionValid] = useState(false) + const [totalAmount, setTotalAmount] = useState('0') const updateTransactionOutput = useUpdateTransactionOutput(dispatch) const onItemChange = useOnItemChange(updateTransactionOutput) @@ -225,6 +229,8 @@ export const useInitialize = ( return { fee, + totalAmount, + setTotalAmount, isTransactionValid, setIsTransactionValid, useOnTransactionChange, diff --git a/packages/neuron-ui/src/components/Send/index.tsx b/packages/neuron-ui/src/components/Send/index.tsx index 0beca4d7b2..12e4fc5f9f 100644 --- a/packages/neuron-ui/src/components/Send/index.tsx +++ b/packages/neuron-ui/src/components/Send/index.tsx @@ -20,9 +20,10 @@ import QRScanner from 'widgets/QRScanner' import { StateWithDispatch } from 'states/stateProvider/reducer' import appState from 'states/initStates/app' -import { PlaceHolders, CapacityUnit } from 'utils/const' +import { PlaceHolders, CapacityUnit, ErrorCode } from 'utils/const' import { shannonToCKBFormatter } from 'utils/formatters' +import { verifyTotalAmount } from 'utils/validators' import { useInitialize } from './hooks' export interface TransactionOutput { @@ -42,6 +43,8 @@ const Send = ({ const { t } = useTranslation() const { fee, + totalAmount, + setTotalAmount, isTransactionValid, setIsTransactionValid, useOnTransactionChange, @@ -55,14 +58,11 @@ const Send = ({ onGetAmountErrorMessage, onClear, } = useInitialize(send.outputs, send.price, send.cycles, dispatch, t) - useOnTransactionChange(walletID, send.outputs, dispatch, setIsTransactionValid) + useOnTransactionChange(walletID, send.outputs, dispatch, setIsTransactionValid, setTotalAmount) const leftStackWidth = '70%' const labelWidth = '140px' - const actionSpacer = ( - - - - ) + + const isAffordable = verifyTotalAmount(totalAmount, fee, balance) return ( @@ -159,15 +159,48 @@ const Send = ({ /> - + + 1 || !isAffordable ? 'flex' : 'none', + }, + }} + tokens={{ childrenGap: 20 }} + > + + + + + + + - + - {actionSpacer} @@ -197,7 +230,7 @@ const Send = ({ )} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 3eb6c2a076..64884b7c2c 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -90,6 +90,7 @@ "input-password-to-confirm": "Input password to confirm", "this-transaction-will-send": "This transaction will send", "scan-to-get-address": "Scan QR code to read the address to send to", + "total-amount": "Total Amount", "description": "Description", "description-optional": "Description (optional)", "balance": "Balance", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 6ff67e4ae3..56197889c8 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -90,6 +90,7 @@ "input-password-to-confirm": "输入密码以确认本次交易", "this-transaction-will-send": "本次交易将发送", "scan-to-get-address": "扫描二维码以获取地址", + "total-amount": "总金额", "description": "备注", "description-optional": "备注 (选填)", "balance": "余额", diff --git a/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts new file mode 100644 index 0000000000..d44e0c1b75 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/fixtures.ts @@ -0,0 +1,35 @@ +const fixtures: { + [title: string]: { + totalAmount: string + fee: string + balance: string + expected: boolean + } +} = { + 'Valid total amount': { + totalAmount: '10000000000000000000000', + fee: '1', + balance: '10000000000000000000001', + expected: true, + }, + 'Too large total amount should fail': { + totalAmount: '10000000000000000000001', + fee: '0', + balance: '10000000000000000000000', + expected: false, + }, + 'Too large fee should fail': { + totalAmount: '10000000000000000000000', + fee: '1', + balance: '10000000000000000000000', + expected: false, + }, + 'Negative balance should fail': { + totalAmount: '10000000000000000000000', + fee: '10000000000', + balance: '-10000000000010000000000', + expected: false, + }, +} + +export default fixtures diff --git a/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts new file mode 100644 index 0000000000..3b18e5a712 --- /dev/null +++ b/packages/neuron-ui/src/tests/validators/verifyTotalAmount/index.test.ts @@ -0,0 +1,19 @@ +import { verifyTotalAmount } from 'utils/validators' +import fixtures from './fixtures' + +const fixtureTable = Object.entries(fixtures).map(([title, { totalAmount, fee, balance, expected }]) => [ + title, + totalAmount, + fee, + balance, + expected, +]) + +describe('Verify total amount', () => { + test.each(fixtureTable)( + `%s`, + (_title: string, totalAmount: string, fee: string, balance: string, expected: boolean) => { + expect(verifyTotalAmount(totalAmount, fee, balance)).toBe(expected) + } + ) +}) diff --git a/packages/neuron-ui/src/utils/validators.ts b/packages/neuron-ui/src/utils/validators.ts index 51b9632c80..cccd918f54 100644 --- a/packages/neuron-ui/src/utils/validators.ts +++ b/packages/neuron-ui/src/utils/validators.ts @@ -1,7 +1,6 @@ import { MAX_NETWORK_NAME_LENGTH } from 'utils/const' /* global BigInt */ import { ckbCore } from 'services/chain' -import { outputsToTotalCapacity } from 'utils/formatters' import { MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, MIN_AMOUNT, MAX_DECIMAL_DIGITS, ErrorCode } from './const' export const verifyAddress = (address: string): boolean => { @@ -33,8 +32,10 @@ export const verifyAmount = (amount: string = '0') => { return true } -export const verifyTotalAmount = (items: any, fee: string, balance: string) => { - const totalAmount = outputsToTotalCapacity(items) +export const verifyTotalAmount = (totalAmount: string, fee: string, balance: string) => { + if (+balance < 0) { + return false + } return BigInt(totalAmount) + BigInt(fee) <= BigInt(balance) } From 3b6cbb12983ddd6b107f0b66b353a7f86ded6688 Mon Sep 17 00:00:00 2001 From: Keith Date: Tue, 27 Aug 2019 12:53:57 +0800 Subject: [PATCH 12/12] refactor(neuron-ui): update i18n text --- packages/neuron-ui/src/components/Send/hooks.ts | 4 +++- packages/neuron-ui/src/containers/Notification/index.tsx | 4 ++-- packages/neuron-ui/src/locales/en.json | 1 + packages/neuron-ui/src/locales/zh.json | 3 ++- packages/neuron-ui/src/utils/const.ts | 1 - 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/neuron-ui/src/components/Send/hooks.ts b/packages/neuron-ui/src/components/Send/hooks.ts index e752b88ec3..6bcb392f5a 100644 --- a/packages/neuron-ui/src/components/Send/hooks.ts +++ b/packages/neuron-ui/src/components/Send/hooks.ts @@ -6,6 +6,7 @@ import { calculateCycles } from 'services/remote/wallets' import { outputsToTotalCapacity, priceToFee } from 'utils/formatters' import { verifyAddress, verifyAmount, verifyAmountRange, verifyTransactionOutputs } from 'utils/validators' import { ErrorCode } from 'utils/const' +import { MAX_DECIMAL_DIGITS } from '../../utils/const' import { TransactionOutput } from '.' let cyclesTimer: ReturnType @@ -90,7 +91,7 @@ const useOnTransactionChange = ( }) } }, 300) - }, [walletID, items, dispatch, setIsTransactionValid]) + }, [walletID, items, dispatch, setIsTransactionValid, setTotalAmount]) } const useOnSubmit = (items: TransactionOutput[], dispatch: StateDispatch) => @@ -214,6 +215,7 @@ export const useInitialize = ( return t(`messages.codes.${msg.code}`, { fieldName: 'amount', fieldValue: amount, + length: MAX_DECIMAL_DIGITS, }) } if (!verifyAmountRange(amount)) { diff --git a/packages/neuron-ui/src/containers/Notification/index.tsx b/packages/neuron-ui/src/containers/Notification/index.tsx index 72b0afe3dc..cce63b155e 100644 --- a/packages/neuron-ui/src/containers/Notification/index.tsx +++ b/packages/neuron-ui/src/containers/Notification/index.tsx @@ -101,7 +101,7 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef {notification.code ? t(`messages.codes.${notification.code}`, notification.meta) - : notification.content || 'Unknonw'} + : notification.content || t('messages.unknown-error')} ) : null} @@ -159,7 +159,7 @@ export const NoticeContent = ({ dispatch }: React.PropsWithoutRef {notification.code ? t(`messages.codes.${notification.code}`, notification.meta) - : notification.content || 'Unknonw'} + : notification.content || t('messages.unknown-error')} ) diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 64884b7c2c..54a6da2105 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -242,6 +242,7 @@ }, "messages": { "error": "Error", + "unknown-error": "Unknown error", "update-wallet-successfully": "Update the wallet successfully", "delete-wallet-successfully": "Delete the wallet successfully", "create-network-successfully": "Create a network successfully", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 56197889c8..c20c3828dd 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -186,7 +186,7 @@ "edit-network": { "title": "添加或编辑网络", "rpc-url": "RPC地址", - "name": "名字" + "name": "名称" } } }, @@ -242,6 +242,7 @@ }, "messages": { "error": "错误", + "unknown-error": "未知错误", "update-wallet-successfully": "已更新钱包信息", "delete-wallet-successfully": "已删除钱包", "create-network-successfully": "新节点已添加", diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 4939e49f91..ad02db9781 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -81,7 +81,6 @@ export enum ErrorCode { NotNegative = 207, ProtocolRequired = 208, NoWhiteSpaces = 209, - // Other errors FieldIrremovable = 301, FailToLaunch = 302, FieldNotFound = 303,