aboutsummaryrefslogtreecommitdiff
path: root/src/webex
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-01-29 16:41:17 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-01-29 16:41:17 +0100
commit1a66e232a55dff8c889e5554f637f4d4e475179c (patch)
treee02390f0edfecf5e925d44a71c62056060819886 /src/webex
parentc8c03e381e252dc3a73a2c35bb1cd2ee24eeaabb (diff)
downloadwallet-core-1a66e232a55dff8c889e5554f637f4d4e475179c.tar.xz
implement aborting and getting refunds from failed payments
Diffstat (limited to 'src/webex')
-rw-r--r--src/webex/messages.ts6
-rw-r--r--src/webex/pages/confirm-contract.tsx129
-rw-r--r--src/webex/wxApi.ts23
-rw-r--r--src/webex/wxBackend.ts8
4 files changed, 132 insertions, 34 deletions
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 9a7dc8fd4..45cac6a9f 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -170,7 +170,7 @@ export interface MessageMap {
response: dbTypes.PurchaseRecord;
};
"get-full-refund-fees": {
- request: { refundPermissions: talerTypes.RefundPermission[] };
+ request: { refundPermissions: talerTypes.MerchantRefundPermission[] };
response: AmountJson;
};
"accept-tip": {
@@ -201,6 +201,10 @@ export interface MessageMap {
request: { refundUrl: string }
response: string;
};
+ "abort-failed-payment": {
+ request: { contractTermsHash: string }
+ response: void;
+ };
}
/**
diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx
index 7fe6b9600..f41dba069 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -40,6 +40,7 @@ import * as wxApi from "../wxApi";
import * as React from "react";
import * as ReactDOM from "react-dom";
import URI = require("urijs");
+import { WalletApiError } from "../wxApi";
interface DetailState {
@@ -111,7 +112,8 @@ interface ContractPromptProps {
interface ContractPromptState {
proposalId: number | undefined;
proposal: ProposalDownloadRecord | undefined;
- error: string | null;
+ checkPayError: string | undefined;
+ confirmPayError: object | undefined;
payDisabled: boolean;
alreadyPaid: boolean;
exchanges: ExchangeRecord[] | undefined;
@@ -124,21 +126,30 @@ interface ContractPromptState {
payStatus?: CheckPayResult;
replaying: boolean;
payInProgress: boolean;
+ payAttempt: number;
+ working: boolean;
+ abortDone: boolean;
+ abortStarted: boolean;
}
class ContractPrompt extends React.Component<ContractPromptProps, ContractPromptState> {
constructor(props: ContractPromptProps) {
super(props);
this.state = {
+ abortDone: false,
+ abortStarted: false,
alreadyPaid: false,
- error: null,
+ checkPayError: undefined,
+ confirmPayError: undefined,
exchanges: undefined,
holdCheck: false,
+ payAttempt: 0,
payDisabled: true,
payInProgress: false,
proposal: undefined,
proposalId: props.proposalId,
replaying: false,
+ working: false,
};
}
@@ -154,7 +165,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
if (this.props.resourceUrl) {
const p = await wxApi.queryPaymentByFulfillmentUrl(this.props.resourceUrl);
console.log("query for resource url", this.props.resourceUrl, "result", p);
- if (p) {
+ if (p && p.finished) {
if (p.lastSessionSig === undefined || p.lastSessionSig === this.props.sessionId) {
const nextUrl = new URI(p.contractTerms.fulfillment_url);
nextUrl.addSearch("order_id", p.contractTerms.order_id);
@@ -166,6 +177,8 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
} else {
// We're in a new session
this.setState({ replaying: true });
+ // FIXME: This could also go wrong. However the payment
+ // was already successful once, so we can just retry and not refund it.
const payResult = await wxApi.submitPay(p.contractTermsHash, this.props.sessionId);
console.log("payResult", payResult);
location.replace(payResult.nextUrl);
@@ -206,24 +219,24 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub);
const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0);
if (ex) {
- this.setState({ error: msgInsufficient });
+ this.setState({ checkPayError: msgInsufficient });
} else {
- this.setState({ error: msgNoMatch });
+ this.setState({ checkPayError: msgNoMatch });
}
} else {
- this.setState({ error: msgInsufficient });
+ this.setState({ checkPayError: msgInsufficient });
}
this.setState({ payDisabled: true });
} else if (payStatus.status === "paid") {
- this.setState({ alreadyPaid: true, payDisabled: false, error: null, payStatus });
+ this.setState({ alreadyPaid: true, payDisabled: false, checkPayError: undefined, payStatus });
} else {
- this.setState({ payDisabled: false, error: null, payStatus });
+ this.setState({ payDisabled: false, checkPayError: undefined, payStatus });
}
}
async doPayment() {
const proposal = this.state.proposal;
- this.setState({holdCheck: true});
+ this.setState({ holdCheck: true, payAttempt: this.state.payAttempt + 1});
if (!proposal) {
return;
}
@@ -234,11 +247,17 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
}
console.log("confirmPay with", proposalId, "and", this.props.sessionId);
let payResult;
+ this.setState({ working: true });
try {
payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
} catch (e) {
-
+ if (!(e instanceof WalletApiError)) {
+ throw e;
+ }
+ this.setState({ confirmPayError: e.detail });
return;
+ } finally {
+ this.setState({ working: false });
}
console.log("payResult", payResult);
document.location.href = payResult.nextUrl;
@@ -246,6 +265,17 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
}
+ async abortPayment() {
+ const proposal = this.state.proposal;
+ this.setState({ holdCheck: true, abortStarted: true });
+ if (!proposal) {
+ return;
+ }
+ wxApi.abortFailedPayment(proposal.contractTermsHash);
+ this.setState({ abortDone: true });
+ }
+
+
render() {
if (this.props.contractUrl === undefined && this.props.proposalId === undefined) {
return <span>Error: either contractUrl or proposalId must be given</span>;
@@ -272,18 +302,72 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
let products = null;
if (c.products.length) {
products = (
- <>
+ <div>
<span>The following items are included:</span>
<ul>
{c.products.map(
(p: any, i: number) => (<li key={i}>{p.description}: {renderAmount(p.price)}</li>))
}
</ul>
- </>
+ </div>
);
}
+
+ const ConfirmButton = () => (
+ <button className="pure-button button-success"
+ disabled={this.state.payDisabled}
+ onClick={() => this.doPayment()}>
+ {i18n.str`Confirm payment`}
+ </button>
+ );
+
+ const WorkingButton = () => (
+ <div>
+ <button className="pure-button button-success"
+ disabled={this.state.payDisabled}
+ onClick={() => this.doPayment()}>
+ <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
+ {i18n.str`Submitting payment`}
+ </button>
+ </div>
+ );
+
+ const ConfirmPayDialog = () => (
+ <div>
+ {this.state.working ? WorkingButton() : ConfirmButton()}
+ <div>
+ {(this.state.alreadyPaid
+ ? <p className="okaybox">
+ You already paid for this, clicking "Confirm payment" will not cost money again.
+ </p>
+ : <p />)}
+ {(this.state.checkPayError ? <p className="errorbox">{this.state.checkPayError}</p> : <p />)}
+ </div>
+ <Details exchanges={this.state.exchanges} contractTerms={c} collapsed={!this.state.checkPayError}/>
+ </div>
+ );
+
+ const PayErrorDialog = () => (
+ <div>
+ <p>There was an error paying (attempt #{this.state.payAttempt}):</p>
+ <pre>{JSON.stringify(this.state.confirmPayError)}</pre>
+ { this.state.abortStarted
+ ? <span>Aborting payment ...</span>
+ : this.state.abortDone
+ ? <span>Payment aborted!</span>
+ : <>
+ <button className="pure-button" onClick={() => this.doPayment()}>
+ Retry Payment
+ </button>
+ <button className="pure-button" onClick={() => this.abortPayment()}>
+ Abort Payment
+ </button>
+ </>
+ }
+ </div>
+ );
+
return (
- <>
<div>
<i18n.Translate wrap="p">
The merchant <span>{merchantName}</span> {" "}
@@ -302,22 +386,11 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
:
<p>The total price is <span>{amount}</span>.</p>
}
+ { this.state.confirmPayError
+ ? PayErrorDialog()
+ : ConfirmPayDialog()
+ }
</div>
- <button className="pure-button button-success"
- disabled={this.state.payDisabled}
- onClick={() => this.doPayment()}>
- {i18n.str`Confirm payment`}
- </button>
- <div>
- {(this.state.alreadyPaid
- ? <p className="okaybox">
- You already paid for this, clicking "Confirm payment" will not cost money again.
- </p>
- : <p />)}
- {(this.state.error ? <p className="errorbox">{this.state.error}</p> : <p />)}
- </div>
- <Details exchanges={this.state.exchanges} contractTerms={c} collapsed={!this.state.error}/>
- </>
);
}
}
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index a1b0380b9..ee1ca23ba 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -43,7 +43,7 @@ import {
} from "../walletTypes";
import {
- RefundPermission,
+ MerchantRefundPermission,
TipToken,
} from "../talerTypes";
@@ -72,14 +72,22 @@ export interface UpgradeResponse {
}
+export class WalletApiError extends Error {
+ constructor(message: string, public detail: any) {
+ super(message);
+ }
+}
+
+
async function callBackend<T extends MessageType>(
type: T,
detail: MessageMap[T]["request"],
): Promise<MessageMap[T]["response"]> {
return new Promise<MessageMap[T]["response"]>((resolve, reject) => {
chrome.runtime.sendMessage({ type, detail }, (resp) => {
- if (resp && resp.error) {
- reject(resp);
+ if (typeof resp === "object" && resp && resp.error) {
+ const e = new WalletApiError(resp.error.message, resp);
+ reject(e);
} else {
resolve(resp);
}
@@ -327,7 +335,7 @@ export function getPurchase(contractTermsHash: string): Promise<PurchaseRecord>
* Get the refund fees for a refund permission, including
* subsequent refresh and unrefreshable coins.
*/
-export function getFullRefundFees(args: { refundPermissions: RefundPermission[] }): Promise<AmountJson> {
+export function getFullRefundFees(args: { refundPermissions: MerchantRefundPermission[] }): Promise<AmountJson> {
return callBackend("get-full-refund-fees", { refundPermissions: args.refundPermissions });
}
@@ -374,3 +382,10 @@ export function downloadProposal(url: string): Promise<number> {
export function acceptRefund(refundUrl: string): Promise<string> {
return callBackend("accept-refund", { refundUrl });
}
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function abortFailedPayment(contractTermsHash: string) {
+ return callBackend("abort-failed-payment", { contractTermsHash });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 98b543d28..a778cc986 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -308,6 +308,12 @@ function handleMessage(sender: MessageSender,
case "download-proposal": {
return needsWallet().downloadProposal(detail.url);
}
+ case "abort-failed-payment": {
+ if (!detail.contractTermsHash) {
+ throw Error("contracTermsHash not given");
+ }
+ return needsWallet().abortFailedPayment(detail.contractTermsHash);
+ }
case "taler-pay": {
const senderUrl = sender.url;
if (!senderUrl) {
@@ -514,7 +520,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
console.log("processing refund");
const uri = new URI(chrome.extension.getURL("/src/webex/pages/refund.html"));
uri.query({ refundUrl: fields.refund_url });
- return { redirectUrl: uri.href };
+ return { redirectUrl: uri.href() };
}
// We need to do some asynchronous operation, we can't directly redirect