Skip to content

Commit

Permalink
[proposal credits] Integrate pending proposal credits payment feedback (
Browse files Browse the repository at this point in the history
decred#670)

* Integrate pending proposal credits payment feedback

* Set message informing that the operation may take a while
  • Loading branch information
fernandoabolafio authored and tiagoalvesdulce committed Oct 9, 2018
1 parent fbc70a7 commit 28f7014
Show file tree
Hide file tree
Showing 26 changed files with 302 additions and 157 deletions.
1 change: 0 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class Loader extends Component {
if (!prevProps.loggedInAsEmail && this.props.loggedInAsEmail) {
verifyUserPubkey(this.props.loggedInAsEmail, this.props.userPubkey, this.props.keyMismatchAction);
this.props.onLoadDraftProposals(this.props.loggedInAsEmail);
this.props.onLoadPaymentPollingQueue(this.props.loggedInAsEmail);
}

if(!prevProps.onboardViewed && this.props.lastLoginTime === 0){
Expand Down
28 changes: 14 additions & 14 deletions src/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,6 @@ export const onRequestMe = () => (dispatch, getState) => {
dispatch(act.RECEIVE_ME(response));
dispatch(act.SET_PROPOSAL_CREDITS(response.proposalcredits));

const paymentPollingQueue = sel.paymentPollingQueue(getState());

if (paymentPollingQueue.length > 0) {
paymentPollingQueue.forEach( p => {
dispatch(
external_api_actions.verifyUserPayment(
p.address,
p.amount,
p.txid,
p.credits
)
);
});
}
// Start polling for the user paywall tx, if applicable.
const paywallAddress = sel.paywallAddress(getState());
if (paywallAddress) {
Expand Down Expand Up @@ -680,3 +666,17 @@ export const onRevokeVote = (email, token, version) =>
}
);
});

export const onFetchProposalPaywallPayment = () =>
dispatch => {
dispatch(act.REQUEST_PROPOSAL_PAYWALL_PAYMENT());
return api.proposalPaywallPayment().then(
response => dispatch(act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT(response))
).catch(
error => {
dispatch(act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT(null, error));
throw error;
}
);
};

22 changes: 2 additions & 20 deletions src/actions/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,6 @@ export const onLoadDraftProposals = (email) => {
export const onDeleteDraftProposal = (draftId) =>
act.DELETE_DRAFT_PROPOSAL(draftId);


export const onLoadPaymentPollingQueue = (email) => {
const stateFromLS = loadStateLocalStorage(email);
const queue = sel.paymentPollingQueue(stateFromLS) || [];
return act.SET_PAYMENT_POLLING_QUEUE(queue);
};

export const onUpdatePaymentPollingQueue = ({ txid, confirmations }) =>
act.UPDATE_PAYMENT_POLLING_QUEUE({ txid, confirmations });

export const onConfirmPollingPayment = ({ address, txNotBefore }) => {
return (dispatch, getState) => {
const queue = getState().app.paymentPollingQueue || [];
const newQueue = queue.filter( payment => {
return (payment.address !== address) && (payment.txid !== txNotBefore);
});
dispatch(act.SET_PAYMENT_POLLING_QUEUE(newQueue));
};
};

export const onSaveChangeUsername = ({ password, newUsername }) =>
(dispatch, getState) =>
dispatch(onChangeUsername(password, newUsername))
Expand Down Expand Up @@ -202,3 +182,5 @@ export const setOnboardAsViewed = () => act.SET_ONBOARD_AS_VIEWED();
export const resetLastSubmittedProposal = () => act.RESET_LAST_SUBMITTED();

export const onSetCommentsSortOption = (option) => act.SET_COMMENTS_SORT_OPTION(option);

export const toggleCreditsPaymentPolling = (bool) => act.TOGGLE_CREDITS_PAYMENT_POLLING(bool);
26 changes: 8 additions & 18 deletions src/actions/external_api.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as external_api from "../lib/external_api";
import { verifyUserPaymentWithPoliteia, onAddProposalCredits } from "./api";
import { onUpdatePaymentPollingQueue, onConfirmPollingPayment } from "./app";
import { verifyUserPaymentWithPoliteia } from "./api";
import act from "./methods";
import {
PAYWALL_STATUS_LACKING_CONFIRMATIONS,
Expand All @@ -9,7 +8,7 @@ import {
} from "../constants";

const POLL_INTERVAL = 10 * 1000;
export const verifyUserPayment = (address, amount, txNotBefore, credits = false) => dispatch => {
export const verifyUserPayment = (address, amount, txNotBefore) => dispatch => {
// Check dcrdata first.
return external_api.getPaymentsByAddressDcrdata(address)
.then(response => {
Expand Down Expand Up @@ -43,28 +42,20 @@ export const verifyUserPayment = (address, amount, txNotBefore, credits = false)
}

if(txn.confirmations < CONFIRMATIONS_REQUIRED) {
if (credits) {
dispatch(onUpdatePaymentPollingQueue({ txid: txn.id, confirmations: txn.confirmations }));
} else {
dispatch(act.UPDATE_USER_PAYWALL_STATUS({
status: PAYWALL_STATUS_LACKING_CONFIRMATIONS,
currentNumberOfConfirmations: txn.confirmations
}));
}
dispatch(act.UPDATE_USER_PAYWALL_STATUS({
status: PAYWALL_STATUS_LACKING_CONFIRMATIONS,
currentNumberOfConfirmations: txn.confirmations
}));
return false;
}

return verifyUserPaymentWithPoliteia(txn.id);
})
.then(verified => {
if(verified && credits) {
dispatch(act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT_WITH_FAUCET(null));
dispatch(onConfirmPollingPayment({ address, txNotBefore }));
dispatch(onAddProposalCredits({ amount, txNotBefore }));
} else if (verified) {
if (verified) {
dispatch(act.UPDATE_USER_PAYWALL_STATUS({ status: PAYWALL_STATUS_PAID }));
} else {
setTimeout(() => dispatch(verifyUserPayment(address, amount, txNotBefore, credits)), POLL_INTERVAL);
setTimeout(() => dispatch(verifyUserPayment(address, amount, txNotBefore)), POLL_INTERVAL);
}
})
.catch(error => {
Expand Down Expand Up @@ -145,7 +136,6 @@ export const payProposalWithFaucet = (address, amount) => dispatch => {
const payload = { txid: json.Txid, address, amount, confirmations: 0, credits: true };
dispatch(act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT_WITH_FAUCET(payload));
dispatch(act.SAVE_PAYMENT_POLLING_QUEUE(payload));
return dispatch(verifyUserPayment(address, amount, json.Txid, true));
})
.catch(error => {
dispatch(act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT_WITH_FAUCET(null, error));
Expand Down
18 changes: 18 additions & 0 deletions src/actions/tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,24 @@ describe("test api actions (actions/api.js)", () => {

});

test("test onFetchProposalPaywallPayment action", async () => {
const path = "/api/v1/proposals/paywallpayment";

//test it handles a successful response
await assertApiActionOnSuccess(
path,
api.onFetchProposalPaywallPayment,
[],
[
{ type: act.REQUEST_PROPOSAL_PAYWALL_PAYMENT },
{
type: act.RECEIVE_PROPOSAL_PAYWALL_PAYMENT,
error: false
}
]
);
});

// TODO: for the following tests
// needs to decouple modal confirmation from the
// actions so it can be tested
Expand Down
7 changes: 7 additions & 0 deletions src/actions/tests/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ describe("test app actions (actions/app.js)", () => {
], done);
});

test("toggleCreditsPaymentPolling action", () => {
expect(app.toggleCreditsPaymentPolling(true))
.toDispatchActions([
{ type: act.TOGGLE_CREDITS_PAYMENT_POLLING, payload: true }
], done);
});

test("on local storage change action", () => {

//save if values aren't equal
Expand Down
5 changes: 5 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,8 @@ export const REQUEST_AUTHORIZE_VOTE = "REQUEST_AUTHORIZE_VOTE";
export const RECEIVE_AUTHORIZE_VOTE = "RECEIVE_AUTHORIZE_VOTE";

export const RECEIVE_REVOKE_AUTH_VOTE = "RECEIVE_REVOKE_AUTH_VOTE";

export const REQUEST_PROPOSAL_PAYWALL_PAYMENT = "REQUEST_PROPOSAL_PAYWALL_PAYMENT";
export const RECEIVE_PROPOSAL_PAYWALL_PAYMENT = "RECEIVE_PROPOSAL_PAYWALL_PAYMENT";

export const TOGGLE_CREDITS_PAYMENT_POLLING = "TOGGLE_CREDITS_PAYMENT_POLLING";
44 changes: 35 additions & 9 deletions src/components/IntervalComponent.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import React from "react";
import PropTypes from "prop-types";

/**
* IntervalComponent executes a given function on a given interval
* it will start the interval once the "active" prop changes from false to true
* and it will end the interval once the "active" prop changes from true to false
*/
class IntervalComponent extends React.Component {
interval = null
constructor(props) {
super(props);
this.state = { numberOfExecutions: 0 };
}
startInterval = () => {
const { onInterval, intervalPeriod } = this.props;
this.interval = setInterval(onInterval, intervalPeriod);
const { intervalPeriod, executeOnIntervalBeforeFirstInterval } = this.props;
if (executeOnIntervalBeforeFirstInterval) {
this.onInterval();
}
this.interval = setInterval(this.onInterval, intervalPeriod);
}
onInterval = () => {
const { maxNumberOfExecutions, onInterval } = this.props;
let { numberOfExecutions } = this.state;

if (maxNumberOfExecutions && numberOfExecutions === maxNumberOfExecutions) {
this.finishInterval();
} else {
this.setState({ numberOfExecutions: ++numberOfExecutions });
onInterval();
}
}
finishInterval = () => {
clearInterval(this.interval);
this.props.onFinishInterval();
this.setState({ numberOfExecutions: 0 });
this.props.onFinishInterval && this.props.onFinishInterval();
}
componentDidUpdate(prevProps) {
const { active } = this.props;
if (!prevProps.active && active) {
const { active, startOnMount } = this.props;
if (!startOnMount && !prevProps.active && active) {
this.startInterval();
} else if(prevProps.active && !active) {
this.finishInterval();
Expand All @@ -28,10 +52,12 @@ class IntervalComponent extends React.Component {
}

IntervalComponent.propTypes = {
intervalPeriod: PropTypes.number.isRequired,
onInterval: PropTypes.func.isRequired,
onFinishInterval: PropTypes.func.isRequired,
active: PropTypes.bool
intervalPeriod: PropTypes.number.isRequired, // the length of the interval
onInterval: PropTypes.func.isRequired, // the function to be executed
executeOnIntervalBeforeFirstInterval: PropTypes.bool, // if true will execute the onInterval before the first period
onFinishInterval: PropTypes.func, // what to execute once the interval has finished
maxNumberOfExecutions: PropTypes.number, // how many times the onInterval should be executed
active: PropTypes.bool // either the interval is active or not
};

export default IntervalComponent;
32 changes: 17 additions & 15 deletions src/components/ProposalCreditsManager/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ProposalCreditsPage extends React.Component {
componentDidMount() {
this.props.onPurchaseProposalCredits();
}
handlePurchaseCreditsClick = () => this.setState({ showBuyingMessage: true })
handlePurchaseCreditsClick = () => this.setState({ showBuyingMessage: !this.state.showBuyingMessage })
handleBuyWithFaucetClick = () => {
const {
payWithFaucet,
Expand Down Expand Up @@ -43,8 +43,8 @@ class ProposalCreditsPage extends React.Component {
isApiRequestingPayWithFaucet,
payWithFaucetTxId,
payWithFaucetError,
lastPaymentNotConfirmed,
recentPaymentsConfirmed
proposalPaywallPaymentTxid,
...props
} = this.props;
return isApiRequestingProposalPaywall ?
<PageLoadingIcon />
Expand All @@ -66,18 +66,20 @@ class ProposalCreditsPage extends React.Component {
proposalCredits={proposalCredits}
proposalCreditPurchases={proposalCreditPurchases}
isTestnet={isTestnet}
lastPaymentNotConfirmed={lastPaymentNotConfirmed}
recentPaymentsConfirmed={recentPaymentsConfirmed}
{ ...{ ...props, proposalPaywallPaymentTxid }}
/>
{!this.state.showBuyingMessage && (
<ButtonWithLoadingIcon
className="c-btn c-btn-primary"
text="Purchase credits"
disabled={isApiRequestingProposalPaywall || !userCanExecuteActions || lastPaymentNotConfirmed}
isLoading={isApiRequestingProposalPaywall}
onClick={this.handlePurchaseCreditsClick} />
)}
{proposalPaywallAddress && this.state.showBuyingMessage && (
<ButtonWithLoadingIcon
className="c-btn c-btn-primary"
text="Purchase credits"
disabled={isApiRequestingProposalPaywall || proposalPaywallPaymentTxid}
isLoading={isApiRequestingProposalPaywall}
onClick={this.handlePurchaseCreditsClick}
/>
{proposalPaywallPaymentTxid ?
<span>Do not send any other transactions until the current payment is confirmed</span>
: null
}
{proposalPaywallAddress && this.state.showBuyingMessage && !proposalPaywallPaymentTxid && (
<Message
type="info"
className="proposal-paywall-message"
Expand All @@ -96,7 +98,7 @@ class ProposalCreditsPage extends React.Component {
the number of proposal credits you paid for.
</p>
<p style={{ marginTop: "24px" }}>
<b>Note:</b> Make sure to only send 1 transaction to the address, and
<b>Note:</b> Make sure to only send<b> 1 transaction to the address per time</b>, and
also send an exact amount. Any amount that is not a multiple of{" "}
{proposalCreditPrice} DCR will be rounded down to the closest number
of proposal credits.
Expand Down
81 changes: 81 additions & 0 deletions src/components/ProposalCreditsManager/ProposalCreditsIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from "react";
import proposalCreditsConnector from "../../connectors/proposalCredits";
import currentUserConnector from "../../connectors/currentUser";
import IntervalComponent from "../IntervalComponent";
import Tooltip from "../Tooltip";
import { PROPOSAL_CREDITS_MODAL, PAYWALL_MODAL } from "../Modal/modalTypes";
import {
PAYWALL_STATUS_PAID
} from "../../constants";

/**
* ProposalCreditsIndicator indicates what are the current number of credits owned by the user.
* It also controls the polling mechanism to check for new payments and payments confirmations.
*/
class ProposalCreditsIndicator extends React.Component {
componentDidUpdate(prevProps) {
const {
proposalPaywallPaymentTxid,
proposalPaywallAddress
} = this.props;

if (prevProps.proposalPaywallPaymentTxid && !proposalPaywallPaymentTxid) {
// a transaction has been confirmed
// request the user proposal credits to get it updated
this.props.onUserProposalCredits();
// stop polling
this.props.toggleCreditsPaymentPolling(false);
}
if (!prevProps.proposalPaywallAddress && proposalPaywallAddress) {
// start polling
this.props.toggleCreditsPaymentPolling(true);
}
}
onFinishInterval = () => {
if(this.props.pollingCreditsPayment) {
this.props.toggleCreditsPaymentPolling(false);
}
}
render() {
const {
onFetchProposalPaywallPayment,
proposalCredits,
userPaywallStatus,
pollingCreditsPayment,
proposalPaywallPaymentTxid,
openModal
} = this.props;

const pollingInterval = 5 * 1000; // 5 seconds
const awaitingConfirmations = proposalPaywallPaymentTxid;

// if its awaiting confirmations, the polling will be up until the payment
// is confirmed. Otherwise, it will do 12 attempts to check for new payments
const numberOfAttempts = awaitingConfirmations ? null : 12;

return (
<IntervalComponent
intervalPeriod={pollingInterval}
onInterval={onFetchProposalPaywallPayment}
active={pollingCreditsPayment}
executeOnIntervalBeforeFirstInterval={true}
maxNumberOfExecutions={numberOfAttempts}
onFinishInterval={this.onFinishInterval}
>
<Tooltip
text="Proposal credits are purchased to submit proposals. Click here for more information."
position="bottom"
>
<div className="user-proposal-credits" onClick={() => userPaywallStatus !== PAYWALL_STATUS_PAID
? openModal(PAYWALL_MODAL)
: openModal(PROPOSAL_CREDITS_MODAL)}>
<div className="proposal-credits-text">{(proposalCredits || 0) + " proposal credit" + (proposalCredits !== 1 ? "s" : "")}</div>
<span className="proposalc-credits-click-here-text">Click here to buy and update credits</span>
</div>
</Tooltip>
</IntervalComponent>
);
}
}

export default proposalCreditsConnector(currentUserConnector(ProposalCreditsIndicator));
Loading

0 comments on commit 28f7014

Please sign in to comment.