Send button on account (#1193)

* starting point

* show all accounts

* edit and save a name but remains ugly

* actually don't use deriveStateFromProps

* fix AdressRow

* indentation

* upperCase

* bring back the index

* trim input

* change edition l&f

* lint

* cater for small screens

* address comments

* lint and alphabetical order

* allow viewing and editing tags

* better UX when tag editing and styling for no tags

* nonce refactoring and show balances

* crypto type

* styling balances

* use css grid

* fix address row and creator

* with buttons

* button styling

* locked tooltip

* edit button always visible

* init

* fix conflicts

* transfer functionnality

* styling the modal

* clean up

* clean up bis

* Transfer dialog enhancements (#1196)

* Button/TxButton submit on enter (#1170)

* Button/TxButton submit on enter

* promise linting

* testing ref approach

* Add TxComponent to handle submit on enter

* remove accountNonce

* remove input ref

* remove TxButton constructor

* remove comment

* empty constructor

* linting

* bug with null ref

* linting

* [CI Skip]  0.33.0-beta.74

* Redirect correctly when user has no account (#1191)

* right link and hide tabs plus buttons primary

* fix comments

* create highlight

* [CI Skip]  0.33.0-beta.75

* Mark ExtrinsicFailed as error (#1190)

* Hide edit on injected accounts, Button w/ tooltip (#1192)

* Hide edit on injected accounts, Button w/ tooltip

* Don't display "add tags" when not isEditable

* Indentation

* [CI Skip]  0.33.0-beta.76

* Make enable() call to extensions (#1194)

* Make enable() call to extensions

* It works

* Add overlay for injected (prompt)

* Use signer from injectedPromise

* [CI Skip]  0.33.0-beta.77

* withInjected -> PureComponent (fix new refs) (#1195)

* withInjected -> PureComponent (fix new refs)

* Consume data from context

* Expand injected

* Close modal on tx start

* align fees

* remove blank lines
This commit is contained in:
Thibaut Sardan 2019-05-24 13:03:54 +02:00 committed by GitHub
parent 3e929c1f21
commit fc59f53869
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 320 additions and 33 deletions

View File

@ -7,12 +7,14 @@ import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import styled from 'styled-components';
import { AddressSummary, Available, Balance, Bonded, Button, CryptoType, Nonce, Unlocking } from '@polkadot/ui-app';
import { AddressSummary, Available, Balance, Bonded, Button, CryptoType, Icon, Nonce, Unlocking } from '@polkadot/ui-app';
import keyring from '@polkadot/ui-keyring';
import Backup from './modals/Backup';
import ChangePass from './modals/ChangePass';
import Forgetting from './modals/Forgetting';
import Transfer from './modals/Transfer';
import translate from './translate';
type Props = I18nProps & {
@ -23,7 +25,8 @@ type State = {
isBackupOpen: boolean,
isEditable: boolean,
isForgetOpen: boolean,
isPasswordOpen: boolean
isPasswordOpen: boolean,
isTransferOpen: boolean
};
const Wrapper = styled.article`
@ -111,7 +114,8 @@ class Account extends React.PureComponent<Props> {
isBackupOpen: false,
isEditable: !(keyring.getAccount(props.address).getMeta().isInjected),
isForgetOpen: false,
isPasswordOpen: false
isPasswordOpen: false,
isTransferOpen: false
};
}
@ -187,7 +191,7 @@ class Account extends React.PureComponent<Props> {
private renderModals () {
const { address } = this.props;
const { isBackupOpen, isForgetOpen, isPasswordOpen } = this.state;
const { isBackupOpen, isForgetOpen, isPasswordOpen, isTransferOpen } = this.state;
if (!address) {
return null;
@ -226,6 +230,16 @@ class Account extends React.PureComponent<Props> {
);
}
if (isTransferOpen) {
modals.push(
<Transfer
address={address}
key='modal-transfer'
onClose={this.toggleTransfer}
/>
);
}
return modals;
}
@ -253,6 +267,14 @@ class Account extends React.PureComponent<Props> {
});
}
private toggleTransfer = (): void => {
const { isTransferOpen } = this.state;
this.setState({
isTransferOpen: !isTransferOpen
});
}
private onForget = (): void => {
const { address, t } = this.props;
@ -314,32 +336,39 @@ class Account extends React.PureComponent<Props> {
const { t } = this.props;
const { isEditable } = this.state;
if (!isEditable) {
return null;
}
return (
<div className='accounts--Account-buttons'>
{isEditable && (
<>
<Button
isNegative
onClick={this.toggleForget}
icon='trash'
size='small'
tooltip={t('Forget this account')}
/>
<Button
icon='cloud download'
isPrimary
onClick={this.toggleBackup}
size='small'
tooltip={t('Create a backup file for this account')}
/>
<Button
icon='key'
isPrimary
onClick={this.togglePass}
size='small'
tooltip={t("Change this account's password")}
/>
</>
)}
<Button
isNegative
onClick={this.toggleForget}
icon='trash'
size='small'
tooltip={t('Forget this account')}
/>
<Button
icon='cloud download'
isPrimary
onClick={this.toggleBackup}
label={<><Icon name='paper plane' /> {t('send')}</>}
onClick={this.toggleTransfer}
size='small'
tooltip={t('Create a backup file for this account')}
/>
<Button
icon='key'
isPrimary
onClick={this.togglePass}
size='small'
tooltip={t("Change this account's password")}
tooltip={t('Send funds from this account')}
/>
</div>
);

View File

@ -0,0 +1,247 @@
// Copyright 2017-2019 @polkadot/app-accounts 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 { ApiProps } from '@polkadot/ui-api/types';
import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/ui-app/types';
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import { Button, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/ui-app';
import Checks, { calcSignatureLength } from '@polkadot/ui-signer/Checks';
import { withApi, withCalls, withMulti } from '@polkadot/ui-api';
import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants';
import translate from '../translate';
type Props = ApiProps & I18nProps & {
address: string,
balances_fees?: DerivedFees,
balances_votingBalance?: DerivedBalances,
onClose: () => void,
system_accountNonce?: BN
};
type State = {
amount: BN,
extrinsic: SubmittableExtrinsic | null,
hasAvailable: boolean,
maxBalance?: BN,
recipientId: string | null
};
const ZERO = new BN(0);
const Wrapper = styled.div`
article.padded {
box-shadow: none;
margin: .75rem 15rem;
padding: 0;
}
label.with-help {
flex-basis: 10rem;
}
.ui--Labelled-content {
flex: initial;
width: 40em;
}
`;
class Transfer extends React.PureComponent<Props> {
state: State = {
amount: ZERO,
extrinsic: null,
hasAvailable: true,
maxBalance: ZERO,
recipientId: null
};
componentDidUpdate (prevProps: Props, prevState: State) {
const { balances_fees } = this.props;
const { extrinsic, recipientId } = this.state;
const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0);
if ((recipientId && prevState.recipientId !== recipientId) ||
(balances_fees !== prevProps.balances_fees) ||
hasLengthChanged
) {
this.setMaxBalance();
}
}
render () {
return (
<Modal
className='app--accounts-Modal'
dimmer='inverted'
open
>
{this.renderContent()}
{this.renderButtons()}
</Modal>
);
}
private nextState (newState: Partial<State>): void {
this.setState((prevState: State): State => {
const { api } = this.props;
const { amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance } = newState;
const extrinsic = recipientId
? api.tx.balances.transfer(recipientId, amount)
: null;
return {
amount,
extrinsic,
hasAvailable,
maxBalance,
recipientId
};
});
}
private renderButtons () {
const { address, onClose, t } = this.props;
const { extrinsic, hasAvailable } = this.state;
return (
<Modal.Actions>
<Button.Group>
<Button
isNegative
label={t('Cancel')}
onClick={onClose}
/>
<Button.Or />
<TxButton
accountId={address}
extrinsic={extrinsic}
isDisabled={!hasAvailable}
isPrimary
label={t('Make Transfer')}
onStart={onClose}
withSpinner={false}
/>
</Button.Group>
</Modal.Actions>
);
}
private renderContent () {
const { address, t } = this.props;
const { extrinsic, hasAvailable, maxBalance } = this.state;
return (
<>
<Modal.Header>
{t('Send funds')}
</Modal.Header>
<Modal.Content className='app--account-Backup-content'>
<Wrapper className='account--Transfer-data'>
<InputAddress
defaultValue={address}
help={t('The account you will send funds from.')}
isDisabled
label={t('from')}
type='account'
/>
<InputAddress
defaultValue={'5xxxxxxxxxxxxxxx'}
help={t('Select a contact or paste the address you want to send funds to.')}
label={t('to')}
onChange={this.onChangeTo}
type='all'
/>
<InputBalance
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}
withMax
/>
<Checks
accountId={address}
extrinsic={extrinsic}
isSendable
onChange={this.onChangeFees}
/>
</Wrapper>
</Modal.Content>
</>
);
}
private onChangeAmount = (amount: BN = new BN(0)) => {
this.nextState({ amount });
}
private onChangeTo = (recipientId: string) => {
this.nextState({ recipientId });
}
private onChangeFees = (hasAvailable: boolean) => {
this.setState({ hasAvailable });
}
private setMaxBalance = () => {
const { address, api, balances_fees = ZERO_FEES } = this.props;
const { recipientId } = this.state;
if (!address || !recipientId) {
return;
}
void api.query.system.accountNonce(address, (accountNonce) => {
const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees;
void api.derive.balances.all(address, ({ availableBalance: senderBalance }) => {
void api.derive.balances.all(recipientId, ({ availableBalance: recipientBalance }) => {
let prevMax = new BN(0);
let maxBalance = new BN(1);
let extrinsic;
while (!prevMax.eq(maxBalance)) {
prevMax = maxBalance;
extrinsic = address && recipientId
? api.tx.balances.transfer(recipientId, prevMax)
: null;
const txLength = calcSignatureLength(extrinsic, accountNonce);
const fees = transactionBaseFee
.add(transactionByteFee.muln(txLength))
.add(transferFee)
.add(senderBalance.isZero() ? creationFee : ZERO)
.add(recipientBalance.isZero() ? creationFee : ZERO);
maxBalance = new BN(senderBalance).sub(fees);
}
this.nextState({
extrinsic,
maxBalance
});
});
});
});
}
}
export default withMulti(
Transfer,
translate,
withApi,
withCalls<Props>(
'derive.balances.fees'
)
);

View File

@ -171,7 +171,7 @@ export default class Queue extends React.Component<Props, State> {
return id;
}
queueExtrinsic = ({ accountId, extrinsic, signerCb, signerOptions, txFailedCb, txSuccessCb, txUpdateCb, isUnsigned }: PartialQueueTx$Extrinsic): number => {
queueExtrinsic = ({ accountId, extrinsic, signerCb, signerOptions, txFailedCb, txSuccessCb, txStartCb, txUpdateCb, isUnsigned }: PartialQueueTx$Extrinsic): number => {
return this.queueAdd({
accountId,
extrinsic,
@ -180,6 +180,7 @@ export default class Queue extends React.Component<Props, State> {
signerOptions,
txFailedCb,
txSuccessCb,
txStartCb,
txUpdateCb
});
}

View File

@ -39,6 +39,7 @@ export type QueueTx = AccountInfo & {
signerOptions?: SignatureOptions,
txFailedCb?: (status: SubmittableResult | null) => void,
txSuccessCb?: TxCallback,
txStartCb?: () => void,
txUpdateCb?: TxCallback,
values?: Array<any>,
status: QueueTx$Status
@ -75,6 +76,7 @@ export type PartialQueueTx$Extrinsic = PartialAccountInfo & {
signerOptions?: SignatureOptions,
txFailedCb?: TxCallback,
txSuccessCb?: TxCallback,
txStartCb?: () => void,
txUpdateCb?: TxCallback,
isUnsigned?: boolean
};

View File

@ -38,10 +38,12 @@ type Props = ApiProps & {
label: React.ReactNode,
onClick?: () => any,
onFailed?: TxCallback,
onStart?: () => void,
onSuccess?: TxCallback,
onUpdate?: TxCallback,
params?: Array<any> | ConstructFn,
tx?: string
tx?: string,
withSpinner?: boolean
};
type InnerProps = Props & InjectedProps;
@ -79,7 +81,7 @@ class TxButtonInner extends React.PureComponent<InnerProps> {
}
send = (): void => {
const { accountId, api, isUnsigned, onClick, onUpdate, params = [], queueExtrinsic, tx = '', extrinsic: propsExtrinsic } = this.props;
const { accountId, api, extrinsic: propsExtrinsic, isUnsigned, onClick, onFailed, onStart, onSuccess, onUpdate, params = [], queueExtrinsic, tx = '', withSpinner = true } = this.props;
let extrinsic: any;
if (propsExtrinsic) {
@ -98,14 +100,17 @@ class TxButtonInner extends React.PureComponent<InnerProps> {
assert(extrinsic, 'Expected generated extrinsic passed to TxButton');
this.setState({ isSending: true });
if (withSpinner) {
this.setState({ isSending: true });
}
queueExtrinsic({
accountId,
extrinsic,
isUnsigned,
txFailedCb: this.onFailed,
txSuccessCb: this.onSuccess,
txFailedCb: withSpinner ? this.onFailed : onFailed,
txStartCb: onStart,
txSuccessCb: withSpinner ? this.onSuccess : onSuccess,
txUpdateCb: onUpdate
});

View File

@ -249,7 +249,6 @@ button.ui.icon.withDrawUnbonded {
text-align: right;
}
.ui--AddressSummary-balance.rx--updated,
.ui--AddressSummary-nonce.rx--updated {
opacity: 1;

View File

@ -312,7 +312,7 @@ class Signer extends React.PureComponent<Props, State> {
}
}
private async makeExtrinsicCall (extrinsic: SubmittableExtrinsic, { id, txFailedCb, txSuccessCb, txUpdateCb }: QueueTx, extrinsicCall: (...params: Array<any>) => any, pair?: KeyringPair): Promise<void> {
private async makeExtrinsicCall (extrinsic: SubmittableExtrinsic, { id, txFailedCb, txSuccessCb, txStartCb, txUpdateCb }: QueueTx, extrinsicCall: (...params: Array<any>) => any, pair?: KeyringPair): Promise<void> {
const { api, injectedPromise, queueSetTxStatus } = this.props;
console.log('makeExtrinsicCall: extrinsic ::', extrinsic.toHex());
@ -365,6 +365,10 @@ class Signer extends React.PureComponent<Props, State> {
});
}
}]);
if (isFunction(txStartCb)) {
txStartCb();
}
} catch (error) {
console.error('makeExtrinsicCall: error:', error.message);
queueSetTxStatus(id, 'error', {}, error);