2019-01-06 14:48:07 -08:00
|
|
|
|
import React from 'react';
|
2019-03-13 16:36:24 -07:00
|
|
|
|
import { connect } from 'react-redux';
|
2019-01-15 12:19:59 -08:00
|
|
|
|
import { Link } from 'react-router-dom';
|
2019-02-23 12:31:07 -08:00
|
|
|
|
import { Modal, Alert } from 'antd';
|
2019-01-06 14:48:07 -08:00
|
|
|
|
import Result from 'ant-design-pro/lib/Result';
|
2019-03-13 16:36:24 -07:00
|
|
|
|
import {
|
|
|
|
|
postProposalContribution,
|
|
|
|
|
getProposalContribution,
|
|
|
|
|
getUserSettings,
|
|
|
|
|
} from 'api/api';
|
2019-02-23 12:31:07 -08:00
|
|
|
|
import { ContributionWithAddressesAndUser } from 'types';
|
2019-01-06 14:48:07 -08:00
|
|
|
|
import PaymentInfo from './PaymentInfo';
|
2019-03-13 16:36:24 -07:00
|
|
|
|
import SetRefundAddress from './SetRefundAddress';
|
|
|
|
|
import { AppState } from 'store/reducers';
|
2019-01-06 14:48:07 -08:00
|
|
|
|
|
|
|
|
|
interface OwnProps {
|
|
|
|
|
isVisible: boolean;
|
2019-02-23 12:31:07 -08:00
|
|
|
|
contribution?: ContributionWithAddressesAndUser | Falsy;
|
2019-01-09 14:26:28 -08:00
|
|
|
|
proposalId?: number;
|
|
|
|
|
contributionId?: number;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
amount?: string;
|
2019-02-23 12:31:07 -08:00
|
|
|
|
isAnonymous?: boolean;
|
2019-01-09 14:26:28 -08:00
|
|
|
|
hasNoButtons?: boolean;
|
2019-02-05 17:45:57 -08:00
|
|
|
|
text?: React.ReactNode;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
handleClose(): void;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 16:36:24 -07:00
|
|
|
|
interface StateProps {
|
|
|
|
|
authUser: AppState['auth']['user'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Props = StateProps & OwnProps;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
|
|
|
|
|
interface State {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
hasConfirmedAnonymous: boolean;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
hasSent: boolean;
|
2019-02-23 12:31:07 -08:00
|
|
|
|
contribution: ContributionWithAddressesAndUser | null;
|
2019-03-13 16:36:24 -07:00
|
|
|
|
needsRefundAddress: boolean;
|
|
|
|
|
noRefund: boolean;
|
2019-02-25 11:26:02 -08:00
|
|
|
|
isFetchingContribution: boolean;
|
2019-01-06 22:58:33 -08:00
|
|
|
|
error: string | null;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 16:36:24 -07:00
|
|
|
|
class ContributionModal extends React.Component<Props, State> {
|
2019-01-06 14:48:07 -08:00
|
|
|
|
state: State = {
|
2019-02-25 11:26:02 -08:00
|
|
|
|
hasConfirmedAnonymous: false,
|
2019-01-06 14:48:07 -08:00
|
|
|
|
hasSent: false,
|
2019-01-06 22:58:33 -08:00
|
|
|
|
contribution: null,
|
2019-03-13 16:36:24 -07:00
|
|
|
|
needsRefundAddress: false,
|
|
|
|
|
noRefund: false,
|
2019-02-25 11:26:02 -08:00
|
|
|
|
isFetchingContribution: false,
|
2019-01-06 22:58:33 -08:00
|
|
|
|
error: null,
|
2019-01-06 14:48:07 -08:00
|
|
|
|
};
|
|
|
|
|
|
2019-02-05 17:45:57 -08:00
|
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
if (props.contribution) {
|
|
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
|
|
|
|
contribution: props.contribution,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 16:36:24 -07:00
|
|
|
|
componentWillUpdate(nextProps: Props, nextState: State) {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
const {
|
|
|
|
|
isVisible,
|
2019-02-25 11:26:02 -08:00
|
|
|
|
isAnonymous,
|
2019-02-23 12:31:07 -08:00
|
|
|
|
proposalId,
|
|
|
|
|
contributionId,
|
|
|
|
|
contribution,
|
|
|
|
|
} = nextProps;
|
2019-02-25 11:26:02 -08:00
|
|
|
|
// When modal is opened and proposalId is provided or changed
|
|
|
|
|
// But not if we're anonymous, that will happen in confirmAnonymous
|
|
|
|
|
if (isVisible && proposalId && !isAnonymous) {
|
2019-02-05 17:45:57 -08:00
|
|
|
|
if (this.props.isVisible !== isVisible || proposalId !== this.props.proposalId) {
|
2019-03-13 16:36:24 -07:00
|
|
|
|
this.fetchAddresses(proposalId, contributionId, nextState.noRefund);
|
2019-01-09 14:26:28 -08:00
|
|
|
|
}
|
2019-01-06 22:58:33 -08:00
|
|
|
|
}
|
2019-02-23 12:31:07 -08:00
|
|
|
|
// If contribution is provided, update it
|
2019-02-05 17:45:57 -08:00
|
|
|
|
if (contribution !== this.props.contribution) {
|
2019-02-25 11:46:47 -08:00
|
|
|
|
this.setState({ contribution: contribution || null });
|
|
|
|
|
}
|
2019-03-12 10:10:56 -07:00
|
|
|
|
// When the modal is closed, clear out the contribution, error, and anonymous check
|
2019-02-25 11:46:47 -08:00
|
|
|
|
if (this.props.isVisible && !isVisible) {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
this.setState({
|
2019-02-25 11:46:47 -08:00
|
|
|
|
contribution: null,
|
|
|
|
|
hasConfirmedAnonymous: false,
|
2019-03-12 15:20:11 -07:00
|
|
|
|
hasSent: false,
|
2019-03-13 16:36:24 -07:00
|
|
|
|
needsRefundAddress: false,
|
|
|
|
|
noRefund: false,
|
2019-03-12 10:10:56 -07:00
|
|
|
|
error: null,
|
2019-02-23 12:31:07 -08:00
|
|
|
|
});
|
2019-02-05 17:45:57 -08:00
|
|
|
|
}
|
2019-01-06 22:58:33 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-06 14:48:07 -08:00
|
|
|
|
render() {
|
2019-03-13 16:36:24 -07:00
|
|
|
|
const { isVisible, isAnonymous, handleClose, text, authUser } = this.props;
|
|
|
|
|
const {
|
|
|
|
|
hasSent,
|
|
|
|
|
hasConfirmedAnonymous,
|
|
|
|
|
needsRefundAddress,
|
|
|
|
|
contribution,
|
|
|
|
|
error,
|
|
|
|
|
} = this.state;
|
|
|
|
|
let { hasNoButtons } = this.props;
|
2019-02-23 12:31:07 -08:00
|
|
|
|
let okText;
|
|
|
|
|
let onOk;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
let content;
|
|
|
|
|
|
2019-03-13 16:36:24 -07:00
|
|
|
|
if (needsRefundAddress && authUser) {
|
|
|
|
|
hasNoButtons = true;
|
|
|
|
|
content = (
|
|
|
|
|
<SetRefundAddress
|
|
|
|
|
userid={authUser.userid}
|
|
|
|
|
onSetRefundAddress={this.confirmRefundAddressSet}
|
|
|
|
|
onSetNoRefund={this.confirmNoRefund}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
} else if (isAnonymous && !hasConfirmedAnonymous) {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
okText = 'I accept';
|
|
|
|
|
onOk = this.confirmAnonymous;
|
|
|
|
|
content = (
|
|
|
|
|
<Alert
|
|
|
|
|
className="PaymentInfo-anonymous"
|
|
|
|
|
type="warning"
|
2019-03-12 09:38:35 -07:00
|
|
|
|
message="This contribution will not be attributed"
|
2019-02-23 12:31:07 -08:00
|
|
|
|
description={
|
|
|
|
|
<>
|
2019-03-12 15:20:11 -07:00
|
|
|
|
Your contribution will show up without attribution. Even if you're logged
|
|
|
|
|
in, the contribution will not appear anywhere on your account after you
|
|
|
|
|
close this modal.
|
2019-02-23 12:31:07 -08:00
|
|
|
|
<br /> <br />
|
2019-03-12 15:20:11 -07:00
|
|
|
|
ZF Grants is unable to offer refunds for non-attributed contributions. If
|
|
|
|
|
refunds for this campaign are issued, your contribution will be treated as a
|
|
|
|
|
donation to the Zcash Foundation.
|
2019-02-23 12:31:07 -08:00
|
|
|
|
<br /> <br />
|
2019-03-12 15:20:11 -07:00
|
|
|
|
If you would like to have your contribution attached to an account and
|
|
|
|
|
remain eligible for refunds, you can close this modal, make sure you're
|
|
|
|
|
logged in, and don't check the "Contribute without attribution" checkbox.
|
2019-03-12 16:04:32 -07:00
|
|
|
|
<br /> <br />
|
2019-03-12 20:35:38 -07:00
|
|
|
|
NOTE: The Zcash Foundation is unable to accept donations of more than $5,000
|
|
|
|
|
USD worth of ZEC from anonymous users.
|
2019-02-23 12:31:07 -08:00
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
} else if (hasSent) {
|
|
|
|
|
okText = 'Done';
|
|
|
|
|
onOk = handleClose;
|
2019-01-06 14:48:07 -08:00
|
|
|
|
content = (
|
|
|
|
|
<Result
|
|
|
|
|
type="success"
|
|
|
|
|
title="Thank you for your contribution!"
|
|
|
|
|
description={
|
|
|
|
|
<>
|
2019-02-25 11:46:47 -08:00
|
|
|
|
Your transaction should be confirmed in about 20 minutes.{' '}
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{isAnonymous ? (
|
|
|
|
|
'Once it’s confirmed, it’ll show up in the contributions tab.'
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
You can keep an eye on it at the{' '}
|
|
|
|
|
<Link to="/profile?tab=funded">funded tab on your profile</Link>.
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2019-01-06 14:48:07 -08:00
|
|
|
|
</>
|
|
|
|
|
}
|
2019-01-06 21:42:24 -08:00
|
|
|
|
style={{ width: '90%' }}
|
2019-01-06 14:48:07 -08:00
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
2019-01-06 22:58:33 -08:00
|
|
|
|
if (error) {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
okText = 'Done';
|
|
|
|
|
onOk = handleClose;
|
2019-03-12 20:35:38 -07:00
|
|
|
|
// This should probably key on non-display text, but oh well.
|
|
|
|
|
let title;
|
|
|
|
|
let description;
|
|
|
|
|
if (error.includes('too many times')) {
|
|
|
|
|
title = 'Take it easy!';
|
|
|
|
|
description = `
|
|
|
|
|
We appreciate your enthusiasm, but you've made too many
|
|
|
|
|
contributions too fast. Please wait for your other contributions,
|
|
|
|
|
and try again later.
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
title = 'Something went wrong';
|
|
|
|
|
description = `
|
|
|
|
|
We were unable to get your contribution started. Please check back
|
|
|
|
|
soon, we're working to fix the problem as soon as possible.
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
content = <Result type="error" title={title} description={description} />;
|
2019-01-06 22:58:33 -08:00
|
|
|
|
} else {
|
2019-02-23 12:31:07 -08:00
|
|
|
|
okText = 'I’ve sent it';
|
|
|
|
|
onOk = this.confirmSend;
|
2019-02-05 17:45:57 -08:00
|
|
|
|
content = <PaymentInfo contribution={contribution} text={text} />;
|
2019-01-06 22:58:33 -08:00
|
|
|
|
}
|
2019-01-06 14:48:07 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
title="Make your contribution"
|
|
|
|
|
visible={isVisible}
|
2019-01-09 14:26:28 -08:00
|
|
|
|
closable={hasSent || hasNoButtons}
|
|
|
|
|
maskClosable={hasSent || hasNoButtons}
|
2019-02-23 12:31:07 -08:00
|
|
|
|
okText={okText}
|
|
|
|
|
onOk={onOk}
|
2019-01-06 21:42:24 -08:00
|
|
|
|
onCancel={handleClose}
|
2019-01-09 14:26:28 -08:00
|
|
|
|
footer={hasNoButtons ? '' : undefined}
|
2019-01-06 21:42:24 -08:00
|
|
|
|
centered
|
2019-01-06 14:48:07 -08:00
|
|
|
|
>
|
|
|
|
|
{content}
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 16:36:24 -07:00
|
|
|
|
private async fetchAddresses(
|
|
|
|
|
proposalId: number,
|
|
|
|
|
contributionId?: number,
|
|
|
|
|
noRefund?: boolean,
|
|
|
|
|
) {
|
2019-02-25 11:26:02 -08:00
|
|
|
|
this.setState({ isFetchingContribution: true });
|
2019-01-06 22:58:33 -08:00
|
|
|
|
try {
|
2019-03-13 16:36:24 -07:00
|
|
|
|
const { amount, isAnonymous, authUser } = this.props;
|
|
|
|
|
|
|
|
|
|
// Ensure auth'd users have a refund address unless they've confirmed
|
|
|
|
|
if (!isAnonymous && !noRefund) {
|
|
|
|
|
// This should never happen, but make Typescript happy
|
|
|
|
|
if (!authUser) {
|
|
|
|
|
throw new Error('You must be logged in to contribute non-anonymously');
|
|
|
|
|
}
|
|
|
|
|
const { data: settings } = await getUserSettings(authUser.userid);
|
|
|
|
|
if (!settings.refundAddress) {
|
|
|
|
|
this.setState({ needsRefundAddress: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-09 14:26:28 -08:00
|
|
|
|
let res;
|
|
|
|
|
if (contributionId) {
|
|
|
|
|
res = await getProposalContribution(proposalId, contributionId);
|
|
|
|
|
} else {
|
2019-03-13 16:36:24 -07:00
|
|
|
|
res = await postProposalContribution(
|
|
|
|
|
proposalId,
|
|
|
|
|
amount || '0',
|
|
|
|
|
isAnonymous,
|
|
|
|
|
noRefund,
|
|
|
|
|
);
|
2019-01-09 14:26:28 -08:00
|
|
|
|
}
|
2019-01-06 22:58:33 -08:00
|
|
|
|
this.setState({ contribution: res.data });
|
2019-02-05 17:45:57 -08:00
|
|
|
|
} catch (err) {
|
2019-01-06 22:58:33 -08:00
|
|
|
|
this.setState({ error: err.message });
|
|
|
|
|
}
|
2019-02-25 11:26:02 -08:00
|
|
|
|
this.setState({ isFetchingContribution: false });
|
2019-01-06 22:58:33 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-23 12:31:07 -08:00
|
|
|
|
private confirmAnonymous = () => {
|
2019-03-13 16:36:24 -07:00
|
|
|
|
this.setState({ hasConfirmedAnonymous: true }, () => {
|
|
|
|
|
const { state, props } = this;
|
|
|
|
|
if (!state.contribution && !props.contribution && props.proposalId) {
|
|
|
|
|
this.fetchAddresses(props.proposalId, props.contributionId, state.noRefund);
|
|
|
|
|
}
|
|
|
|
|
});
|
2019-02-23 12:31:07 -08:00
|
|
|
|
};
|
|
|
|
|
|
2019-01-06 14:48:07 -08:00
|
|
|
|
private confirmSend = () => {
|
|
|
|
|
this.setState({ hasSent: true });
|
|
|
|
|
};
|
2019-03-13 16:36:24 -07:00
|
|
|
|
|
|
|
|
|
private confirmRefundAddressSet = () => {
|
|
|
|
|
this.setState(
|
|
|
|
|
{
|
|
|
|
|
needsRefundAddress: false,
|
|
|
|
|
noRefund: false,
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
const { state, props } = this;
|
|
|
|
|
if (!state.contribution && !props.contribution && props.proposalId) {
|
|
|
|
|
this.fetchAddresses(props.proposalId, props.contributionId, state.noRefund);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private confirmNoRefund = () => {
|
|
|
|
|
this.setState(
|
|
|
|
|
{
|
|
|
|
|
needsRefundAddress: false,
|
|
|
|
|
noRefund: true,
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
const { state, props } = this;
|
|
|
|
|
if (!state.contribution && !props.contribution && props.proposalId) {
|
|
|
|
|
this.fetchAddresses(props.proposalId, props.contributionId, state.noRefund);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
};
|
2019-01-06 14:48:07 -08:00
|
|
|
|
}
|
2019-03-13 16:36:24 -07:00
|
|
|
|
|
|
|
|
|
export default connect<StateProps, {}, OwnProps, AppState>(state => ({
|
|
|
|
|
authUser: state.auth.user,
|
|
|
|
|
}))(ContributionModal);
|