Merge pull request #261 from grant-project/anonymous-contributions
Anonymous contributions
This commit is contained in:
commit
db436f014b
|
@ -46,7 +46,7 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSendRefund = () => {
|
const renderSendRefund = () => {
|
||||||
if (c.staking || !c.refundAddress || c.refundTxId || !c.proposal.isFailed) {
|
if (c.staking || !c.refundAddress || c.refundTxId || !c.proposal.isFailed || !c.user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const percent = c.proposal.milestones.reduce((prev, m) => {
|
const percent = c.proposal.milestones.reduce((prev, m) => {
|
||||||
|
@ -98,9 +98,10 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
<pre>{JSON.stringify(c.addresses, null, 4)}</pre>
|
<pre>{JSON.stringify(c.addresses, null, 4)}</pre>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
|
||||||
<Collapse.Panel key="user" header="user">
|
|
||||||
<UserItem {...c.user} />
|
<Collapse.Panel key="user" header="user">
|
||||||
</Collapse.Panel>
|
{c.user ? <UserItem {...c.user} /> : <em>Anonymous contribution</em>}
|
||||||
|
</Collapse.Panel>
|
||||||
|
|
||||||
<Collapse.Panel key="proposal" header="proposal">
|
<Collapse.Panel key="proposal" header="proposal">
|
||||||
<ProposalItem {...c.proposal} />
|
<ProposalItem {...c.proposal} />
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ContributionForm extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
defaults = {
|
defaults = {
|
||||||
proposalId: contribution.proposal.proposalId,
|
proposalId: contribution.proposal.proposalId,
|
||||||
userId: contribution.user.userid,
|
userId: contribution.user ? contribution.user.userid : '',
|
||||||
amount: contribution.amount,
|
amount: contribution.amount,
|
||||||
txId: contribution.txId || '',
|
txId: contribution.txId || '',
|
||||||
};
|
};
|
||||||
|
@ -68,14 +68,11 @@ class ContributionForm extends React.Component<Props> {
|
||||||
<Form.Item label="User ID">
|
<Form.Item label="User ID">
|
||||||
{getFieldDecorator('userId', {
|
{getFieldDecorator('userId', {
|
||||||
initialValue: defaults.userId,
|
initialValue: defaults.userId,
|
||||||
rules: [
|
|
||||||
{ required: true, message: 'User ID is required' },
|
|
||||||
],
|
|
||||||
})(
|
})(
|
||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="userId"
|
name="userId"
|
||||||
placeholder="Must be an existing user id"
|
placeholder="Existing user id or blank for anonymous"
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -152,6 +149,10 @@ class ContributionForm extends React.Component<Props> {
|
||||||
};
|
};
|
||||||
let msg;
|
let msg;
|
||||||
if (id) {
|
if (id) {
|
||||||
|
// Explicitly make it zero of omitted to indicate to remove user
|
||||||
|
if (!args.userId) {
|
||||||
|
args.userId = 0;
|
||||||
|
}
|
||||||
await store.editContribution(id, args);
|
await store.editContribution(id, args);
|
||||||
msg = 'Successfully updated contribution';
|
msg = 'Successfully updated contribution';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default class ContributionItem extends React.PureComponent<Props> {
|
||||||
>
|
>
|
||||||
<Link to={`/contributions/${id}`}>
|
<Link to={`/contributions/${id}`}>
|
||||||
<h2>
|
<h2>
|
||||||
{user.displayName} <small>for</small> {proposal.title}
|
{user ? user.displayName : <em>Anonymous</em>} <small>for</small> {proposal.title}
|
||||||
<Tooltip title={status.hint}>
|
<Tooltip title={status.hint}>
|
||||||
<Tag color={status.tagColor}>{status.tagDisplay}</Tag>
|
<Tag color={status.tagColor}>{status.tagDisplay}</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -141,7 +141,7 @@ export interface Contribution {
|
||||||
txId: null | string;
|
txId: null | string;
|
||||||
amount: string;
|
amount: string;
|
||||||
dateCreated: number;
|
dateCreated: number;
|
||||||
user: User;
|
user: User | null;
|
||||||
proposal: Proposal;
|
proposal: Proposal;
|
||||||
addresses: {
|
addresses: {
|
||||||
transparent: string;
|
transparent: string;
|
||||||
|
|
|
@ -128,7 +128,8 @@ export const PROPOSAL_STAGES: Array<StatusSoT<PROPOSAL_STAGE>> = [
|
||||||
id: PROPOSAL_STAGE.CANCELED,
|
id: PROPOSAL_STAGE.CANCELED,
|
||||||
tagDisplay: 'Canceled',
|
tagDisplay: 'Canceled',
|
||||||
tagColor: '#eb4118',
|
tagColor: '#eb4118',
|
||||||
hint: 'Proposal was canceled by an admin and is currently refunding all contributors.',
|
hint:
|
||||||
|
'Proposal was canceled by an admin and is currently refunding all contributors.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -190,9 +191,10 @@ export const CONTRIBUTION_STATUSES: Array<StatusSoT<CONTRIBUTION_STATUS>> = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: CONTRIBUTION_STATUS.DELETED,
|
id: CONTRIBUTION_STATUS.DELETED,
|
||||||
tagDisplay: 'Closed',
|
tagDisplay: 'Deleted',
|
||||||
tagColor: '#eb4118',
|
tagColor: '#eb4118',
|
||||||
hint: 'User deleted the contribution before it was sent or confirmed',
|
hint:
|
||||||
|
'User deleted the contribution before it was sent, or after it didn’t confirm after 24 hours',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -80,9 +80,11 @@ example_email_args = {
|
||||||
'proposal': proposal,
|
'proposal': proposal,
|
||||||
'contribution': contribution,
|
'contribution': contribution,
|
||||||
'contributor': user,
|
'contributor': user,
|
||||||
|
# 'contributor': None,
|
||||||
'funded': '50',
|
'funded': '50',
|
||||||
'proposal_url': 'http://someproposal.com',
|
'proposal_url': 'http://someproposal.com',
|
||||||
'contributor_url': 'http://someuser.com',
|
'contributor_url': 'http://someuser.com',
|
||||||
|
# 'contributor_url': None,
|
||||||
},
|
},
|
||||||
'proposal_comment': {
|
'proposal_comment': {
|
||||||
'author': user,
|
'author': user,
|
||||||
|
|
|
@ -590,7 +590,7 @@ def get_contributions(page, filters, search, sort):
|
||||||
@blueprint.route('/contributions', methods=['POST'])
|
@blueprint.route('/contributions', methods=['POST'])
|
||||||
@endpoint.api(
|
@endpoint.api(
|
||||||
parameter('proposalId', type=int, required=True),
|
parameter('proposalId', type=int, required=True),
|
||||||
parameter('userId', type=int, required=True),
|
parameter('userId', type=int, required=False, default=None),
|
||||||
parameter('status', type=str, required=True),
|
parameter('status', type=str, required=True),
|
||||||
parameter('amount', type=str, required=True),
|
parameter('amount', type=str, required=True),
|
||||||
parameter('txId', type=str, required=False),
|
parameter('txId', type=str, required=False),
|
||||||
|
@ -653,12 +653,15 @@ def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_
|
||||||
if not proposal:
|
if not proposal:
|
||||||
return {"message": "No proposal matching that id"}, 400
|
return {"message": "No proposal matching that id"}, 400
|
||||||
contribution.proposal_id = proposal_id
|
contribution.proposal_id = proposal_id
|
||||||
# User ID (must belong to an existing user)
|
# User ID (must belong to an existing user or 0 to unset)
|
||||||
if user_id:
|
if user_id is not None:
|
||||||
user = User.query.filter(User.id == user_id).first()
|
if user_id == 0:
|
||||||
if not user:
|
contribution.user_id = None
|
||||||
return {"message": "No user matching that id"}, 400
|
else:
|
||||||
contribution.user_id = user_id
|
user = User.query.filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
return {"message": "No user matching that id"}, 400
|
||||||
|
contribution.user_id = user_id
|
||||||
# Status (must be in list of statuses)
|
# Status (must be in list of statuses)
|
||||||
if status:
|
if status:
|
||||||
if not ContributionStatus.includes(status):
|
if not ContributionStatus.includes(status):
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from grant.proposal.models import (
|
from grant.proposal.models import (
|
||||||
ProposalContribution,
|
ProposalContribution,
|
||||||
proposal_contributions_schema,
|
proposal_contributions_schema,
|
||||||
|
@ -9,6 +11,7 @@ from grant.utils.enums import ContributionStatus
|
||||||
def make_bootstrap_data():
|
def make_bootstrap_data():
|
||||||
pending_contributions = ProposalContribution.query \
|
pending_contributions = ProposalContribution.query \
|
||||||
.filter_by(status=ContributionStatus.PENDING) \
|
.filter_by(status=ContributionStatus.PENDING) \
|
||||||
|
.filter(ProposalContribution.date_created + timedelta(hours=24) > datetime.now()) \
|
||||||
.all()
|
.all()
|
||||||
latest_contribution = ProposalContribution.query \
|
latest_contribution = ProposalContribution.query \
|
||||||
.filter_by(status=ContributionStatus.CONFIRMED) \
|
.filter_by(status=ContributionStatus.CONFIRMED) \
|
||||||
|
|
|
@ -86,7 +86,7 @@ def proposal_contribution(email_args):
|
||||||
'subject': 'You just got a contribution!',
|
'subject': 'You just got a contribution!',
|
||||||
'title': 'You just got a contribution',
|
'title': 'You just got a contribution',
|
||||||
'preview': '{} just contributed {} to your proposal {}'.format(
|
'preview': '{} just contributed {} to your proposal {}'.format(
|
||||||
email_args['contributor'].display_name,
|
email_args['contributor'].display_name if email_args['contributor'] else 'An anonymous contributor',
|
||||||
email_args['contribution'].amount,
|
email_args['contribution'].amount,
|
||||||
email_args['proposal'].title,
|
email_args['proposal'].title,
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,6 +3,7 @@ from functools import reduce
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from marshmallow import post_dump
|
||||||
|
|
||||||
from grant.comment.models import Comment
|
from grant.comment.models import Comment
|
||||||
from grant.email.send import send_email
|
from grant.email.send import send_email
|
||||||
|
@ -19,6 +20,7 @@ from grant.utils.enums import (
|
||||||
ProposalArbiterStatus,
|
ProposalArbiterStatus,
|
||||||
MilestoneStage
|
MilestoneStage
|
||||||
)
|
)
|
||||||
|
from grant.utils.stubs import anonymous_user
|
||||||
|
|
||||||
proposal_team = db.Table(
|
proposal_team = db.Table(
|
||||||
'proposal_team', db.Model.metadata,
|
'proposal_team', db.Model.metadata,
|
||||||
|
@ -87,13 +89,13 @@ class ProposalContribution(db.Model):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
proposal_id: int,
|
proposal_id: int,
|
||||||
user_id: int,
|
|
||||||
amount: str,
|
amount: str,
|
||||||
|
user_id: int = None,
|
||||||
staking: bool = False,
|
staking: bool = False,
|
||||||
):
|
):
|
||||||
self.proposal_id = proposal_id
|
self.proposal_id = proposal_id
|
||||||
self.user_id = user_id
|
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
|
self.user_id = user_id
|
||||||
self.staking = staking
|
self.staking = staking
|
||||||
self.date_created = datetime.datetime.now()
|
self.date_created = datetime.datetime.now()
|
||||||
self.status = ContributionStatus.PENDING
|
self.status = ContributionStatus.PENDING
|
||||||
|
@ -163,7 +165,7 @@ class ProposalContribution(db.Model):
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def refund_address(self):
|
def refund_address(self):
|
||||||
return self.user.settings.refund_address
|
return self.user.settings.refund_address if self.user else None
|
||||||
|
|
||||||
|
|
||||||
class ProposalArbiter(db.Model):
|
class ProposalArbiter(db.Model):
|
||||||
|
@ -337,11 +339,11 @@ class Proposal(db.Model):
|
||||||
self.deadline_duration = deadline_duration
|
self.deadline_duration = deadline_duration
|
||||||
Proposal.validate(vars(self))
|
Proposal.validate(vars(self))
|
||||||
|
|
||||||
def create_contribution(self, user_id: int, amount, staking: bool = False):
|
def create_contribution(self, amount, user_id: int = None, staking: bool = False):
|
||||||
contribution = ProposalContribution(
|
contribution = ProposalContribution(
|
||||||
proposal_id=self.id,
|
proposal_id=self.id,
|
||||||
user_id=user_id,
|
|
||||||
amount=amount,
|
amount=amount,
|
||||||
|
user_id=user_id,
|
||||||
staking=staking,
|
staking=staking,
|
||||||
)
|
)
|
||||||
db.session.add(contribution)
|
db.session.add(contribution)
|
||||||
|
@ -713,12 +715,14 @@ class ProposalContributionSchema(ma.Schema):
|
||||||
"amount",
|
"amount",
|
||||||
"date_created",
|
"date_created",
|
||||||
"addresses",
|
"addresses",
|
||||||
|
"is_anonymous",
|
||||||
)
|
)
|
||||||
|
|
||||||
proposal = ma.Nested("ProposalSchema")
|
proposal = ma.Nested("ProposalSchema")
|
||||||
user = ma.Nested("UserSchema")
|
user = ma.Nested("UserSchema", default=anonymous_user)
|
||||||
date_created = ma.Method("get_date_created")
|
date_created = ma.Method("get_date_created")
|
||||||
addresses = ma.Method("get_addresses")
|
addresses = ma.Method("get_addresses")
|
||||||
|
is_anonymous = ma.Method("get_is_anonymous")
|
||||||
|
|
||||||
def get_date_created(self, obj):
|
def get_date_created(self, obj):
|
||||||
return dt_to_unix(obj.date_created)
|
return dt_to_unix(obj.date_created)
|
||||||
|
@ -731,6 +735,15 @@ class ProposalContributionSchema(ma.Schema):
|
||||||
'transparent': addresses['transparent'],
|
'transparent': addresses['transparent'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_is_anonymous(self, obj):
|
||||||
|
return not obj.user_id
|
||||||
|
|
||||||
|
@post_dump
|
||||||
|
def stub_anonymous_user(self, data):
|
||||||
|
if 'user' in data and data['user'] is None:
|
||||||
|
data['user'] = anonymous_user
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
proposal_contribution_schema = ProposalContributionSchema()
|
proposal_contribution_schema = ProposalContributionSchema()
|
||||||
proposal_contributions_schema = ProposalContributionSchema(many=True)
|
proposal_contributions_schema = ProposalContributionSchema(many=True)
|
||||||
|
|
|
@ -375,11 +375,12 @@ def post_proposal_update(proposal_id, title, content):
|
||||||
# Send email to all contributors (even if contribution failed)
|
# Send email to all contributors (even if contribution failed)
|
||||||
contributions = ProposalContribution.query.filter_by(proposal_id=proposal_id).all()
|
contributions = ProposalContribution.query.filter_by(proposal_id=proposal_id).all()
|
||||||
for c in contributions:
|
for c in contributions:
|
||||||
send_email(c.user.email_address, 'contribution_update', {
|
if c.user:
|
||||||
'proposal': g.current_proposal,
|
send_email(c.user.email_address, 'contribution_update', {
|
||||||
'proposal_update': update,
|
'proposal': g.current_proposal,
|
||||||
'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'),
|
'proposal_update': update,
|
||||||
})
|
'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'),
|
||||||
|
})
|
||||||
|
|
||||||
dumped_update = proposal_update_schema.dump(update)
|
dumped_update = proposal_update_schema.dump(update)
|
||||||
return dumped_update, 201
|
return dumped_update, 201
|
||||||
|
@ -481,22 +482,31 @@ def get_proposal_contribution(proposal_id, contribution_id):
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
||||||
@requires_auth
|
|
||||||
@endpoint.api(
|
@endpoint.api(
|
||||||
parameter('amount', type=str, required=True)
|
parameter('amount', type=str, required=True),
|
||||||
|
parameter('anonymous', type=bool, required=False)
|
||||||
)
|
)
|
||||||
def post_proposal_contribution(proposal_id, amount):
|
def post_proposal_contribution(proposal_id, amount, anonymous):
|
||||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||||
if not proposal:
|
if not proposal:
|
||||||
return {"message": "No proposal matching id"}, 404
|
return {"message": "No proposal matching id"}, 404
|
||||||
|
|
||||||
code = 200
|
code = 200
|
||||||
contribution = ProposalContribution \
|
user = None
|
||||||
.get_existing_contribution(g.current_user.id, proposal_id, amount)
|
contribution = None
|
||||||
|
|
||||||
|
if not anonymous:
|
||||||
|
user = get_authed_user()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
contribution = ProposalContribution.get_existing_contribution(user.id, proposal_id, amount)
|
||||||
|
|
||||||
if not contribution:
|
if not contribution:
|
||||||
code = 201
|
code = 201
|
||||||
contribution = proposal.create_contribution(g.current_user.id, amount)
|
contribution = proposal.create_contribution(
|
||||||
|
amount=amount,
|
||||||
|
user_id=user.id if user else None,
|
||||||
|
)
|
||||||
|
|
||||||
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
||||||
return dumped_contribution, code
|
return dumped_contribution, code
|
||||||
|
@ -544,11 +554,12 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Send to the user
|
# Send to the user
|
||||||
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
if contribution.user:
|
||||||
'contribution': contribution,
|
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
||||||
'proposal': contribution.proposal,
|
'contribution': contribution,
|
||||||
'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}',
|
'proposal': contribution.proposal,
|
||||||
})
|
'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}',
|
||||||
|
})
|
||||||
|
|
||||||
# Send to the full proposal gang
|
# Send to the full proposal gang
|
||||||
for member in contribution.proposal.team:
|
for member in contribution.proposal.team:
|
||||||
|
@ -558,7 +569,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
'contributor': contribution.user,
|
'contributor': contribution.user,
|
||||||
'funded': contribution.proposal.funded,
|
'funded': contribution.proposal.funded,
|
||||||
'proposal_url': make_url(f'/proposals/{contribution.proposal.id}'),
|
'proposal_url': make_url(f'/proposals/{contribution.proposal.id}'),
|
||||||
'contributor_url': make_url(f'/profile/{contribution.user.id}'),
|
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
|
||||||
})
|
})
|
||||||
|
|
||||||
# TODO: Once we have a task queuer in place, queue emails to everyone
|
# TODO: Once we have a task queuer in place, queue emails to everyone
|
||||||
|
|
|
@ -78,12 +78,13 @@ class ProposalDeadline:
|
||||||
'proposal': proposal,
|
'proposal': proposal,
|
||||||
})
|
})
|
||||||
for c in proposal.contributions:
|
for c in proposal.contributions:
|
||||||
send_email(c.user.email_address, 'contribution_proposal_failed', {
|
if c.user:
|
||||||
'contribution': c,
|
send_email(c.user.email_address, 'contribution_proposal_failed', {
|
||||||
'proposal': proposal,
|
'contribution': c,
|
||||||
'refund_address': c.user.settings.refund_address,
|
'proposal': proposal,
|
||||||
'account_settings_url': make_url('/profile/settings?tab=account')
|
'refund_address': c.user.settings.refund_address,
|
||||||
})
|
'account_settings_url': make_url('/profile/settings?tab=account')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
JOBS = {
|
JOBS = {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<p style="margin: 0 0 20px;">
|
<p style="margin: 0 0 20px;">
|
||||||
Your proposal <strong>{{ args.proposal.title }}</strong> just got a
|
Your proposal <strong>{{ args.proposal.title }}</strong> just got a
|
||||||
<strong>{{ args.contribution.amount }} ZEC</strong> contribution from
|
<strong>{{ args.contribution.amount }} ZEC</strong> contribution from
|
||||||
|
{% if args.contributor %}
|
||||||
<a href="{{ args.contributor_url }}" target="_blank">{{ args.contributor.display_name }}</a>.
|
<a href="{{ args.contributor_url }}" target="_blank">{{ args.contributor.display_name }}</a>.
|
||||||
|
{% else %}
|
||||||
|
an anonymous contributor.
|
||||||
|
{% endif %}
|
||||||
Your proposal is now at
|
Your proposal is now at
|
||||||
<strong>{{ args.funded }} / {{ args.proposal.target }} ZEC</strong>.
|
<strong>{{ args.funded }} / {{ args.proposal.target }} ZEC</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
Your proposal "{{ args.proposal.title }}" just got a
|
Your proposal "{{ args.proposal.title }}" just got a
|
||||||
{{ args.contribution.amount }} ZEC contribution from
|
{{ args.contribution.amount }} ZEC contribution from {{ args.contributor.display_name if args.contributor else 'an anonymous contributor' }}.
|
||||||
{{ args.contributor.display_name }}. Your proposal is now at {{ args.funded }} / {{ args.proposal.target }} ZEC.
|
Your proposal is now at {{ args.funded }} / {{ args.proposal.target }} ZEC.
|
||||||
|
|
||||||
|
{% if args.contributor %}
|
||||||
See {{ args.contributor.display_name }}'s profile: {{ args.contributor_url }}
|
See {{ args.contributor.display_name }}'s profile: {{ args.contributor_url }}
|
||||||
|
{% endif %}
|
||||||
View your proposal: {{ args.proposal_url }}
|
View your proposal: {{ args.proposal_url }}
|
|
@ -0,0 +1,9 @@
|
||||||
|
# This should match UserSchema in grant.user.models
|
||||||
|
anonymous_user = {
|
||||||
|
'userid': 0,
|
||||||
|
'display_name': 'Anonymous',
|
||||||
|
'title': 'N/A',
|
||||||
|
'avatar': None,
|
||||||
|
'social_medias': [],
|
||||||
|
'email_verified': True,
|
||||||
|
}
|
|
@ -30,7 +30,7 @@
|
||||||
"typescript": "^3.2.1"
|
"typescript": "^3.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "4.5.2",
|
"@sentry/node": "4.4.2",
|
||||||
"@types/cors": "2.8.4",
|
"@types/cors": "2.8.4",
|
||||||
"@types/dotenv": "^6.1.0",
|
"@types/dotenv": "^6.1.0",
|
||||||
"@types/ws": "^6.0.1",
|
"@types/ws": "^6.0.1",
|
||||||
|
|
|
@ -63,7 +63,6 @@ async function scanBlock(height: number) {
|
||||||
consecutiveBlockFailures++;
|
consecutiveBlockFailures++;
|
||||||
// If we fail a certain number of times, it's reasonable to
|
// If we fail a certain number of times, it's reasonable to
|
||||||
// assume that the blockchain is down, and we should just quit.
|
// assume that the blockchain is down, and we should just quit.
|
||||||
// TODO: Scream at sentry or something!
|
|
||||||
if (consecutiveBlockFailures >= MAXIMUM_BLOCK_FAILURES) {
|
if (consecutiveBlockFailures >= MAXIMUM_BLOCK_FAILURES) {
|
||||||
captureException(err);
|
captureException(err);
|
||||||
log.error('Maximum consecutive failures reached, exiting!');
|
log.error('Maximum consecutive failures reached, exiting!');
|
||||||
|
|
|
@ -2,57 +2,62 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@sentry/core@4.5.2":
|
"@sentry/core@4.4.2":
|
||||||
version "4.5.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.5.2.tgz#b103b659857e0fabf7219e9f34c4070bcb2af0c2"
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.4.2.tgz#562526bc634c087f04bbca68b09cedc4b41cc64d"
|
||||||
|
integrity sha512-hJyAodTCf4sZfVdf41Rtuzj4EsyzYq5rdMZ+zc2Vinwdf8D0/brHe91fHeO0CKXEb2P0wJsrjwMidG/ccq/M8A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "4.5.2"
|
"@sentry/hub" "4.4.2"
|
||||||
"@sentry/minimal" "4.5.2"
|
"@sentry/minimal" "4.4.2"
|
||||||
"@sentry/types" "4.5.0"
|
"@sentry/types" "4.4.2"
|
||||||
"@sentry/utils" "4.5.2"
|
"@sentry/utils" "4.4.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/hub@4.5.2":
|
"@sentry/hub@4.4.2":
|
||||||
version "4.5.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.5.2.tgz#b1df97dccc8644f6ef9b36e8747fe04342cb92ef"
|
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.4.2.tgz#1399556fda06fb83c4f186c4aa842725f520159c"
|
||||||
|
integrity sha512-oe9ytXkTWyD+QmOpVzHAqTbRV4Hc0ee2Nt6HvrDtRmlXzQxfvTWG2F8KYT6w8kzqg5klnuRpnsmgTTV3KuNBVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "4.5.0"
|
"@sentry/types" "4.4.2"
|
||||||
"@sentry/utils" "4.5.2"
|
"@sentry/utils" "4.4.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/minimal@4.5.2":
|
"@sentry/minimal@4.4.2":
|
||||||
version "4.5.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.5.2.tgz#c090427f6ac276b4dc449e8be460ca5b35ce4fb3"
|
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.4.2.tgz#13fffc6b17a2401b6a79947838a637626ab80b10"
|
||||||
|
integrity sha512-GEZZiNvVgqFAESZhAe3vjwTInn13lI2bSI3ItQN4RUWKL/W4n/fwVoDJbkb1U8aWxanuMnRDEpKwyQv6zYTZfw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "4.5.2"
|
"@sentry/hub" "4.4.2"
|
||||||
"@sentry/types" "4.5.0"
|
"@sentry/types" "4.4.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/node@4.5.2":
|
"@sentry/node@4.4.2":
|
||||||
version "4.5.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.5.2.tgz#b90747749cf919006ca44fd873da87fd3608f8e0"
|
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.4.2.tgz#549921d2df3cbf58ebcfb525c3005c3fec4739a3"
|
||||||
|
integrity sha512-8/KlSdfVhledZ6PS6muxZY5r2pqhw8MNSXP7AODR2qRrHwsbnirVgV21WIAYAjKXEfYQGbm69lyoaTJGazlQ3Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "4.5.2"
|
"@sentry/core" "4.4.2"
|
||||||
"@sentry/hub" "4.5.2"
|
"@sentry/hub" "4.4.2"
|
||||||
"@sentry/types" "4.5.0"
|
"@sentry/types" "4.4.2"
|
||||||
"@sentry/utils" "4.5.2"
|
"@sentry/utils" "4.4.2"
|
||||||
"@types/stack-trace" "0.0.29"
|
"@types/stack-trace" "0.0.29"
|
||||||
cookie "0.3.1"
|
cookie "0.3.1"
|
||||||
https-proxy-agent "2.2.1"
|
https-proxy-agent "^2.2.1"
|
||||||
lru_map "0.3.3"
|
|
||||||
lsmod "1.0.0"
|
lsmod "1.0.0"
|
||||||
stack-trace "0.0.10"
|
stack-trace "0.0.10"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/types@4.5.0":
|
"@sentry/types@4.4.2":
|
||||||
version "4.5.0"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.5.0.tgz#59e2a27d48b01b44e8959aa5c8a30514fe1086a9"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.4.2.tgz#f38dd3bc671cd2f5983a85553aebeac9c2286b17"
|
||||||
|
integrity sha512-QyQd6PKKIyjJgaq/RQjsxPJEWbXcuiWZ9RvSnhBjS5jj53HEzkM1qkbAFqlYHJ1DTJJ1EuOM4+aTmGzHe93zuA==
|
||||||
|
|
||||||
"@sentry/utils@4.5.2":
|
"@sentry/utils@4.4.2":
|
||||||
version "4.5.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.5.2.tgz#0de5bef1c71e0dd800d378c010e5bfad91add91a"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.4.2.tgz#e05a47e135ecef29e63a996f59aee8c8f792c222"
|
||||||
|
integrity sha512-j/Ad8G1abHlJdD2q7aWWbSOSeWB5M5v1R1VKL8YPlwEbSvvmEQWePhBKFI0qlnKd2ObdUQsj86pHEXJRSFNfCw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "4.5.0"
|
"@sentry/types" "4.4.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@types/body-parser@*", "@types/body-parser@1.17.0":
|
"@types/body-parser@*", "@types/body-parser@1.17.0":
|
||||||
|
@ -1000,9 +1005,10 @@ http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
|
||||||
setprototypeof "1.1.0"
|
setprototypeof "1.1.0"
|
||||||
statuses ">= 1.4.0 < 2"
|
statuses ">= 1.4.0 < 2"
|
||||||
|
|
||||||
https-proxy-agent@2.2.1:
|
https-proxy-agent@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
|
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
|
||||||
|
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base "^4.1.0"
|
agent-base "^4.1.0"
|
||||||
debug "^3.1.0"
|
debug "^3.1.0"
|
||||||
|
@ -1314,10 +1320,6 @@ lru-cache@^4.0.1:
|
||||||
pseudomap "^1.0.2"
|
pseudomap "^1.0.2"
|
||||||
yallist "^2.1.2"
|
yallist "^2.1.2"
|
||||||
|
|
||||||
lru_map@0.3.3:
|
|
||||||
version "0.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
|
|
||||||
|
|
||||||
lsmod@1.0.0:
|
lsmod@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b"
|
resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b"
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
TeamInvite,
|
TeamInvite,
|
||||||
TeamInviteWithProposal,
|
TeamInviteWithProposal,
|
||||||
SOCIAL_SERVICE,
|
SOCIAL_SERVICE,
|
||||||
ContributionWithAddresses,
|
ContributionWithAddressesAndUser,
|
||||||
EmailSubscriptions,
|
EmailSubscriptions,
|
||||||
RFP,
|
RFP,
|
||||||
ProposalPageParams,
|
ProposalPageParams,
|
||||||
|
@ -309,9 +309,11 @@ export function putInviteResponse(
|
||||||
export function postProposalContribution(
|
export function postProposalContribution(
|
||||||
proposalId: number,
|
proposalId: number,
|
||||||
amount: string,
|
amount: string,
|
||||||
): Promise<{ data: ContributionWithAddresses }> {
|
anonymous?: boolean,
|
||||||
|
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||||
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
||||||
amount,
|
amount,
|
||||||
|
anonymous,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,13 +333,13 @@ export function deleteProposalContribution(contributionId: string | number) {
|
||||||
export function getProposalContribution(
|
export function getProposalContribution(
|
||||||
proposalId: number,
|
proposalId: number,
|
||||||
contributionId: number,
|
contributionId: number,
|
||||||
): Promise<{ data: ContributionWithAddresses }> {
|
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||||
return axios.get(`/api/v1/proposals/${proposalId}/contributions/${contributionId}`);
|
return axios.get(`/api/v1/proposals/${proposalId}/contributions/${contributionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProposalStakingContribution(
|
export function getProposalStakingContribution(
|
||||||
proposalId: number,
|
proposalId: number,
|
||||||
): Promise<{ data: ContributionWithAddresses }> {
|
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||||
return axios.get(`/api/v1/proposals/${proposalId}/stake`);
|
return axios.get(`/api/v1/proposals/${proposalId}/stake`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
@import '~styles/variables.less';
|
@import '~styles/variables.less';
|
||||||
|
|
||||||
.PaymentInfo {
|
.PaymentInfo {
|
||||||
|
&-anonymous {
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
margin-bottom: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&-text {
|
&-text {
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
|
|
@ -26,8 +26,9 @@ export default class PaymentInfo extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { contribution, text } = this.props;
|
const { contribution } = this.props;
|
||||||
const { sendType } = this.state;
|
const { sendType } = this.state;
|
||||||
|
let text = this.props.text;
|
||||||
let address;
|
let address;
|
||||||
let memo;
|
let memo;
|
||||||
let amount;
|
let amount;
|
||||||
|
@ -103,14 +104,24 @@ export default class PaymentInfo extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
if (contribution && contribution.isAnonymous) {
|
||||||
|
text = `
|
||||||
|
Thank you for contributing! Just send using whichever method works best for
|
||||||
|
you, and your contribution will show up anonymously once it's been confirmed.
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
text = `
|
||||||
|
Thank you for contributing! Just send using whichever method works best for
|
||||||
|
you, and we'll let you know once it's been confirmed.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="PaymentInfo" layout="vertical">
|
<Form className="PaymentInfo" layout="vertical">
|
||||||
<div className="PaymentInfo-text">
|
<div className="PaymentInfo-text">
|
||||||
{text ||
|
{text}
|
||||||
`
|
|
||||||
Thank you for contributing! Just send using whichever method works best for
|
|
||||||
you, and we'll let you know when your contribution has been confirmed.
|
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
className="PaymentInfo-types"
|
className="PaymentInfo-types"
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Modal } from 'antd';
|
import { Modal, Alert } from 'antd';
|
||||||
import Result from 'ant-design-pro/lib/Result';
|
import Result from 'ant-design-pro/lib/Result';
|
||||||
import { postProposalContribution, getProposalContribution } from 'api/api';
|
import { postProposalContribution, getProposalContribution } from 'api/api';
|
||||||
import { ContributionWithAddresses } from 'types';
|
import { ContributionWithAddressesAndUser } from 'types';
|
||||||
import PaymentInfo from './PaymentInfo';
|
import PaymentInfo from './PaymentInfo';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
contribution?: ContributionWithAddresses | Falsy;
|
contribution?: ContributionWithAddressesAndUser | Falsy;
|
||||||
proposalId?: number;
|
proposalId?: number;
|
||||||
contributionId?: number;
|
contributionId?: number;
|
||||||
amount?: string;
|
amount?: string;
|
||||||
|
isAnonymous?: boolean;
|
||||||
hasNoButtons?: boolean;
|
hasNoButtons?: boolean;
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
handleClose(): void;
|
handleClose(): void;
|
||||||
|
@ -20,15 +21,19 @@ interface OwnProps {
|
||||||
type Props = OwnProps;
|
type Props = OwnProps;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
hasConfirmedAnonymous: boolean;
|
||||||
hasSent: boolean;
|
hasSent: boolean;
|
||||||
contribution: ContributionWithAddresses | null;
|
contribution: ContributionWithAddressesAndUser | null;
|
||||||
|
isFetchingContribution: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ContributionModal extends React.Component<Props, State> {
|
export default class ContributionModal extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
|
hasConfirmedAnonymous: false,
|
||||||
hasSent: false,
|
hasSent: false,
|
||||||
contribution: null,
|
contribution: null,
|
||||||
|
isFetchingContribution: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,34 +48,83 @@ export default class ContributionModal extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps: Props) {
|
componentWillUpdate(nextProps: Props) {
|
||||||
const { isVisible, proposalId, contributionId, contribution } = nextProps;
|
const {
|
||||||
|
isVisible,
|
||||||
|
isAnonymous,
|
||||||
|
proposalId,
|
||||||
|
contributionId,
|
||||||
|
contribution,
|
||||||
|
} = nextProps;
|
||||||
// When modal is opened and proposalId is provided or changed
|
// When modal is opened and proposalId is provided or changed
|
||||||
if (isVisible && proposalId) {
|
// But not if we're anonymous, that will happen in confirmAnonymous
|
||||||
|
if (isVisible && proposalId && !isAnonymous) {
|
||||||
if (this.props.isVisible !== isVisible || proposalId !== this.props.proposalId) {
|
if (this.props.isVisible !== isVisible || proposalId !== this.props.proposalId) {
|
||||||
this.fetchAddresses(proposalId, contributionId);
|
this.fetchAddresses(proposalId, contributionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If contribution is provided
|
// If contribution is provided, update it
|
||||||
if (contribution !== this.props.contribution) {
|
if (contribution !== this.props.contribution) {
|
||||||
this.setState({ contribution: contribution || null });
|
this.setState({ contribution: contribution || null });
|
||||||
}
|
}
|
||||||
|
// When the modal is closed, clear out the contribution and anonymous check
|
||||||
|
if (this.props.isVisible && !isVisible) {
|
||||||
|
this.setState({
|
||||||
|
contribution: null,
|
||||||
|
hasConfirmedAnonymous: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isVisible, handleClose, hasNoButtons, text } = this.props;
|
const { isVisible, isAnonymous, handleClose, hasNoButtons, text } = this.props;
|
||||||
const { hasSent, contribution, error } = this.state;
|
const { hasSent, hasConfirmedAnonymous, contribution, error } = this.state;
|
||||||
|
let okText;
|
||||||
|
let onOk;
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (hasSent) {
|
if (isAnonymous && !hasConfirmedAnonymous) {
|
||||||
|
okText = 'I accept';
|
||||||
|
onOk = this.confirmAnonymous;
|
||||||
|
content = (
|
||||||
|
<Alert
|
||||||
|
className="PaymentInfo-anonymous"
|
||||||
|
type="warning"
|
||||||
|
message="This contribution is anonymous"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
You are about to contribute anonymously. Your contribution will show up
|
||||||
|
without attribution, and even if you're logged in, will not
|
||||||
|
appear anywhere on your account after you close this modal.
|
||||||
|
<br /> <br />
|
||||||
|
In the case of a refund, your contribution will be treated as a donation to
|
||||||
|
the Zcash Foundation instead.
|
||||||
|
<br /> <br />
|
||||||
|
If you would like to have your contribution attached to an account, you
|
||||||
|
can close this modal, make sure you're logged in, and don't check the
|
||||||
|
"Contribute anonymously" checkbox.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hasSent) {
|
||||||
|
okText = 'Done';
|
||||||
|
onOk = handleClose;
|
||||||
content = (
|
content = (
|
||||||
<Result
|
<Result
|
||||||
type="success"
|
type="success"
|
||||||
title="Thank you for your contribution!"
|
title="Thank you for your contribution!"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
Your contribution should be confirmed in about 20 minutes. You can keep an
|
Your transaction should be confirmed in about 20 minutes.{' '}
|
||||||
eye on it at the{' '}
|
{isAnonymous
|
||||||
<Link to="/profile?tab=funded">funded tab on your profile</Link>.
|
? '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>.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
style={{ width: '90%' }}
|
style={{ width: '90%' }}
|
||||||
|
@ -78,8 +132,12 @@ export default class ContributionModal extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
okText = 'Done';
|
||||||
|
onOk = handleClose;
|
||||||
content = error;
|
content = error;
|
||||||
} else {
|
} else {
|
||||||
|
okText = 'I’ve sent it';
|
||||||
|
onOk = this.confirmSend;
|
||||||
content = <PaymentInfo contribution={contribution} text={text} />;
|
content = <PaymentInfo contribution={contribution} text={text} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,8 +148,8 @@ export default class ContributionModal extends React.Component<Props, State> {
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
closable={hasSent || hasNoButtons}
|
closable={hasSent || hasNoButtons}
|
||||||
maskClosable={hasSent || hasNoButtons}
|
maskClosable={hasSent || hasNoButtons}
|
||||||
okText={hasSent ? 'Done' : 'I’ve sent it'}
|
okText={okText}
|
||||||
onOk={hasSent ? handleClose : this.confirmSend}
|
onOk={onOk}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
footer={hasNoButtons ? '' : undefined}
|
footer={hasNoButtons ? '' : undefined}
|
||||||
centered
|
centered
|
||||||
|
@ -102,21 +160,31 @@ export default class ContributionModal extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchAddresses(proposalId: number, contributionId?: number) {
|
private async fetchAddresses(proposalId: number, contributionId?: number) {
|
||||||
|
this.setState({ isFetchingContribution: true });
|
||||||
try {
|
try {
|
||||||
|
const { amount, isAnonymous } = this.props;
|
||||||
let res;
|
let res;
|
||||||
if (contributionId) {
|
if (contributionId) {
|
||||||
res = await getProposalContribution(proposalId, contributionId);
|
res = await getProposalContribution(proposalId, contributionId);
|
||||||
} else {
|
} else {
|
||||||
res = await postProposalContribution(proposalId, this.props.amount || '0');
|
res = await postProposalContribution(proposalId, amount || '0', isAnonymous);
|
||||||
}
|
}
|
||||||
this.setState({ contribution: res.data });
|
this.setState({ contribution: res.data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: err.message });
|
this.setState({ error: err.message });
|
||||||
}
|
}
|
||||||
|
this.setState({ isFetchingContribution: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private confirmAnonymous = () => {
|
||||||
|
const { state, props } = this;
|
||||||
|
this.setState({ hasConfirmedAnonymous: true });
|
||||||
|
if (!state.contribution && !props.contribution && props.proposalId) {
|
||||||
|
this.fetchAddresses(props.proposalId, props.contributionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private confirmSend = () => {
|
private confirmSend = () => {
|
||||||
// TODO: Mark on backend
|
|
||||||
this.setState({ hasSent: true });
|
this.setState({ hasSent: true });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, Popconfirm, message, Tag } from 'antd';
|
import { Button, Popconfirm, message, Tag } from 'antd';
|
||||||
import { UserProposal, STATUS, ContributionWithAddresses } from 'types';
|
import { UserProposal, STATUS, ContributionWithAddressesAndUser } from 'types';
|
||||||
import ContributionModal from 'components/ContributionModal';
|
import ContributionModal from 'components/ContributionModal';
|
||||||
import { getProposalStakingContribution } from 'api/api';
|
import { getProposalStakingContribution } from 'api/api';
|
||||||
import { deletePendingProposal, publishPendingProposal } from 'modules/users/actions';
|
import { deletePendingProposal, publishPendingProposal } from 'modules/users/actions';
|
||||||
|
@ -29,7 +29,7 @@ interface State {
|
||||||
isDeleting: boolean;
|
isDeleting: boolean;
|
||||||
isPublishing: boolean;
|
isPublishing: boolean;
|
||||||
isLoadingStake: boolean;
|
isLoadingStake: boolean;
|
||||||
stakeContribution: ContributionWithAddresses | null;
|
stakeContribution: ContributionWithAddressesAndUser | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfilePending extends React.Component<Props, State> {
|
class ProfilePending extends React.Component<Props, State> {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Form, Input, Button, Icon, Popover } from 'antd';
|
import { Form, Input, Checkbox, Button, Icon, Popover, Tooltip } from 'antd';
|
||||||
|
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||||
import { Proposal, STATUS } from 'types';
|
import { Proposal, STATUS } from 'types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { fromZat } from 'utils/units';
|
import { fromZat } from 'utils/units';
|
||||||
|
@ -21,7 +22,7 @@ interface OwnProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
sendLoading: boolean;
|
authUser: AppState['auth']['user'];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = OwnProps & StateProps;
|
type Props = OwnProps & StateProps;
|
||||||
|
@ -29,6 +30,7 @@ type Props = OwnProps & StateProps;
|
||||||
interface State {
|
interface State {
|
||||||
amountToRaise: string;
|
amountToRaise: string;
|
||||||
amountError: string | null;
|
amountError: string | null;
|
||||||
|
isAnonymous: boolean;
|
||||||
isContributing: boolean;
|
isContributing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,13 +40,14 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
amountToRaise: '',
|
amountToRaise: '',
|
||||||
amountError: null,
|
amountError: null,
|
||||||
|
isAnonymous: false,
|
||||||
isContributing: false,
|
isContributing: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { proposal, sendLoading, isPreview } = this.props;
|
const { proposal, isPreview, authUser } = this.props;
|
||||||
const { amountToRaise, amountError, isContributing } = this.state;
|
const { amountToRaise, amountError, isAnonymous, isContributing } = this.state;
|
||||||
const amountFloat = parseFloat(amountToRaise) || 0;
|
const amountFloat = parseFloat(amountToRaise) || 0;
|
||||||
let content;
|
let content;
|
||||||
if (proposal) {
|
if (proposal) {
|
||||||
|
@ -165,7 +168,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form layout="vertical">
|
<Form layout="vertical" className="ProposalCampaignBlock-contribute">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
validateStatus={amountError ? 'error' : undefined}
|
validateStatus={amountError ? 'error' : undefined}
|
||||||
help={amountError}
|
help={amountError}
|
||||||
|
@ -185,12 +188,20 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
disabled={isPreview}
|
disabled={isPreview}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{amountToRaise &&
|
||||||
|
!!authUser && (
|
||||||
|
<Checkbox checked={isAnonymous} onChange={this.handleChangeAnonymity}>
|
||||||
|
Contribute anonymously
|
||||||
|
<Tooltip title="Contribute with no attribution to your account. This will make you ineligible for refunds.">
|
||||||
|
<Icon type="question-circle" />
|
||||||
|
</Tooltip>
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={this.openContributionModal}
|
onClick={this.openContributionModal}
|
||||||
size="large"
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
loading={sendLoading}
|
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
Fund this project
|
Fund this project
|
||||||
|
@ -203,6 +214,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
isVisible={isContributing}
|
isVisible={isContributing}
|
||||||
proposalId={proposal.proposalId}
|
proposalId={proposal.proposalId}
|
||||||
amount={amountToRaise}
|
amount={amountToRaise}
|
||||||
|
isAnonymous={isAnonymous || !authUser}
|
||||||
handleClose={this.closeContributionModal}
|
handleClose={this.closeContributionModal}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -244,14 +256,17 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
this.setState({ amountToRaise: value, amountError });
|
this.setState({ amountToRaise: value, amountError });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleChangeAnonymity = (ev: CheckboxChangeEvent) => {
|
||||||
|
this.setState({ isAnonymous: ev.target.checked });
|
||||||
|
};
|
||||||
|
|
||||||
private openContributionModal = () => this.setState({ isContributing: true });
|
private openContributionModal = () => this.setState({ isContributing: true });
|
||||||
private closeContributionModal = () => this.setState({ isContributing: false });
|
private closeContributionModal = () => this.setState({ isContributing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
console.warn('TODO - new redux flag for sendLoading?', state);
|
|
||||||
return {
|
return {
|
||||||
sendLoading: false,
|
authUser: state.auth.user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-contribute {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-checkbox-wrapper {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
color: @primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-fundingOver {
|
&-fundingOver {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -9,8 +9,16 @@ interface Props {
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Wrap = ({ user, children }: { user: User, children: React.ReactNode }) => {
|
||||||
|
if (user.userid) {
|
||||||
|
return <Link to={`/profile/${user.userid}`} className="UserRow" children={children} />;
|
||||||
|
} else {
|
||||||
|
return <div className="UserRow" children={children} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const UserRow = ({ user, extra }: Props) => (
|
const UserRow = ({ user, extra }: Props) => (
|
||||||
<Link to={`/profile/${user.userid}`} className="UserRow">
|
<Wrap user={user}>
|
||||||
<div className="UserRow-avatar">
|
<div className="UserRow-avatar">
|
||||||
<UserAvatar user={user} className="UserRow-avatar-img" />
|
<UserAvatar user={user} className="UserRow-avatar-img" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +31,7 @@ const UserRow = ({ user, extra }: Props) => (
|
||||||
{extra}
|
{extra}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Wrap>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default UserRow;
|
export default UserRow;
|
||||||
|
|
|
@ -8,6 +8,13 @@ import 'components/Proposal/style.less';
|
||||||
import 'components/Proposal/Governance/style.less';
|
import 'components/Proposal/Governance/style.less';
|
||||||
import { generateProposal } from './props';
|
import { generateProposal } from './props';
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
userid: 1,
|
||||||
|
displayName: 'Duderino',
|
||||||
|
title: 'The dudester',
|
||||||
|
socialMedias: [],
|
||||||
|
avatar: null,
|
||||||
|
};
|
||||||
const propsNoFunding = generateProposal({
|
const propsNoFunding = generateProposal({
|
||||||
amount: 5,
|
amount: 5,
|
||||||
funded: 0,
|
funded: 0,
|
||||||
|
@ -28,16 +35,16 @@ const propsNotFundedExpired = generateProposal({
|
||||||
const CampaignBlocks = ({ style }: { style: any }) => (
|
const CampaignBlocks = ({ style }: { style: any }) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ProposalCampaignBlock {...propsNoFunding} />
|
<ProposalCampaignBlock {...propsNoFunding} authUser={user} />
|
||||||
</div>
|
</div>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ProposalCampaignBlock {...propsHalfFunded} />
|
<ProposalCampaignBlock {...propsHalfFunded} authUser={user} />
|
||||||
</div>
|
</div>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ProposalCampaignBlock {...propsFunded} />
|
<ProposalCampaignBlock {...propsFunded} authUser={user} />
|
||||||
</div>
|
</div>
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ProposalCampaignBlock {...propsNotFundedExpired} />
|
<ProposalCampaignBlock {...propsNotFundedExpired} authUser={user} />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -203,7 +203,6 @@ export function generateProposal({
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
sendLoading: false,
|
sendLoading: false,
|
||||||
isMissingWeb3: false,
|
|
||||||
proposal,
|
proposal,
|
||||||
...proposal, // yeah...
|
...proposal, // yeah...
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface Contribution {
|
||||||
amount: string;
|
amount: string;
|
||||||
dateCreated: number;
|
dateCreated: number;
|
||||||
status: 'PENDING' | 'CONFIRMED';
|
status: 'PENDING' | 'CONFIRMED';
|
||||||
|
isAnonymous: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContributionWithAddresses extends Contribution {
|
export interface ContributionWithAddresses extends Contribution {
|
||||||
|
@ -22,6 +23,9 @@ export interface ContributionWithUser extends Contribution {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ContributionWithAddressesAndUser = ContributionWithAddresses &
|
||||||
|
ContributionWithUser;
|
||||||
|
|
||||||
export interface UserContribution extends Omit<Contribution, 'amount' | 'txId'> {
|
export interface UserContribution extends Omit<Contribution, 'amount' | 'txId'> {
|
||||||
amount: Zat;
|
amount: Zat;
|
||||||
txId?: string;
|
txId?: string;
|
||||||
|
|
Loading…
Reference in New Issue