zcash-grant-system/frontend/client/components/ContributionModal/index.tsx

304 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Modal, Alert } from 'antd';
import Result from 'ant-design-pro/lib/Result';
import {
postProposalContribution,
getProposalContribution,
getUserSettings,
} from 'api/api';
import { ContributionWithAddressesAndUser } from 'types';
import PaymentInfo from './PaymentInfo';
import SetRefundAddress from './SetRefundAddress';
import { AppState } from 'store/reducers';
interface OwnProps {
isVisible: boolean;
contribution?: ContributionWithAddressesAndUser | Falsy;
proposalId?: number;
contributionId?: number;
amount?: string;
isAnonymous?: boolean;
hasNoButtons?: boolean;
text?: React.ReactNode;
handleClose(): void;
}
interface StateProps {
authUser: AppState['auth']['user'];
}
type Props = StateProps & OwnProps;
interface State {
hasConfirmedAnonymous: boolean;
hasSent: boolean;
contribution: ContributionWithAddressesAndUser | null;
needsRefundAddress: boolean;
noRefund: boolean;
isFetchingContribution: boolean;
error: string | null;
}
class ContributionModal extends React.Component<Props, State> {
state: State = {
hasConfirmedAnonymous: false,
hasSent: false,
contribution: null,
needsRefundAddress: false,
noRefund: false,
isFetchingContribution: false,
error: null,
};
constructor(props: Props) {
super(props);
if (props.contribution) {
this.state = {
...this.state,
contribution: props.contribution,
};
}
}
componentWillUpdate(nextProps: Props, nextState: State) {
const {
isVisible,
isAnonymous,
proposalId,
contributionId,
contribution,
} = nextProps;
// 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) {
if (this.props.isVisible !== isVisible || proposalId !== this.props.proposalId) {
this.fetchAddresses(proposalId, contributionId, nextState.noRefund);
}
}
// If contribution is provided, update it
if (contribution !== this.props.contribution) {
this.setState({ contribution: contribution || null });
}
// When the modal is closed, clear out the contribution, error, and anonymous check
if (this.props.isVisible && !isVisible) {
this.setState({
contribution: null,
hasConfirmedAnonymous: false,
hasSent: false,
needsRefundAddress: false,
noRefund: false,
error: null,
});
}
}
render() {
const { isVisible, isAnonymous, handleClose, text, authUser } = this.props;
const {
hasSent,
hasConfirmedAnonymous,
needsRefundAddress,
contribution,
error,
} = this.state;
let { hasNoButtons } = this.props;
let okText;
let onOk;
let content;
if (needsRefundAddress && authUser) {
hasNoButtons = true;
content = (
<SetRefundAddress
userid={authUser.userid}
onSetRefundAddress={this.confirmRefundAddressSet}
onSetNoRefund={this.confirmNoRefund}
/>
);
} else if (isAnonymous && !hasConfirmedAnonymous) {
okText = 'I accept';
onOk = this.confirmAnonymous;
content = (
<Alert
className="PaymentInfo-anonymous"
type="warning"
message="This contribution will not be attributed"
description={
<>
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.
<br /> <br />
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.
<br /> <br />
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.
<br /> <br />
NOTE: The Zcash Foundation is unable to accept donations of more than $5,000
USD worth of ZEC from anonymous users.
</>
}
/>
);
} else if (hasSent) {
okText = 'Done';
onOk = handleClose;
content = (
<Result
type="success"
title="Thank you for your contribution!"
description={
<>
Your transaction should be confirmed in about 20 minutes.{' '}
{isAnonymous ? (
'Once its confirmed, itll 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>.
</>
)}
</>
}
style={{ width: '90%' }}
/>
);
} else {
if (error) {
okText = 'Done';
onOk = handleClose;
// 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} />;
} else {
okText = 'Ive sent it';
onOk = this.confirmSend;
content = <PaymentInfo contribution={contribution} text={text} />;
}
}
return (
<Modal
title="Make your contribution"
visible={isVisible}
closable={hasSent || hasNoButtons}
maskClosable={hasSent || hasNoButtons}
okText={okText}
onOk={onOk}
onCancel={handleClose}
footer={hasNoButtons ? '' : undefined}
centered
>
{content}
</Modal>
);
}
private async fetchAddresses(
proposalId: number,
contributionId?: number,
noRefund?: boolean,
) {
this.setState({ isFetchingContribution: true });
try {
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;
}
}
let res;
if (contributionId) {
res = await getProposalContribution(proposalId, contributionId);
} else {
res = await postProposalContribution(
proposalId,
amount || '0',
isAnonymous,
noRefund,
);
}
this.setState({ contribution: res.data });
} catch (err) {
this.setState({ error: err.message });
}
this.setState({ isFetchingContribution: false });
}
private confirmAnonymous = () => {
this.setState({ hasConfirmedAnonymous: true }, () => {
const { state, props } = this;
if (!state.contribution && !props.contribution && props.proposalId) {
this.fetchAddresses(props.proposalId, props.contributionId, state.noRefund);
}
});
};
private confirmSend = () => {
this.setState({ hasSent: true });
};
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);
}
},
);
};
}
export default connect<StateProps, {}, OwnProps, AppState>(state => ({
authUser: state.auth.user,
}))(ContributionModal);