aboutsummaryrefslogtreecommitdiff
path: root/src/webex
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2017-08-27 03:56:19 +0200
committerFlorian Dold <florian.dold@gmail.com>2017-08-27 03:56:19 +0200
commit8697efd2c8751717a3a3fcaf72feb7c49ebfec02 (patch)
treee41b044c85d459e9b6042aab541fd6c88470528b /src/webex
parent21c176a69ee04c4d59baedb79017f6c42ece22d6 (diff)
downloadwallet-core-8697efd2c8751717a3a3fcaf72feb7c49ebfec02.tar.xz
implement refunds
Diffstat (limited to 'src/webex')
-rw-r--r--src/webex/messages.ts8
-rw-r--r--src/webex/notify.ts186
-rw-r--r--src/webex/pages/refund.html18
-rw-r--r--src/webex/pages/refund.tsx138
-rw-r--r--src/webex/renderHtml.tsx2
-rw-r--r--src/webex/wxApi.ts16
-rw-r--r--src/webex/wxBackend.ts22
7 files changed, 272 insertions, 118 deletions
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 397e8876e..7de28b9e9 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -184,6 +184,14 @@ export interface MessageMap {
request: { reportUid: string };
response: void;
};
+ "accept-refund": {
+ request: any;
+ response: void;
+ };
+ "get-purchase": {
+ request: any;
+ response: void;
+ }
}
/**
diff --git a/src/webex/notify.ts b/src/webex/notify.ts
index 51abdb0e0..da4657a96 100644
--- a/src/webex/notify.ts
+++ b/src/webex/notify.ts
@@ -30,6 +30,8 @@ import wxApi = require("./wxApi");
import { QueryPaymentResult } from "../types";
+import axios from 'axios';
+
declare var cloneInto: any;
let logVerbose: boolean = false;
@@ -98,85 +100,38 @@ function setStyles(installed: boolean) {
}
-function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
+async function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
if (!maybeFoundResponse.found) {
console.log("pay-failed", {hint: "payment not found in the wallet"});
return;
}
const walletResp = maybeFoundResponse;
- /**
- * Handle a failed payment.
- *
- * Try to notify the wallet first, before we show a potentially
- * synchronous error message (such as an alert) or leave the page.
- */
- async function handleFailedPayment(r: XMLHttpRequest) {
- let timeoutHandle: number|null = null;
- function err() {
- // FIXME: proper error reporting!
- console.log("pay-failed", {status: r.status, response: r.responseText});
- }
- function onTimeout() {
- timeoutHandle = null;
- err();
- }
- timeoutHandle = window.setTimeout(onTimeout, 200);
-
- await wxApi.paymentFailed(walletResp.contractTermsHash);
- if (timeoutHandle !== null) {
- clearTimeout(timeoutHandle);
- timeoutHandle = null;
- }
- err();
- }
logVerbose && console.log("handling taler-notify-payment: ", walletResp);
- // Payment timeout in ms.
- let timeout_ms = 1000;
- // Current request.
- let r: XMLHttpRequest|null;
- let timeoutHandle: number|null = null;
- function sendPay() {
- r = new XMLHttpRequest();
- r.open("post", walletResp.contractTerms.pay_url);
- r.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
- r.send(JSON.stringify(walletResp.payReq));
- r.onload = async () => {
- if (!r) {
- return;
- }
- switch (r.status) {
- case 200:
- const merchantResp = JSON.parse(r.responseText);
- logVerbose && console.log("got success from pay_url");
- await wxApi.paymentSucceeded(walletResp.contractTermsHash, merchantResp.sig);
- const nextUrl = walletResp.contractTerms.fulfillment_url;
- logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl);
- window.location.href = nextUrl;
- window.location.reload(true);
- break;
- default:
- handleFailedPayment(r);
- break;
- }
- r = null;
- if (timeoutHandle !== null) {
- clearTimeout(timeoutHandle!);
- timeoutHandle = null;
- }
- };
- function retry() {
- if (r) {
- r.abort();
- r = null;
- }
- timeout_ms = Math.min(timeout_ms * 2, 10 * 1000);
- logVerbose && console.log("sendPay timed out, retrying in ", timeout_ms, "ms");
- sendPay();
+ let resp;
+ try {
+ const config = {
+ timeout: 5000, /* 5 seconds */
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
+ validateStatus: (s: number) => s == 200,
}
- timeoutHandle = window.setTimeout(retry, timeout_ms);
+ resp = await axios.post(walletResp.contractTerms.pay_url, walletResp.payReq, config);
+ } catch (e) {
+ // Gives the user the option to retry / abort and refresh
+ wxApi.logAndDisplayError({
+ name: "pay-post-failed",
+ message: e.message,
+ response: e.response,
+ });
+ throw e;
}
- sendPay();
+ const merchantResp = resp.data;
+ logVerbose && console.log("got success from pay_url");
+ await wxApi.paymentSucceeded(walletResp.contractTermsHash, merchantResp.sig);
+ const nextUrl = walletResp.contractTerms.fulfillment_url;
+ logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl);
+ window.location.href = nextUrl;
+ window.location.reload(true);
}
@@ -233,53 +188,24 @@ function init() {
type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
-function downloadContract(url: string, nonce: string): Promise<any> {
+async function downloadContract(url: string, nonce: string): Promise<any> {
const parsed_url = new URI(url);
url = parsed_url.setQuery({nonce}).href();
- // FIXME: include and check nonce!
- return new Promise((resolve, reject) => {
- const contract_request = new XMLHttpRequest();
- console.log("downloading contract from '" + url + "'");
- contract_request.open("GET", url, true);
- contract_request.onload = (e) => {
- if (contract_request.readyState === 4) {
- if (contract_request.status === 200) {
- console.log("response text:",
- contract_request.responseText);
- const contract_wrapper = JSON.parse(contract_request.responseText);
- if (!contract_wrapper) {
- console.error("response text was invalid json");
- const detail = {
- body: contract_request.responseText,
- hint: "invalid json",
- status: contract_request.status,
- };
- reject(detail);
- return;
- }
- resolve(contract_wrapper);
- } else {
- const detail = {
- body: contract_request.responseText,
- hint: "contract download failed",
- status: contract_request.status,
- };
- reject(detail);
- return;
- }
- }
- };
- contract_request.onerror = (e) => {
- const detail = {
- body: contract_request.responseText,
- hint: "contract download failed",
- status: contract_request.status,
- };
- reject(detail);
- return;
- };
- contract_request.send();
- });
+ console.log("downloading contract from '" + url + "'");
+ let resp;
+ try {
+ resp = await axios.get(url, { validateStatus: (s) => s == 200 });
+ } catch (e) {
+ wxApi.logAndDisplayError({
+ name: "contract-download-failed",
+ message: e.message,
+ response: e.response,
+ sameTab: true,
+ });
+ throw e;
+ }
+ console.log("got response", resp);
+ return resp.data;
}
async function processProposal(proposal: any) {
@@ -328,8 +254,38 @@ async function processProposal(proposal: any) {
document.location.replace(target);
}
+
+/**
+ * Handle a payment request (coming either from an HTTP 402 or
+ * the JS wallet API).
+ */
function talerPay(msg: any): Promise<any> {
+ // Use a promise directly instead of of an async
+ // function since some paths never resolve the promise.
return new Promise(async(resolve, reject) => {
+ if (msg.refund_url) {
+ console.log("processing refund");
+ let resp;
+ try {
+ const config = {
+ validateStatus: (s: number) => s == 200,
+ }
+ resp = await axios.get(msg.refund_url, config);
+ } catch (e) {
+ wxApi.logAndDisplayError({
+ name: "refund-download-failed",
+ message: e.message,
+ response: e.response,
+ sameTab: true,
+ });
+ throw e;
+ }
+ await wxApi.acceptRefund(resp.data);
+ const hc = resp.data.refund_permissions[0].h_contract_terms;
+ document.location.href = chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
+ return;
+ }
+
// current URL without fragment
const url = new URI(document.location.href).fragment("").href();
const res = await wxApi.queryPayment(url);
diff --git a/src/webex/pages/refund.html b/src/webex/pages/refund.html
new file mode 100644
index 000000000..f97dc9d6c
--- /dev/null
+++ b/src/webex/pages/refund.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <title>Taler Wallet: Refund Status</title>
+
+ <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+
+ <link rel="icon" href="/img/icon.png">
+
+ <script src="/dist/page-common-bundle.js"></script>
+ <script src="/dist/refund-bundle.js"></script>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx
new file mode 100644
index 000000000..b9506bf29
--- /dev/null
+++ b/src/webex/pages/refund.tsx
@@ -0,0 +1,138 @@
+/*
+ This file is part of TALER
+ (C) 2015-2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+/**
+ * Page that shows refund status for purchases.
+ *
+ * @author Florian Dold
+ */
+
+
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import URI = require("urijs");
+
+import * as wxApi from "../wxApi";
+import * as types from "../../types";
+
+import { AmountDisplay } from "../renderHtml";
+
+interface RefundStatusViewProps {
+ contractTermsHash: string;
+}
+
+interface RefundStatusViewState {
+ purchase?: types.PurchaseRecord;
+ gotResult: boolean;
+}
+
+
+const RefundDetail = ({purchase}: {purchase: types.PurchaseRecord}) => {
+ const pendingKeys = Object.keys(purchase.refundsPending);
+ const doneKeys = Object.keys(purchase.refundsDone);
+ if (pendingKeys.length == 0 && doneKeys.length == 0) {
+ return <p>No refunds</p>;
+ }
+
+ const currency = { ...purchase.refundsDone, ...purchase.refundsPending }[([...pendingKeys, ...doneKeys][0])].refund_amount.currency;
+ if (!currency) {
+ throw Error("invariant");
+ }
+
+ let amountPending = types.Amounts.getZero(currency);
+ let feesPending = types.Amounts.getZero(currency)
+ for (let k of pendingKeys) {
+ amountPending = types.Amounts.add(amountPending, purchase.refundsPending[k].refund_amount).amount;
+ feesPending = types.Amounts.add(feesPending, purchase.refundsPending[k].refund_fee).amount;
+ }
+ let amountDone = types.Amounts.getZero(currency);
+ let feesDone = types.Amounts.getZero(currency);
+ for (let k of doneKeys) {
+ amountDone = types.Amounts.add(amountDone, purchase.refundsDone[k].refund_amount).amount;
+ feesDone = types.Amounts.add(feesDone, purchase.refundsDone[k].refund_fee).amount;
+ }
+
+ return (
+ <div>
+ <p>Refund fully received: <AmountDisplay amount={amountDone} /> (refund fees: <AmountDisplay amount={feesDone} />)</p>
+ <p>Refund incoming: <AmountDisplay amount={amountPending} /> (refund fees: <AmountDisplay amount={feesPending} />)</p>
+ </div>
+ );
+};
+
+class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStatusViewState> {
+
+ constructor(props: RefundStatusViewProps) {
+ super(props);
+ this.state = { gotResult: false };
+ }
+
+ componentDidMount() {
+ this.update();
+ const port = chrome.runtime.connect();
+ port.onMessage.addListener((msg: any) => {
+ if (msg.notify) {
+ console.log("got notified");
+ this.update();
+ }
+ });
+ }
+
+ render(): JSX.Element {
+ const purchase = this.state.purchase;
+ if (!purchase) {
+ if (this.state.gotResult) {
+ return <span>No purchase with contract terms hash {this.props.contractTermsHash} found</span>;
+ } else {
+ return <span>...</span>;
+ }
+ }
+ const merchantName = purchase.contractTerms.merchant.name || "(unknown)";
+ const summary = purchase.contractTerms.summary || purchase.contractTerms.order_id;
+ return (
+ <div id="main">
+ <h1>Refund Status</h1>
+ <p>Status of purchase <strong>{summary}</strong> from merchant <strong>{merchantName}</strong> (order id {purchase.contractTerms.order_id}).</p>
+ <p>Total amount: <AmountDisplay amount={purchase.contractTerms.amount} /></p>
+ {purchase.finished ? <RefundDetail purchase={purchase} /> : <p>Purchase not completed.</p>}
+ </div>
+ );
+ }
+
+ async update() {
+ const purchase = await wxApi.getPurchase(this.props.contractTermsHash);
+ console.log("got purchase", purchase);
+ this.setState({ purchase, gotResult: true });
+ }
+}
+
+
+async function main() {
+ const url = new URI(document.location.href);
+ const query: any = URI.parseQuery(url.query());
+
+ const container = document.getElementById("container");
+ if (!container) {
+ console.error("fatal: can't mount component, countainer missing");
+ return;
+ }
+
+ const contractTermsHash = query.contractTermsHash || "(none)";
+ ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} />, container);
+}
+
+document.addEventListener("DOMContentLoaded", () => main());
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index 51f9019ef..fe964e68a 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -73,6 +73,8 @@ export function renderAmount(amount: AmountJson) {
return <span>{x}&nbsp;{amount.currency}</span>;
}
+export const AmountDisplay = ({amount}: {amount: AmountJson}) => renderAmount(amount);
+
/**
* Abbreviate a string to a given length, and show the full
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index 306406a1a..1423da53b 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -31,6 +31,7 @@ import {
DenominationRecord,
ExchangeRecord,
PreCoinRecord,
+ PurchaseRecord,
QueryPaymentResult,
ReserveCreationInfo,
ReserveRecord,
@@ -322,6 +323,13 @@ export function returnCoins(args: { amount: AmountJson, exchange: string, sender
return callBackend("return-coins", args);
}
+
+/**
+ * Record an error report and display it in a tabl.
+ *
+ * If sameTab is set, the error report will be opened in the current tab,
+ * otherwise in a new tab.
+ */
export function logAndDisplayError(args: any): Promise<void> {
return callBackend("log-and-display-error", args);
}
@@ -329,3 +337,11 @@ export function logAndDisplayError(args: any): Promise<void> {
export function getReport(reportUid: string): Promise<void> {
return callBackend("get-report", { reportUid });
}
+
+export function acceptRefund(refundData: any): Promise<number> {
+ return callBackend("accept-refund", refundData);
+}
+
+export function getPurchase(contractTermsHash: string): Promise<PurchaseRecord> {
+ return callBackend("get-purchase", { contractTermsHash });
+}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 353961ff0..0d1c2d8ca 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -305,13 +305,24 @@ function handleMessage(sender: MessageSender,
}
case "log-and-display-error":
logging.storeReport(detail).then((reportUid) => {
- chrome.tabs.create({
- url: chrome.extension.getURL(`/src/webex/pages/error.html?reportUid=${reportUid}`),
- });
+ const url = chrome.extension.getURL(`/src/webex/pages/error.html?reportUid=${reportUid}`);
+ if (detail.sameTab && sender && sender.tab && sender.tab.id) {
+ chrome.tabs.update(detail.tabId, { url });
+ } else {
+ chrome.tabs.create({ url });
+ }
});
return;
case "get-report":
return logging.getReport(detail.reportUid);
+ case "accept-refund":
+ return needsWallet().acceptRefund(detail.refund_permissions);
+ case "get-purchase":
+ const contractTermsHash = detail.contractTermsHash;
+ if (!contractTermsHash) {
+ throw Error("contractTermsHash missing");
+ }
+ return needsWallet().getPurchase(contractTermsHash);
default:
// Exhaustiveness check.
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html
@@ -380,6 +391,9 @@ class ChromeNotifier implements Notifier {
/**
* Mapping from tab ID to payment information (if any).
+ *
+ * Used to pass information from an intercepted HTTP header to the content
+ * script on the page.
*/
const paymentRequestCookies: { [n: number]: any } = {};
@@ -401,6 +415,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
const fields = {
contract_url: headers["x-taler-contract-url"],
offer_url: headers["x-taler-offer-url"],
+ refund_url: headers["x-taler-refund-url"],
};
const talerHeaderFound = Object.keys(fields).filter((x: any) => (fields as any)[x]).length !== 0;
@@ -415,6 +430,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
const payDetail = {
contract_url: fields.contract_url,
offer_url: fields.offer_url,
+ refund_url: fields.refund_url,
};
console.log("got pay detail", payDetail);