Turn SideBar transfer into modal link (#1217)

* Turn SideBar transfer into modal link

* ui--RecentlyOffline badge location
This commit is contained in:
Jaco Greeff 2019-05-27 22:24:59 +02:00 committed by GitHub
parent 1bb275fb15
commit 5b5ab129d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 165 additions and 377 deletions

View File

@ -20,7 +20,7 @@ import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
import translate from '../translate';
type Props = ApiProps & I18nProps & {
address: string,
address?: string,
balances_fees?: DerivedFees,
balances_votingBalance?: DerivedBalances,
onClose: () => void,
@ -32,8 +32,8 @@ type State = {
extrinsic: SubmittableExtrinsic | null,
hasAvailable: boolean,
maxBalance?: BN,
recipientId: string | null,
senderId: string
recipientId?: string | null,
senderId?: string | null
};
const ZERO = new BN(0);
@ -77,7 +77,7 @@ class Transfer extends React.PureComponent<Props> {
hasAvailable: true,
maxBalance: ZERO,
recipientId: null,
senderId: props.address
senderId: props.address || null
};
}
@ -115,7 +115,7 @@ class Transfer extends React.PureComponent<Props> {
this.setState((prevState: State): State => {
const { api } = this.props;
const { amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance, senderId = prevState.senderId } = newState;
const extrinsic = recipientId
const extrinsic = recipientId && senderId
? api.tx.balances.transfer(recipientId, amount)
: null;
@ -131,8 +131,8 @@ class Transfer extends React.PureComponent<Props> {
}
private renderButtons () {
const { address, onClose, t } = this.props;
const { extrinsic, hasAvailable } = this.state;
const { onClose, t } = this.props;
const { extrinsic, hasAvailable, senderId } = this.state;
return (
<Modal.Actions>
@ -144,7 +144,7 @@ class Transfer extends React.PureComponent<Props> {
/>
<Button.Or />
<TxButton
accountId={address}
accountId={senderId}
extrinsic={extrinsic}
isDisabled={!hasAvailable}
isPrimary
@ -168,8 +168,9 @@ class Transfer extends React.PureComponent<Props> {
<InputAddress
defaultValue={address}
help={t('The account you will send funds from.')}
isDisabled
isDisabled={!!address}
label={t('send from account')}
onChange={this.onChangeFrom}
type='account'
/>
<div className='balance'><Available label={available} params={senderId} /></div>
@ -189,7 +190,7 @@ class Transfer extends React.PureComponent<Props> {
withMax
/>
<Checks
accountId={address}
accountId={senderId}
extrinsic={extrinsic}
isSendable
onChange={this.onChangeFees}
@ -203,6 +204,10 @@ class Transfer extends React.PureComponent<Props> {
this.nextState({ amount });
}
private onChangeFrom = (senderId: string) => {
this.nextState({ senderId });
}
private onChangeTo = (recipientId: string) => {
this.nextState({ recipientId });
}
@ -212,10 +217,10 @@ class Transfer extends React.PureComponent<Props> {
}
private setMaxBalance = async () => {
const { address, api, balances_fees = ZERO_FEES } = this.props;
const { recipientId } = this.state;
const { api, balances_fees = ZERO_FEES } = this.props;
const { senderId, recipientId } = this.state;
if (!address || !recipientId) {
if (!senderId || !recipientId) {
return;
}
@ -224,8 +229,8 @@ class Transfer extends React.PureComponent<Props> {
// FIXME The any casts here are irritating, but they are basically caused by the derive
// not really returning an actual `class implements Codec`
// (if casting to DerivedBalance it would be `as any as DerivedBalance`)
const accountNonce = await api.query.system.accountNonce(address) as Index;
const senderBalance = (await api.derive.balances.all(address) as any).availableBalance;
const accountNonce = await api.query.system.accountNonce(senderId) as Index;
const senderBalance = (await api.derive.balances.all(senderId) as any).availableBalance;
const recipientBalance = (await api.derive.balances.all(recipientId) as any).availableBalance;
let prevMax = new BN(0);

View File

@ -1,243 +0,0 @@
// Copyright 2017-2019 @polkadot/app-transfer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/ui-app/types';
import { ApiProps } from '@polkadot/ui-api/types';
import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import { Index } from '@polkadot/types';
import { AddressRow, Button, InputAddress, InputBalance, TxButton, TxComponent } from '@polkadot/ui-app';
import { withApi, withCalls, withMulti } from '@polkadot/ui-api';
import keyring from '@polkadot/ui-keyring';
import Checks, { calcSignatureLength } from '@polkadot/ui-signer/Checks';
import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
import translate from './translate';
type Props = I18nProps & ApiProps & {
balances_fees?: DerivedFees,
balances_votingBalance?: DerivedBalances
};
type State = {
accountId: string | null,
amount: BN,
extrinsic: SubmittableExtrinsic | null,
hasAvailable: boolean,
maxBalance?: BN,
recipientId: string | null
};
const ZERO = new BN(0);
const Wrapper = styled.div`
.transfer--Transfer-address {
flex: 0 1;
padding: 1.5rem 0;
.ui--AddressSummary {
text-align: center;
}
}
.transfer--Transfer-data {
flex: 1 1;
min-width: 0;
padding: 0 1rem;
}
.transfer--Transfer-info {
display: flex;
flex-direction: row;
}
`;
class Transfer extends TxComponent<Props, State> {
state: State = {
accountId: null,
amount: ZERO,
extrinsic: null,
hasAvailable: true,
maxBalance: ZERO,
recipientId: null
};
componentDidUpdate (prevProps: Props, prevState: State) {
const { balances_fees } = this.props;
const { accountId, extrinsic, recipientId } = this.state;
const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0);
if ((accountId && prevState.accountId !== accountId) ||
(recipientId && prevState.recipientId !== recipientId) ||
(balances_fees !== prevProps.balances_fees) ||
hasLengthChanged
) {
this.setMaxBalance().catch(console.error);
}
}
render () {
const { t } = this.props;
const { accountId, extrinsic, recipientId, hasAvailable, maxBalance } = this.state;
return (
<Wrapper>
<div className='transfer--Transfer-info'>
{this.renderAddress(accountId, 'medium')}
<div className='transfer--Transfer-data'>
<InputAddress
help={t('Select the account you want to send funds from.')}
label={t('from')}
onChange={this.onChangeFrom}
type='account'
/>
<InputAddress
help={t('Select a contact or paste the address you want to send funds to.')}
label={t('to')}
onChange={this.onChangeTo}
type='all'
/>
<InputBalance
autoFocus
help={t('Type the amount you want to transfer. Note that you can select the unit on the right e.g sending 1 mili is equivalent to sending 0.001.')}
isError={!hasAvailable}
label={t('amount')}
maxValue={maxBalance}
onChange={this.onChangeAmount}
onEnter={this.sendTx}
withMax
/>
<Checks
accountId={accountId}
extrinsic={extrinsic}
isSendable
onChange={this.onChangeFees}
/>
<Button.Group>
<TxButton
accountId={accountId}
isDisabled={!hasAvailable}
isPrimary
label={t('Make Transfer')}
extrinsic={extrinsic}
ref={this.button}
/>
</Button.Group>
</div>
{this.renderAddress(recipientId, 'large')}
</div>
</Wrapper>
);
}
private renderAddress (accountId: string | null, media: 'large' | 'medium') {
if (!accountId) {
return null;
}
try {
keyring.decodeAddress(accountId);
} catch (err) {
return null;
}
return (
<div className={`transfer--Transfer-address ui--media-${media}`}>
<AddressRow
value={accountId}
withAvailable
/>
</div>
);
}
private nextState (newState: Partial<State>): void {
this.setState((prevState: State): State => {
const { api } = this.props;
const { accountId = prevState.accountId, amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance } = newState;
const extrinsic = accountId && recipientId
? api.tx.balances.transfer(recipientId, amount)
: null;
return {
accountId,
amount,
extrinsic,
hasAvailable,
maxBalance,
recipientId
};
});
}
private setMaxBalance = async () => {
const { api, balances_fees = ZERO_FEES } = this.props;
const { accountId, recipientId } = this.state;
if (!accountId || !recipientId) {
return;
}
const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees;
// FIXME The any casts here are irritating, but they are basically caused by the derive
// not really returning an actual `class implements Codec`
// (if casting to DerivedBalance it would be `as any as DerivedBalance`)
const accountNonce = await api.query.system.accountNonce(accountId) as Index;
const senderBalance = (await api.derive.balances.all(accountId) as any).availableBalance;
const recipientBalance = (await api.derive.balances.all(recipientId) as any).availableBalance;
let prevMax = new BN(0);
let maxBalance = new BN(1);
let extrinsic;
while (!prevMax.eq(maxBalance)) {
prevMax = maxBalance;
extrinsic = api.tx.balances.transfer(recipientId, prevMax);
const txLength = calcSignatureLength(extrinsic, accountNonce);
const fees = transactionBaseFee
.add(transactionByteFee.muln(txLength))
.add(transferFee)
.add(recipientBalance.isZero() ? creationFee : ZERO);
maxBalance = senderBalance.sub(fees);
}
this.nextState({
extrinsic,
maxBalance
});
}
private onChangeFrom = (accountId: string) => {
this.nextState({ accountId });
}
private onChangeAmount = (amount: BN = new BN(0)) => {
this.nextState({ amount });
}
private onChangeTo = (recipientId: string) => {
this.nextState({ recipientId });
}
private onChangeFees = (hasAvailable: boolean) => {
this.setState({ hasAvailable });
}
}
export default withMulti(
Transfer,
translate,
withApi,
withCalls<Props>(
'derive.balances.fees'
)
);

View File

@ -2,37 +2,14 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { AppProps } from '@polkadot/ui-app/types';
import React from 'react';
import { HelpOverlay,Tabs } from '@polkadot/ui-app';
import basicMd from './md/basic.md';
import Transfer from './Transfer';
import translate from './translate';
type Props = AppProps;
type Props = AppProps & I18nProps;
class App extends React.PureComponent<Props> {
export default class App extends React.PureComponent<Props> {
render () {
const { basePath, t } = this.props;
return (
<main className='transfer--App'>
<HelpOverlay md={basicMd} />
<header>
<Tabs
basePath={basePath}
items={[{
name: 'create',
text: t('Balance transfer')
}]}
/>
</header>
<Transfer />
</main>
);
return 'Deprecated, now operates via modal';
}
}
export default translate(App);

View File

@ -1,22 +0,0 @@
# Transfer
This tab allows you to send funds from one account you own to another account.
# Minimum allowed balance
Accounts with a balance lower than the minimal amount, 100 miliUnits (miliDOTs for Polkadot) as of writing, are considered as inexistent for the network. If an account's balance ever drops below this amount, it is removed from the network. In this application, it will still be visible, but with a balance of 0.
For a fund transfer to a **new account** (read an account with a balance of 0), if the amount transferred is less than the minimum allowed balance, then the transfer will "succeed" but the destination account will not be created (read its balance will remain 0); this essentially burns the transfer balance from the sender, because the receiver's balance never exceed the minimum allowed balance.
If the receiver already exists (read it has a balance greater than 0), it is perfectly possible to transfer very low amounts.
# Fees
Fees will be added to the transfer amount on top of the amount you wish to send.
An information box will show you the amount of fees that you will need to pay for the transfer and the total amount that will be deducted from the sender.
- transfer:
You need to pay fees to transfer funds from one account to the other in order to pay for the security of the network.
- account creation
In addition to the transfer fees, if you are sending funds to an account with a 0 balance, not only do you need to make sure to send enough funds (see the "minimum allowed balance" section) but you will need to pay a creation fee. This is to prevent the generation of unused account.

View File

@ -1,7 +0,0 @@
// Copyright 2017-2019 @polkadot/app-transfer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { withTranslation } from 'react-i18next';
export default withTranslation(['transfer', 'ui']);

View File

@ -5,10 +5,12 @@
import { Routes } from './types';
import Transfer from '@polkadot/app-transfer';
import TransferModal from '@polkadot/app-accounts/modals/Transfer';
export default ([
{
Component: Transfer,
Modal: TransferModal,
display: {
isHidden: false,
needsAccounts: true,

View File

@ -9,8 +9,10 @@ export type RouteProps = AppProps & BareProps;
export type Route = {
Component: React.ComponentType<RouteProps>,
Modal?: React.ComponentType<any>,
display: {
isHidden?: boolean,
isModal?: boolean,
needsAccounts?: boolean,
needsApi?: Array<string | Array<string>>,
needsSudo?: boolean

View File

@ -14,6 +14,8 @@ import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import { withApi,withCalls, withMulti, withObservable } from '@polkadot/ui-api';
import { isFunction } from '@polkadot/util';
import translate from '../translate';
type Props = I18nProps & ApiProps & {
isCollapsed: boolean,
onClick: () => void,
@ -24,32 +26,54 @@ type Props = I18nProps & ApiProps & {
class Item extends React.PureComponent<Props> {
render () {
const { route: { i18n, icon, name }, t, isCollapsed } = this.props;
const { route: { Modal, i18n, icon, name }, t, isCollapsed, onClick } = this.props;
if (!this.isVisible()) {
return null;
}
const body = (
<>
<Icon name={icon} />
<span className='text'>{t(`sidebar.${name}`, i18n)}</span>
<Tooltip
offset={{ right: -4 }}
place='right'
text={t(`sidebar.${name}`, i18n)}
trigger={`nav-${name}`}
/>
</>
);
return (
<Menu.Item className='apps--SideBar-Item'>
<NavLink
activeClassName='apps--SideBar-Item-NavLink-active'
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={this.props.onClick}
to={`/${name}`}
>
<Icon name={icon} />
<span className='text'>{t(`sidebar.${name}`, i18n)}</span>
<Tooltip
offset={{ right: -4 }}
place='right'
text={t(`sidebar.${name}`, i18n)}
trigger={`nav-${name}`}
/>
</NavLink>
{
Modal
? (
<a
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
>
{body}
</a>
)
: (
<NavLink
activeClassName='apps--SideBar-Item-NavLink-active'
className='apps--SideBar-Item-NavLink'
data-for={`nav-${name}`}
data-tip
data-tip-disable={!isCollapsed}
onClick={onClick}
to={`/${name}`}
>
{body}
</NavLink>
)
}
</Menu.Item>
);
}
@ -103,6 +127,7 @@ class Item extends React.PureComponent<Props> {
export default withMulti(
Item,
translate,
withApi,
withCalls<Props>(
['query.sudo.key', { transform: key => key.toString() }]

View File

@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Route } from '@polkadot/apps-routing/types';
import { I18nProps } from '@polkadot/ui-app/types';
import { SIDEBAR_MENU_THRESHOLD } from '../constants';
@ -28,6 +29,12 @@ type Props = I18nProps & {
toggleMenu: () => void
};
type State = {
modals: {
[index: string]: boolean
}
};
const Toggle = styled.img`
background: ${logoBackground};
padding: ${logoPadding};
@ -54,21 +61,40 @@ const Toggle = styled.img`
`}
`;
class SideBar extends React.PureComponent<Props> {
class SideBar extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
// setup modals for each of the actual modal routes
this.state = {
modals: routing.routes.reduce((result, route) => {
if (route && route.Modal) {
result[route.name] = false;
}
return result;
}, {} as { [index: string]: boolean })
};
}
render () {
const { isCollapsed } = this.props;
const { handleResize, isCollapsed, toggleMenu, menuOpen } = this.props;
const logo = getLogo(true);
return (
<Responsive
onUpdate={this.props.handleResize}
className={
classes(
'apps-SideBar-Wrapper',
isCollapsed ? 'collapsed' : 'expanded'
)
}
onUpdate={handleResize}
className={classes('apps-SideBar-Wrapper', isCollapsed ? 'collapsed' : 'expanded')}
>
{this.renderMenuToggle()}
<Toggle
alt='logo'
className={menuOpen ? 'closed' : 'open delayed'}
onClick={toggleMenu}
src={logo}
/>
{this.renderModals()}
<div className='apps--SideBar'>
<Menu
secondary
@ -83,13 +109,18 @@ class SideBar extends React.PureComponent<Props> {
<Menu.Divider hidden />
{
isCollapsed
? null
? undefined
: <NodeInfo />
}
</div>
{this.renderCollapse()}
</Menu>
{this.renderToggleBar()}
<Responsive minWidth={SIDEBAR_MENU_THRESHOLD}>
<div
className='apps--SideBar-toggle'
onClick={this.props.collapse}
/>
</Responsive>
</div>
</Responsive>
);
@ -126,9 +157,24 @@ class SideBar extends React.PureComponent<Props> {
);
}
private renderModals () {
const { modals } = this.state;
const filtered = routing.routes.filter((route) => route && route.Modal) as Array<Route>;
return filtered.map(({ name, Modal }) => (
Modal && modals[name]
? (
<Modal
key={name}
onClose={this.closeModal(name)}
/>
)
: <div key={name} />
));
}
private renderRoutes () {
const { isCollapsed } = this.props;
const { t } = this.props;
const { handleResize, isCollapsed } = this.props;
return routing.routes.map((route, index) => (
route
@ -137,8 +183,11 @@ class SideBar extends React.PureComponent<Props> {
isCollapsed={isCollapsed}
key={route.name}
route={route}
onClick={this.props.handleResize}
t={t}
onClick={
route.Modal
? this.openModal(route.name)
: handleResize
}
/>
)
: (
@ -164,32 +213,6 @@ class SideBar extends React.PureComponent<Props> {
);
}
private renderToggleBar () {
return (
<Responsive minWidth={SIDEBAR_MENU_THRESHOLD}>
<div
className='apps--SideBar-toggle'
onClick={this.props.collapse}
>
</div>
</Responsive>
);
}
private renderMenuToggle () {
const logo = getLogo(true);
const { toggleMenu, menuOpen } = this.props;
return (
<Toggle
alt='logo'
className={menuOpen ? 'closed' : 'open delayed'}
onClick={toggleMenu}
src={logo}
/>
);
}
private renderWiki () {
return (
<Menu.Item className='apps--SideBar-Item'>
@ -203,6 +226,28 @@ class SideBar extends React.PureComponent<Props> {
</Menu.Item>
);
}
private closeModal = (name: string) => {
return () => {
this.setState(({ modals }) => ({
modals: {
...modals,
[name]: false
}
}));
};
}
private openModal = (name: string) => {
return () => {
this.setState(({ modals }) => ({
modals: {
...modals,
[name]: true
}
}));
};
}
}
export default translate(SideBar);

View File

@ -17,6 +17,10 @@ body {
height: 100%;
}
a {
cursor: pointer;
}
h1, h2, h3, h4, h5 {
color: rgba(0, 0, 0, .6);
font-family: sans-serif;

View File

@ -437,12 +437,12 @@ button.ui.icon.iconButton {
.ui--RecentlyOffline {
position: absolute;
top: 0.75rem;
top: -0.5rem;
font-size: 12px;
cursor: help;
display: flex;
justify-content: center;
left: 0.75rem;
left: -0.5rem;
width: 22px;
height: 22px;
padding: 0;