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:
parent
3e929c1f21
commit
fc59f53869
@ -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>
|
||||
);
|
||||
|
||||
247
packages/app-accounts/src/modals/Transfer.tsx
Normal file
247
packages/app-accounts/src/modals/Transfer.tsx
Normal 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'
|
||||
)
|
||||
);
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -249,7 +249,6 @@ button.ui.icon.withDrawUnbonded {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.ui--AddressSummary-balance.rx--updated,
|
||||
.ui--AddressSummary-nonce.rx--updated {
|
||||
opacity: 1;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user