implement flicker-free refunds

This commit is contained in:
Florian Dold 2018-01-22 01:12:08 +01:00
parent 1671d9a508
commit ae177549a5
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 63 additions and 11 deletions

View File

@ -2532,6 +2532,10 @@ export class Wallet {
} }
} }
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
*/
async acceptRefund(refundUrl: string): Promise<string> { async acceptRefund(refundUrl: string): Promise<string> {
console.log("processing refund"); console.log("processing refund");
let resp; let resp;
@ -2598,7 +2602,8 @@ export class Wallet {
return refundPermissions[0].h_contract_terms; return refundPermissions[0].h_contract_terms;
} }
async submitRefunds(contractTermsHash: string): Promise<void> {
private async submitRefunds(contractTermsHash: string): Promise<void> {
const purchase = await this.q().get(Stores.purchases, contractTermsHash); const purchase = await this.q().get(Stores.purchases, contractTermsHash);
if (!purchase) { if (!purchase) {
console.error("not submitting refunds, contract terms not found:", contractTermsHash); console.error("not submitting refunds, contract terms not found:", contractTermsHash);
@ -2644,7 +2649,6 @@ export class Wallet {
return c; return c;
}; };
await this.q() await this.q()
.mutate(Stores.purchases, contractTermsHash, transformPurchase) .mutate(Stores.purchases, contractTermsHash, transformPurchase)
.mutate(Stores.coins, perm.coin_pub, transformCoin) .mutate(Stores.coins, perm.coin_pub, transformCoin)

View File

@ -195,6 +195,10 @@ export interface MessageMap {
request: { contractTermsHash: string, sessionId: string | undefined }; request: { contractTermsHash: string, sessionId: string | undefined };
response: void; response: void;
}; };
"accept-refund": {
request: { refundUrl: string }
response: string;
};
} }
/** /**

View File

@ -35,10 +35,12 @@ import { AmountDisplay } from "../renderHtml";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
interface RefundStatusViewProps { interface RefundStatusViewProps {
contractTermsHash: string; contractTermsHash?: string;
refundUrl?: string;
} }
interface RefundStatusViewState { interface RefundStatusViewState {
contractTermsHash?: string;
purchase?: dbTypes.PurchaseRecord; purchase?: dbTypes.PurchaseRecord;
refundFees?: AmountJson; refundFees?: AmountJson;
gotResult: boolean; gotResult: boolean;
@ -102,13 +104,22 @@ class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStat
} }
render(): JSX.Element { render(): JSX.Element {
if (!this.props.contractTermsHash && !this.props.refundUrl) {
return (
<div id="main">
<span>Error: Neither contract terms hash nor refund url given.</span>
</div>
);
}
const purchase = this.state.purchase; const purchase = this.state.purchase;
if (!purchase) { if (!purchase) {
let message;
if (this.state.gotResult) { if (this.state.gotResult) {
return <span>No purchase with contract terms hash {this.props.contractTermsHash} found</span>; message = <span>No purchase with contract terms hash {this.props.contractTermsHash} found</span>;
} else { } else {
return <span>...</span>; message = <span>...</span>;
} }
return <div id="main">{message}</div>;
} }
const merchantName = purchase.contractTerms.merchant.name || "(unknown)"; const merchantName = purchase.contractTerms.merchant.name || "(unknown)";
const summary = purchase.contractTerms.summary || purchase.contractTerms.order_id; const summary = purchase.contractTerms.summary || purchase.contractTerms.order_id;
@ -128,7 +139,16 @@ class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStat
} }
async update() { async update() {
const purchase = await wxApi.getPurchase(this.props.contractTermsHash); let contractTermsHash = this.state.contractTermsHash;
if (!contractTermsHash) {
const refundUrl = this.props.refundUrl;
if (!refundUrl) {
console.error("neither contractTermsHash nor refundUrl is given");
return;
}
contractTermsHash = await wxApi.acceptRefund(refundUrl);
}
const purchase = await wxApi.getPurchase(contractTermsHash);
console.log("got purchase", purchase); console.log("got purchase", purchase);
const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]); const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]);
const refundFees = await wxApi.getFullRefundFees( {refundPermissions: refundsDone }); const refundFees = await wxApi.getFullRefundFees( {refundPermissions: refundsDone });
@ -147,8 +167,9 @@ async function main() {
return; return;
} }
const contractTermsHash = query.contractTermsHash || "(none)"; const contractTermsHash = query.contractTermsHash;
ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} />, container); const refundUrl = query.refundUrl;
ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} refundUrl={refundUrl} />, container);
} }
document.addEventListener("DOMContentLoaded", () => main()); document.addEventListener("DOMContentLoaded", () => main());

View File

@ -363,3 +363,10 @@ export function talerPay(msg: any): Promise<void> {
export function downloadProposal(url: string): Promise<number> { export function downloadProposal(url: string): Promise<number> {
return callBackend("download-proposal", { url }); return callBackend("download-proposal", { url });
} }
/**
* Download a refund and accept it.
*/
export function acceptRefund(refundUrl: string): Promise<string> {
return callBackend("accept-refund", { refundUrl });
}

View File

@ -292,6 +292,8 @@ function handleMessage(sender: MessageSender,
} }
case "get-full-refund-fees": case "get-full-refund-fees":
return needsWallet().getFullRefundFees(detail.refundPermissions); return needsWallet().getFullRefundFees(detail.refundPermissions);
case "accept-refund":
return needsWallet().acceptRefund(detail.refundUrl);
case "get-tip-status": { case "get-tip-status": {
const tipToken = TipToken.checked(detail.tipToken); const tipToken = TipToken.checked(detail.tipToken);
return needsWallet().getTipStatus(tipToken); return needsWallet().getTipStatus(tipToken);
@ -430,8 +432,8 @@ async function talerPay(fields: any, url: string, tabId: number): Promise<string
} }
if (fields.refund_url) { if (fields.refund_url) {
console.log("processing refund"); console.log("processing refund");
const hc = await w.acceptRefund(fields.refund_url); const uri = new URI(chrome.extension.getURL("/src/webex/pages/refund.html"));
return chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`); return uri.query({ refundUrl: fields.refund_url }).href();
} }
if (fields.tip) { if (fields.tip) {
const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html")); const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html"));
@ -507,10 +509,24 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
return { redirectUrl: uri.href() }; return { redirectUrl: uri.href() };
} }
// Synchronous fast path for refund
if (fields.refund_url) {
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 };
}
// We need to do some asynchronous operation, we can't directly redirect // We need to do some asynchronous operation, we can't directly redirect
talerPay(fields, url, tabId).then((nextUrl) => { talerPay(fields, url, tabId).then((nextUrl) => {
if (nextUrl) { if (nextUrl) {
chrome.tabs.update(tabId, { url: nextUrl }); // We use chrome.tabs.executeScript instead of chrome.tabs.update
// because the latter is buggy when it does not execute in the same
// (micro-?)task as the header callback.
chrome.tabs.executeScript({
code: `document.location.href = decodeURIComponent("${encodeURI(nextUrl)}");`,
runAt: "document_start",
});
} }
}); });