Require refund address or confirm donation (#354)
* Add API check to disallow unsetting refund address. * Require either refund address to be set or explicit consent for a donation. Dont have them show up as refundable in admin. * Show donations on financials page * Continue after entry * Consider no_refund when checking for duplicate contribution. * Fix types. * Add a filter for all contributions that are considered donations. Update financial query to include donations. * Elaborate in message.
This commit is contained in:
parent
e018f11018
commit
9d0591918e
|
@ -137,6 +137,7 @@ class ContributionDetail extends React.Component<Props, State> {
|
|||
: <em>N/A</em>
|
||||
)}
|
||||
{renderDeetItem('staking tx', JSON.stringify(c.staking))}
|
||||
{renderDeetItem('no refund', JSON.stringify(c.noRefund))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -20,7 +20,7 @@ class Financials extends React.Component {
|
|||
return (
|
||||
<div className="Financials">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Col lg={8} md={12} sm={24}>
|
||||
<Card size="small" title="Contributions">
|
||||
<Charts.Pie
|
||||
hasLegend
|
||||
|
@ -38,13 +38,16 @@ class Financials extends React.Component {
|
|||
{ x: 'funding', y: parseFloat(contributions.funding) },
|
||||
{ x: 'refunding', y: parseFloat(contributions.refunding) },
|
||||
{ x: 'refunded', y: parseFloat(contributions.refunded) },
|
||||
{ x: 'donation', y: parseFloat(contributions.donations) },
|
||||
{ x: 'staking', y: parseFloat(contributions.staking) },
|
||||
]}
|
||||
valueFormat={val => <span dangerouslySetInnerHTML={{ __html: val }} />}
|
||||
height={180}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col lg={8} md={12} sm={24}>
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
|
@ -85,7 +88,9 @@ class Financials extends React.Component {
|
|||
height={180}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col lg={8} md={12} sm={24}>
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
|
|
|
@ -240,6 +240,7 @@ const app = store({
|
|||
funded: '0',
|
||||
refunding: '0',
|
||||
refunded: '0',
|
||||
donations: '0',
|
||||
},
|
||||
payouts: {
|
||||
total: '0',
|
||||
|
|
|
@ -151,6 +151,7 @@ export interface Contribution {
|
|||
memo: string;
|
||||
};
|
||||
staking: boolean;
|
||||
noRefund: boolean;
|
||||
refundAddress?: string;
|
||||
refundTxId?: string;
|
||||
}
|
||||
|
|
|
@ -92,6 +92,11 @@ const CONTRIBUTION_FILTERS = CONTRIBUTION_STATUSES.map(s => ({
|
|||
display: 'Refundable',
|
||||
color: '#afd500',
|
||||
group: 'Refundable',
|
||||
}, {
|
||||
id: 'DONATION',
|
||||
display: 'Donations',
|
||||
color: '#afd500',
|
||||
group: 'Donations',
|
||||
}]);
|
||||
|
||||
export const contributionFilters: Filters = {
|
||||
|
|
|
@ -155,6 +155,7 @@ def stats():
|
|||
contribution_refundable_count = db.session.query(func.count(ProposalContribution.id)) \
|
||||
.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.staking == False) \
|
||||
.filter(ProposalContribution.no_refund == False) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
|
@ -751,10 +752,13 @@ def financials():
|
|||
'funding': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage = 'FUNDING_REQUIRED'"))),
|
||||
'funded': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage in ('WIP', 'COMPLETED')"))),
|
||||
'refunding': str(ex(sql_pc_p(
|
||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.refund_tx_id IS NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
||||
))),
|
||||
'refunded': str(ex(sql_pc_p(
|
||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.refund_tx_id IS NOT NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NOT NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
||||
))),
|
||||
'donations': str(ex(sql_pc_p(
|
||||
"(pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.refund_tx_id IS NULL) AND (pc.no_refund = TRUE OR pc.user_id IS NULL) AND p.stage IN ('CANCELED', 'FAILED')"
|
||||
))),
|
||||
'gross': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.refund_tx_id IS NULL"))),
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ class ProposalContribution(db.Model):
|
|||
tx_id = db.Column(db.String(255), nullable=True)
|
||||
refund_tx_id = db.Column(db.String(255), nullable=True)
|
||||
staking = db.Column(db.Boolean, nullable=False)
|
||||
no_refund = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
user = db.relationship("User")
|
||||
|
||||
|
@ -94,20 +95,23 @@ class ProposalContribution(db.Model):
|
|||
amount: str,
|
||||
user_id: int = None,
|
||||
staking: bool = False,
|
||||
no_refund: bool = False,
|
||||
):
|
||||
self.proposal_id = proposal_id
|
||||
self.amount = amount
|
||||
self.user_id = user_id
|
||||
self.staking = staking
|
||||
self.no_refund = no_refund
|
||||
self.date_created = datetime.datetime.now()
|
||||
self.status = ContributionStatus.PENDING
|
||||
|
||||
@staticmethod
|
||||
def get_existing_contribution(user_id: int, proposal_id: int, amount: str):
|
||||
def get_existing_contribution(user_id: int, proposal_id: int, amount: str, no_refund: bool = False):
|
||||
return ProposalContribution.query.filter_by(
|
||||
user_id=user_id,
|
||||
proposal_id=proposal_id,
|
||||
amount=amount,
|
||||
no_refund=no_refund,
|
||||
status=ContributionStatus.PENDING,
|
||||
).first()
|
||||
|
||||
|
@ -359,12 +363,19 @@ class Proposal(db.Model):
|
|||
self.set_contribution_matching(0)
|
||||
self.set_contribution_bounty('0')
|
||||
|
||||
def create_contribution(self, amount, user_id: int = None, staking: bool = False):
|
||||
def create_contribution(
|
||||
self,
|
||||
amount,
|
||||
user_id: int = None,
|
||||
staking: bool = False,
|
||||
no_refund: bool = False,
|
||||
):
|
||||
contribution = ProposalContribution(
|
||||
proposal_id=self.id,
|
||||
amount=amount,
|
||||
user_id=user_id,
|
||||
staking=staking,
|
||||
no_refund=no_refund,
|
||||
)
|
||||
db.session.add(contribution)
|
||||
db.session.flush()
|
||||
|
@ -812,7 +823,8 @@ class AdminProposalContributionSchema(ma.Schema):
|
|||
"addresses",
|
||||
"refund_address",
|
||||
"refund_tx_id",
|
||||
"staking"
|
||||
"staking",
|
||||
"no_refund",
|
||||
)
|
||||
|
||||
proposal = ma.Nested("ProposalSchema")
|
||||
|
|
|
@ -490,9 +490,10 @@ def get_proposal_contribution(proposal_id, contribution_id):
|
|||
# TODO add gaurd (minimum, maximum)
|
||||
@body({
|
||||
"amount": fields.Str(required=True),
|
||||
"anonymous": fields.Bool(required=False, missing=None)
|
||||
"anonymous": fields.Bool(required=False, missing=None),
|
||||
"noRefund": fields.Bool(required=False, missing=False),
|
||||
})
|
||||
def post_proposal_contribution(proposal_id, amount, anonymous):
|
||||
def post_proposal_contribution(proposal_id, amount, anonymous, no_refund):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if not proposal:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
@ -505,12 +506,14 @@ def post_proposal_contribution(proposal_id, amount, anonymous):
|
|||
user = get_authed_user()
|
||||
|
||||
if user:
|
||||
contribution = ProposalContribution.get_existing_contribution(user.id, proposal_id, amount)
|
||||
contribution = ProposalContribution \
|
||||
.get_existing_contribution(user.id, proposal_id, amount, no_refund)
|
||||
|
||||
if not contribution:
|
||||
code = 201
|
||||
contribution = proposal.create_contribution(
|
||||
amount=amount,
|
||||
no_refund=no_refund,
|
||||
user_id=user.id if user else None,
|
||||
)
|
||||
|
||||
|
|
|
@ -357,6 +357,8 @@ def set_user_settings(user_id, email_subscriptions, refund_address):
|
|||
except ValidationException as e:
|
||||
return {"message": str(e)}, 400
|
||||
|
||||
if refund_address == '' and g.current_user.settings.refund_address:
|
||||
return {"message": "Refund address cannot be unset, only changed"}, 400
|
||||
if refund_address:
|
||||
g.current_user.settings.refund_address = refund_address
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ class ProposalPagination(Pagination):
|
|||
class ContributionPagination(Pagination):
|
||||
def __init__(self):
|
||||
self.FILTERS = [f'STATUS_{s}' for s in ContributionStatus.list()]
|
||||
self.FILTERS.extend(['REFUNDABLE'])
|
||||
self.FILTERS.extend(['REFUNDABLE', 'DONATION'])
|
||||
self.PAGE_SIZE = 9
|
||||
self.SORT_MAP = {
|
||||
'CREATED:DESC': ProposalContribution.date_created.desc(),
|
||||
|
@ -153,6 +153,7 @@ class ContributionPagination(Pagination):
|
|||
if 'REFUNDABLE' in filters:
|
||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.staking == False) \
|
||||
.filter(ProposalContribution.no_refund == False) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
|
@ -161,7 +162,20 @@ class ContributionPagination(Pagination):
|
|||
)) \
|
||||
.join(ProposalContribution.user) \
|
||||
.join(UserSettings) \
|
||||
.filter(UserSettings.refund_address != None) \
|
||||
.filter(UserSettings.refund_address != None)
|
||||
|
||||
if 'DONATION' in filters:
|
||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.filter(or_(
|
||||
ProposalContribution.no_refund == True,
|
||||
ProposalContribution.user_id == None,
|
||||
)) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
Proposal.stage == ProposalStage.FAILED,
|
||||
Proposal.stage == ProposalStage.CANCELED,
|
||||
))
|
||||
|
||||
|
||||
# SORT (see self.SORT_MAP)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"""Add no_refund column to contributions, default to false
|
||||
|
||||
Revision ID: 484abe10e157
|
||||
Revises: 13365ffe910e
|
||||
Create Date: 2019-03-13 16:35:01.520404
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '484abe10e157'
|
||||
down_revision = '13365ffe910e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('proposal_contribution', sa.Column('no_refund', sa.Boolean(), nullable=False, server_default=expression.false()))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('proposal_contribution', 'no_refund')
|
||||
# ### end Alembic commands ###
|
|
@ -13,6 +13,7 @@ import {
|
|||
RFP,
|
||||
ProposalPageParams,
|
||||
PageParams,
|
||||
UserSettings,
|
||||
} from 'types';
|
||||
import {
|
||||
formatUserForPost,
|
||||
|
@ -127,7 +128,9 @@ export function updateUser(user: User): Promise<{ data: User }> {
|
|||
return axios.put(`/api/v1/users/${user.userid}`, formatUserForPost(user));
|
||||
}
|
||||
|
||||
export function getUserSettings(userId: string | number): Promise<any> {
|
||||
export function getUserSettings(
|
||||
userId: string | number,
|
||||
): Promise<{ data: UserSettings }> {
|
||||
return axios.get(`/api/v1/users/${userId}/settings`);
|
||||
}
|
||||
|
||||
|
@ -138,7 +141,7 @@ interface SettingsArgs {
|
|||
export function updateUserSettings(
|
||||
userId: string | number,
|
||||
args?: SettingsArgs,
|
||||
): Promise<any> {
|
||||
): Promise<{ data: UserSettings }> {
|
||||
return axios.put(`/api/v1/users/${userId}/settings`, args);
|
||||
}
|
||||
|
||||
|
@ -313,11 +316,13 @@ export function putInviteResponse(
|
|||
export function postProposalContribution(
|
||||
proposalId: number,
|
||||
amount: string,
|
||||
anonymous?: boolean,
|
||||
anonymous: boolean = false,
|
||||
noRefund: boolean = false,
|
||||
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
||||
amount,
|
||||
anonymous,
|
||||
noRefund,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
.SetRefundAddress {
|
||||
.ant-alert {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&.ant-form-item-with-help {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
import { Form, Input, Button, message, Alert, Divider } from 'antd';
|
||||
import { updateUserSettings } from 'api/api';
|
||||
import { isValidAddress } from 'utils/validators';
|
||||
import './SetRefundAddress.less';
|
||||
|
||||
interface OwnProps {
|
||||
userid: number;
|
||||
onSetRefundAddress(): void;
|
||||
onSetNoRefund(): void;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
|
||||
const STATE = {
|
||||
refundAddress: '',
|
||||
isSaving: false,
|
||||
};
|
||||
|
||||
type State = typeof STATE;
|
||||
|
||||
export default class SetRefundAddress extends React.Component<Props, State> {
|
||||
state: State = { ...STATE };
|
||||
|
||||
render() {
|
||||
const { refundAddress, isSaving } = this.state;
|
||||
|
||||
let status: 'validating' | 'error' | undefined;
|
||||
let help;
|
||||
if (refundAddress && !isValidAddress(refundAddress)) {
|
||||
status = 'error';
|
||||
help = 'That doesn’t look like a valid address';
|
||||
}
|
||||
|
||||
return (
|
||||
<Form className="SetRefundAddress" layout="vertical" onSubmit={this.handleSubmit}>
|
||||
<Alert
|
||||
type="info"
|
||||
message="Please set a refund address"
|
||||
description={`
|
||||
If the proposal fails to reach its funding goal or gets
|
||||
canceled, we need an address to refund your contribution to.
|
||||
You’ll only need to set this once, and can change it later from
|
||||
the Settings page.
|
||||
`}
|
||||
/>
|
||||
|
||||
<Form.Item label="Refund address" validateStatus={status} help={help}>
|
||||
<Input
|
||||
value={refundAddress}
|
||||
placeholder="Z or T address"
|
||||
size="large"
|
||||
onChange={this.handleChange}
|
||||
disabled={isSaving}
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
disabled={!refundAddress || isSaving || !!status}
|
||||
loading={isSaving}
|
||||
block
|
||||
>
|
||||
Set refund address & continue
|
||||
</Button>
|
||||
|
||||
<Divider>or</Divider>
|
||||
|
||||
<Button type="danger" block onClick={this.props.onSetNoRefund}>
|
||||
I don't want a refund, consider it a donation instead
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ refundAddress: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
const { userid } = this.props;
|
||||
const { refundAddress } = this.state;
|
||||
if (!refundAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isSaving: true });
|
||||
try {
|
||||
await updateUserSettings(userid, { refundAddress });
|
||||
this.props.onSetRefundAddress();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.error(err.message || err.toString(), 5);
|
||||
this.setState({ isSaving: false });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,10 +1,17 @@
|
|||
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 } from 'api/api';
|
||||
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;
|
||||
|
@ -18,21 +25,29 @@ interface OwnProps {
|
|||
handleClose(): void;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
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;
|
||||
}
|
||||
|
||||
export default class ContributionModal extends React.Component<Props, State> {
|
||||
class ContributionModal extends React.Component<Props, State> {
|
||||
state: State = {
|
||||
hasConfirmedAnonymous: false,
|
||||
hasSent: false,
|
||||
contribution: null,
|
||||
needsRefundAddress: false,
|
||||
noRefund: false,
|
||||
isFetchingContribution: false,
|
||||
error: null,
|
||||
};
|
||||
|
@ -47,7 +62,7 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: Props) {
|
||||
componentWillUpdate(nextProps: Props, nextState: State) {
|
||||
const {
|
||||
isVisible,
|
||||
isAnonymous,
|
||||
|
@ -59,7 +74,7 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
// 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);
|
||||
this.fetchAddresses(proposalId, contributionId, nextState.noRefund);
|
||||
}
|
||||
}
|
||||
// If contribution is provided, update it
|
||||
|
@ -72,19 +87,37 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
contribution: null,
|
||||
hasConfirmedAnonymous: false,
|
||||
hasSent: false,
|
||||
needsRefundAddress: false,
|
||||
noRefund: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isVisible, isAnonymous, handleClose, hasNoButtons, text } = this.props;
|
||||
const { hasSent, hasConfirmedAnonymous, contribution, error } = this.state;
|
||||
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 (isAnonymous && !hasConfirmedAnonymous) {
|
||||
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 = (
|
||||
|
@ -181,15 +214,38 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private async fetchAddresses(proposalId: number, contributionId?: number) {
|
||||
private async fetchAddresses(
|
||||
proposalId: number,
|
||||
contributionId?: number,
|
||||
noRefund?: boolean,
|
||||
) {
|
||||
this.setState({ isFetchingContribution: true });
|
||||
try {
|
||||
const { amount, isAnonymous } = this.props;
|
||||
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);
|
||||
res = await postProposalContribution(
|
||||
proposalId,
|
||||
amount || '0',
|
||||
isAnonymous,
|
||||
noRefund,
|
||||
);
|
||||
}
|
||||
this.setState({ contribution: res.data });
|
||||
} catch (err) {
|
||||
|
@ -199,14 +255,49 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private confirmAnonymous = () => {
|
||||
this.setState({ hasConfirmedAnonymous: true }, () => {
|
||||
const { state, props } = this;
|
||||
this.setState({ hasConfirmedAnonymous: true });
|
||||
if (!state.contribution && !props.contribution && props.proposalId) {
|
||||
this.fetchAddresses(props.proposalId, props.contributionId);
|
||||
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);
|
||||
|
|
|
@ -92,7 +92,7 @@ class RefundAddress extends React.Component<Props, State> {
|
|||
try {
|
||||
const res = await updateUserSettings(userid, { refundAddress });
|
||||
message.success('Settings saved');
|
||||
this.setState({ refundAddress: res.data.refundAddress });
|
||||
this.setState({ refundAddress: res.data.refundAddress || '' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.error(err.message || err.toString(), 5);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { SocialMedia } from 'types';
|
||||
import { SocialMedia } from './social';
|
||||
import { EmailSubscriptions } from './email';
|
||||
|
||||
export interface User {
|
||||
userid: number;
|
||||
|
@ -9,3 +10,8 @@ export interface User {
|
|||
socialMedias: SocialMedia[];
|
||||
avatar: { imageUrl: string } | null;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
emailSubscriptions: EmailSubscriptions;
|
||||
refundAddress?: string | null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue