Show send instructions on profile pending contributions.

This commit is contained in:
Will O'Beirne 2019-01-09 17:26:28 -05:00
parent 5a922cefee
commit 6c8ce3f87d
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
8 changed files with 98 additions and 31 deletions

View File

@ -220,6 +220,7 @@ export function postProposalContribution(
}); });
} }
export function postProposalComment(payload: { export function postProposalComment(payload: {
proposalId: number; proposalId: number;
parentCommentId?: number; parentCommentId?: number;
@ -232,3 +233,10 @@ export function postProposalComment(payload: {
export function deleteProposalContribution(contributionId: string | number) { export function deleteProposalContribution(contributionId: string | number) {
return axios.delete(`/api/v1/proposals/contribution/${contributionId}`); return axios.delete(`/api/v1/proposals/contribution/${contributionId}`);
} }
export function getProposalContribution(
proposalId: number,
contributionId: number,
): Promise<{ data: ContributionWithAddresses }> {
return axios.get(`/api/v1/proposals/${proposalId}/contributions/${contributionId}`);
}

View File

@ -19,6 +19,7 @@
border-bottom: 1px solid rgba(#000, 0.06); border-bottom: 1px solid rgba(#000, 0.06);
&-qr { &-qr {
position: relative;
padding: 0.5rem; padding: 0.5rem;
margin-right: 1rem; margin-right: 1rem;
border-radius: 4px; border-radius: 4px;
@ -28,6 +29,16 @@
canvas { canvas {
display: block; display: block;
} }
&.is-loading canvas {
opacity: 0;
}
.ant-spin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
} }
&-info { &-info {

View File

@ -67,8 +67,13 @@ export default class PaymentInfo extends React.Component<Props, State> {
</Radio.Group> </Radio.Group>
<div className="PaymentInfo-uri"> <div className="PaymentInfo-uri">
<div className="PaymentInfo-uri-qr"> <div className={
{uri ? <QRCode value={uri} /> : <Spin />} classnames('PaymentInfo-uri-qr', !uri && 'is-loading')
}>
<span style={{ opacity: uri ? 1 : 0 }}>
<QRCode value={uri || ''} />
</span>
{!uri && <Spin size="large" />}
</div> </div>
<div className="PaymentInfo-uri-info"> <div className="PaymentInfo-uri-info">
<CopyInput <CopyInput

View File

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import { Modal } from 'antd'; import { Modal } from 'antd';
import Result from 'ant-design-pro/lib/Result'; import Result from 'ant-design-pro/lib/Result';
import { postProposalContribution } from 'api/api'; import { postProposalContribution, getProposalContribution } from 'api/api';
import { ContributionWithAddresses } from 'types'; import { ContributionWithAddresses } from 'types';
import PaymentInfo from './PaymentInfo'; import PaymentInfo from './PaymentInfo';
interface OwnProps { interface OwnProps {
isVisible: boolean; isVisible: boolean;
proposalId: number; proposalId?: number;
contributionId?: number;
amount?: string; amount?: string;
hasNoButtons?: boolean;
handleClose(): void; handleClose(): void;
} }
@ -28,17 +30,21 @@ export default class ContributionModal extends React.Component<Props, State> {
}; };
componentWillUpdate(nextProps: Props) { componentWillUpdate(nextProps: Props) {
const { isVisible, proposalId } = nextProps const { isVisible, proposalId, contributionId } = nextProps;
if (isVisible && this.props.isVisible !== isVisible) { // When modal is opened and proposalId is provided or changed
this.fetchAddresses(proposalId); if (isVisible && proposalId) {
} if (
else if (proposalId !== this.props.proposalId) { this.props.isVisible !== isVisible ||
this.fetchAddresses(proposalId); proposalId !== this.props.proposalId
) {
this.fetchAddresses(proposalId, contributionId);
} }
} }
}
render() { render() {
const { isVisible, handleClose } = this.props; const { isVisible, handleClose, hasNoButtons } = this.props;
const { hasSent, contribution, error } = this.state; const { hasSent, contribution, error } = this.state;
let content; let content;
@ -68,11 +74,12 @@ export default class ContributionModal extends React.Component<Props, State> {
<Modal <Modal
title="Make your contribution" title="Make your contribution"
visible={isVisible} visible={isVisible}
closable={hasSent} closable={hasSent || hasNoButtons}
maskClosable={hasSent} maskClosable={hasSent || hasNoButtons}
okText={hasSent ? 'Done' : 'Ive sent it'} okText={hasSent ? 'Done' : 'Ive sent it'}
onOk={hasSent ? handleClose : this.confirmSend} onOk={hasSent ? handleClose : this.confirmSend}
onCancel={handleClose} onCancel={handleClose}
footer={hasNoButtons ? '' : undefined}
centered centered
> >
{content} {content}
@ -80,12 +87,20 @@ export default class ContributionModal extends React.Component<Props, State> {
); );
} }
private async fetchAddresses(proposalId: number) { private async fetchAddresses(
proposalId: number,
contributionId?: number,
) {
try { try {
const res = await postProposalContribution( let res;
if (contributionId) {
res = await getProposalContribution(proposalId, contributionId);
} else {
res = await postProposalContribution(
proposalId, proposalId,
this.props.amount || '0', this.props.amount || '0',
); );
}
this.setState({ contribution: res.data }); this.setState({ contribution: res.data });
} catch(err) { } catch(err) {
this.setState({ error: err.message }); this.setState({ error: err.message });

View File

@ -12,6 +12,7 @@ import './ProfileContribution.less';
interface OwnProps { interface OwnProps {
userId: number; userId: number;
contribution: UserContribution; contribution: UserContribution;
showSendInstructions(contribution: UserContribution): void;
} }
interface DispatchProps { interface DispatchProps {
@ -30,7 +31,6 @@ class ProfileContribution extends React.Component<Props> {
let tag; let tag;
let actions: React.ReactNode; let actions: React.ReactNode;
if (isConfirmed) { if (isConfirmed) {
// TODO: Link to block explorer
actions = ( actions = (
<a <a
href={formatTxExplorerUrl(contribution.txId as string)} href={formatTxExplorerUrl(contribution.txId as string)}
@ -50,12 +50,15 @@ class ProfileContribution extends React.Component<Props> {
> >
<a>Delete</a> <a>Delete</a>
</Popconfirm> </Popconfirm>
<a>Contact support</a> <Link to="/support">Contact support</Link>
</>; </>;
} else { } else {
tag = <Tag color="orange">Pending</Tag>; tag = <Tag color="orange">Pending</Tag>;
// TODO: Show ContributionModal actions = (
actions = <a>View instructions</a>; <a onClick={() => this.props.showSendInstructions(contribution)}>
View send instructions
</a>
);
} }
return ( return (

View File

@ -21,7 +21,9 @@ import ProfileComment from './ProfileComment';
import ProfileInvite from './ProfileInvite'; import ProfileInvite from './ProfileInvite';
import Placeholder from 'components/Placeholder'; import Placeholder from 'components/Placeholder';
import Exception from 'pages/exception'; import Exception from 'pages/exception';
import ContributionModal from 'components/ContributionModal';
import './style.less'; import './style.less';
import { UserContribution } from 'types';
interface StateProps { interface StateProps {
usersMap: AppState['users']['map']; usersMap: AppState['users']['map'];
@ -35,7 +37,15 @@ interface DispatchProps {
type Props = RouteComponentProps<any> & StateProps & DispatchProps; type Props = RouteComponentProps<any> & StateProps & DispatchProps;
class Profile extends React.Component<Props> { interface State {
activeContribution: UserContribution | null;
}
class Profile extends React.Component<Props, State> {
state: State = {
activeContribution: null,
};
componentDidMount() { componentDidMount() {
this.fetchData(); this.fetchData();
} }
@ -50,6 +60,8 @@ class Profile extends React.Component<Props> {
render() { render() {
const userLookupParam = this.props.match.params.id; const userLookupParam = this.props.match.params.id;
const { authUser, match } = this.props; const { authUser, match } = this.props;
const { activeContribution } = this.state;
if (!userLookupParam) { if (!userLookupParam) {
if (authUser && authUser.userid) { if (authUser && authUser.userid) {
return <Redirect to={`/profile/${authUser.userid}`} />; return <Redirect to={`/profile/${authUser.userid}`} />;
@ -128,7 +140,12 @@ class Profile extends React.Component<Props> {
<div> <div>
{noneFunded && <Placeholder subtitle="Has not funded any proposals yet" />} {noneFunded && <Placeholder subtitle="Has not funded any proposals yet" />}
{contributions.map(c => ( {contributions.map(c => (
<ProfileContribution key={c.id} userId={user.userid} contribution={c} /> <ProfileContribution
key={c.id}
userId={user.userid}
contribution={c}
showSendInstructions={this.openContributionModal}
/>
))} ))}
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
@ -160,9 +177,20 @@ class Profile extends React.Component<Props> {
</Tabs.TabPane> </Tabs.TabPane>
)} )}
</Tabs> </Tabs>
<ContributionModal
isVisible={!!activeContribution}
proposalId={
activeContribution ? activeContribution.proposal.proposalId : undefined
}
contributionId={activeContribution ? activeContribution.id : undefined}
hasNoButtons
handleClose={this.closeContributionModal}
/>
</div> </div>
); );
} }
private fetchData() { private fetchData() {
const { match } = this.props; const { match } = this.props;
const userLookupId = match.params.id; const userLookupId = match.params.id;
@ -171,6 +199,9 @@ class Profile extends React.Component<Props> {
this.props.fetchUserInvites(userLookupId); this.props.fetchUserInvites(userLookupId);
} }
} }
private openContributionModal = (c: UserContribution) => this.setState({ activeContribution: c });
private closeContributionModal = () => this.setState({ activeContribution: null });
} }
const TabTitle = (disp: string, count: number) => ( const TabTitle = (disp: string, count: number) => (

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { ProposalCampaignBlock } from 'components/Proposal/CampaignBlock'; import { ProposalCampaignBlock } from 'components/Proposal/CampaignBlock';
import Contributors from 'components/Proposal/Contributors';
import 'styles/style.less'; import 'styles/style.less';
import 'components/Proposal/style.less'; import 'components/Proposal/style.less';
@ -53,9 +52,4 @@ storiesOf('Proposal', module)
<div style={{ display: 'flex', flexWrap: 'wrap' }}> <div style={{ display: 'flex', flexWrap: 'wrap' }}>
<CampaignBlocks style={{ margin: '0 12px' }} /> <CampaignBlocks style={{ margin: '0 12px' }} />
</div> </div>
))
.add('Contributors', () => (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Contributors />
</div>
)); ));

View File

@ -2,7 +2,7 @@ import { Zat } from 'utils/units';
import { Proposal, User } from 'types'; import { Proposal, User } from 'types';
export interface Contribution { export interface Contribution {
id: string; id: number;
txId: string; txId: string;
amount: string; amount: string;
dateCreated: number; dateCreated: number;