diff options
-rw-r--r-- | src/amounts.ts | 8 | ||||
-rw-r--r-- | src/wallet.ts | 47 | ||||
-rw-r--r-- | src/walletTypes.ts | 10 | ||||
-rw-r--r-- | src/webex/messages.ts | 4 | ||||
-rw-r--r-- | src/webex/pages/refund.html | 5 | ||||
-rw-r--r-- | src/webex/pages/refund.tsx | 190 | ||||
-rw-r--r-- | src/webex/renderHtml.tsx | 2 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 9 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 18 |
9 files changed, 132 insertions, 161 deletions
diff --git a/src/amounts.ts b/src/amounts.ts index 8b5278330..b90d54a31 100644 --- a/src/amounts.ts +++ b/src/amounts.ts @@ -103,6 +103,14 @@ export function getZero(currency: string): AmountJson { } +export function sum(amounts: AmountJson[]) { + if (amounts.length <= 0) { + throw Error("can't sum zero amounts"); + } + return add(amounts[0], ...amounts.slice(1)); +} + + /** * Add two amounts. Return the result and whether * the addition overflowed. The overflow is always handled diff --git a/src/wallet.ts b/src/wallet.ts index e90a6e3db..825763173 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -107,9 +107,15 @@ import { DownloadedWithdrawInfo, WithdrawDetails, AcceptWithdrawalResponse, + PurchaseDetails, } from "./walletTypes"; import { openPromise } from "./promiseUtils"; -import { parsePayUri, parseWithdrawUri, parseTipUri, parseRefundUri } from "./taleruri"; +import { + parsePayUri, + parseWithdrawUri, + parseTipUri, + parseRefundUri, +} from "./taleruri"; interface SpeculativePayData { payCoinInfo: PayCoinInfo; @@ -3462,7 +3468,10 @@ export class Wallet { timestamp: new Date().getTime(), tipId: res.tipId, pickupUrl: res.tipPickupUrl, - totalFees: Amounts.add(withdrawDetails.overhead, withdrawDetails.withdrawFee).amount, + totalFees: Amounts.add( + withdrawDetails.overhead, + withdrawDetails.withdrawFee, + ).amount, }; await this.q().put(Stores.tips, tipRecord); } @@ -3585,6 +3594,40 @@ export class Wallet { }; } + async getPurchaseDetails(hc: string): Promise<PurchaseDetails> { + const purchase = await this.q().get(Stores.purchases, hc); + if (!purchase) { + throw Error("unknown purchase"); + } + const refundsDoneAmounts = Object.values(purchase.refundsDone).map(x => + Amounts.parseOrThrow(x.refund_amount), + ); + const refundsPendingAmounts = Object.values(purchase.refundsPending).map( + x => Amounts.parseOrThrow(x.refund_amount), + ); + const totalRefundAmount = Amounts.sum([ + ...refundsDoneAmounts, + ...refundsPendingAmounts, + ]).amount; + const refundsDoneFees = Object.values(purchase.refundsDone).map(x => + Amounts.parseOrThrow(x.refund_amount), + ); + const refundsPendingFees = Object.values(purchase.refundsPending).map( + x => Amounts.parseOrThrow(x.refund_amount), + ); + const totalRefundFees = Amounts.sum([ + ...refundsDoneFees, + ...refundsPendingFees, + ]).amount; + const totalFees = totalRefundFees; + return { + contractTerms: purchase.contractTerms, + hasRefund: purchase.timestamp_refund !== 0, + totalRefundAmount: totalRefundAmount, + totalRefundAndRefreshFees: totalFees, + }; + } + /** * Reset the retry timeouts for ongoing operations. */ diff --git a/src/walletTypes.ts b/src/walletTypes.ts index 060401c2f..0d18e4a9b 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -502,3 +502,13 @@ export interface AcceptWithdrawalResponse { reservePub: string; confirmTransferUrl?: string; } + +/** + * Details about a purchase, including refund status. + */ +export interface PurchaseDetails { + contractTerms: ContractTerms, + hasRefund: boolean, + totalRefundAmount: AmountJson, + totalRefundAndRefreshFees: AmountJson, +}
\ No newline at end of file diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 7b3041ac2..7e99cfc77 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -157,9 +157,9 @@ export interface MessageMap { request: { reportUid: string }; response: void; }; - "get-purchase": { + "get-purchase-details": { request: { contractTermsHash: string }; - response: dbTypes.PurchaseRecord; + response: walletTypes.PurchaseDetails; }; "accept-tip": { request: { talerTipUri: string }; diff --git a/src/webex/pages/refund.html b/src/webex/pages/refund.html index f97dc9d6c..203fda21b 100644 --- a/src/webex/pages/refund.html +++ b/src/webex/pages/refund.html @@ -13,6 +13,9 @@ <script src="/dist/refund-bundle.js"></script> <body> - <div id="container"></div> + <section id="main"> + <h1>GNU Taler Wallet</h1> + <article id="container" class="fade"></article> + </section> </body> </html> diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx index 57d740486..9a7828edb 100644 --- a/src/webex/pages/refund.tsx +++ b/src/webex/pages/refund.tsx @@ -14,177 +14,74 @@ 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 React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom"; import URI = require("urijs"); -import * as dbTypes from "../../dbTypes"; - -import { AmountJson } from "../../amounts"; -import * as Amounts from "../../amounts"; - -import * as timer from "../../timer"; - -import { AmountDisplay } from "../renderHtml"; import * as wxApi from "../wxApi"; +import { PurchaseDetails } from "../../walletTypes"; +import { AmountView } from "../renderHtml"; + +function RefundStatusView(props: { talerRefundUri: string }) { + const [applied, setApplied] = useState(false); + const [purchaseDetails, setPurchaseDetails] = useState< + PurchaseDetails | undefined + >(undefined); + const [errMsg, setErrMsg] = useState<string | undefined>(undefined); + + useEffect(() => { + const doFetch = async () => { + try { + const hc = await wxApi.applyRefund(props.talerRefundUri); + setApplied(true); + const r = await wxApi.getPurchaseDetails(hc); + setPurchaseDetails(r); + } catch (e) { + console.error(e); + setErrMsg(e.message); + console.log("err message", e.message); + } + }; + doFetch(); + }); -interface RefundStatusViewProps { - contractTermsHash?: string; - refundUrl?: string; -} - -interface RefundStatusViewState { - contractTermsHash?: string; - purchase?: dbTypes.PurchaseRecord; - refundFees?: AmountJson; - gotResult: boolean; -} - -interface RefundDetailProps { - purchase: dbTypes.PurchaseRecord; - /** - * Full refund fees (including refreshing) so far, or undefined if no refund - * permission was processed yet - */ - fullRefundFees?: AmountJson; -} - -const RefundDetail = ({purchase, fullRefundFees}: RefundDetailProps) => { - const pendingKeys = Object.keys(purchase.refundsPending); - const doneKeys = Object.keys(purchase.refundsDone); - if (pendingKeys.length === 0 && doneKeys.length === 0) { - return <p>No refunds</p>; - } + console.log("rendering"); - const firstRefundKey = [...pendingKeys, ...doneKeys][0]; - if (!firstRefundKey) { - return <p>Waiting for refunds ...</p>; - } - const allRefunds = { ...purchase.refundsDone, ...purchase.refundsPending }; - const currency = Amounts.parseOrThrow(allRefunds[firstRefundKey].refund_amount).currency; - if (!currency) { - throw Error("invariant"); + if (errMsg) { + return <span>Error: {errMsg}</span>; } - let amountPending = Amounts.getZero(currency); - for (const k of pendingKeys) { - const refundAmount = Amounts.parseOrThrow(purchase.refundsPending[k].refund_amount); - amountPending = Amounts.add(amountPending, refundAmount).amount; + if (!applied || !purchaseDetails) { + return <span>Updating refund status</span>; } - let amountDone = Amounts.getZero(currency); - for (const k of doneKeys) { - const refundAmount = Amounts.parseOrThrow(purchase.refundsDone[k].refund_amount); - amountDone = Amounts.add(amountDone, refundAmount).amount; - } - - const hasPending = amountPending.fraction !== 0 || amountPending.value !== 0; return ( - <div> - {hasPending ? <p>Refund pending: <AmountDisplay amount={amountPending} /></p> : null} + <> + <h2>Refund Status</h2> <p> - Refund received: <AmountDisplay amount={amountDone} />{" "} - (refund fees: {fullRefundFees ? <AmountDisplay amount={fullRefundFees} /> : "??" }) + The product <em>{purchaseDetails.contractTerms.summary!}</em> has + received a total refund of <AmountView amount={purchaseDetails.totalRefundAmount} />. </p> - </div> + <p> + Note that additional fees from the exchange may apply. + </p> + </> ); -}; - -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(); - } - }); - // Just to be safe: update every second, in case we miss a notification - // from the background page. - timer.after(1000, () => this.update()); - } - - 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; - if (!purchase) { - let message; - if (this.state.gotResult) { - message = <span>No purchase with contract terms hash {this.props.contractTermsHash} found</span>; - } else { - message = <span>...</span>; - } - return <div id="main">{message}</div>; - } - 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={Amounts.parseOrThrow(purchase.contractTerms.amount)} /></p> - {purchase.finished - ? <RefundDetail purchase={purchase} fullRefundFees={this.state.refundFees} /> - : <p>Purchase not completed.</p>} - </div> - ); - } - - async update() { - 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); - this.setState({ contractTermsHash }); - } - const purchase = await wxApi.getPurchase(contractTermsHash); - console.log("got purchase", purchase); - // We got a result, but it might be undefined if not found in DB. - this.setState({ purchase, gotResult: true }); - const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]); - if (refundsDone.length) { - const refundFees = await wxApi.getFullRefundFees({ refundPermissions: refundsDone }); - this.setState({ purchase, gotResult: true, refundFees }); - } - } } - 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"); + console.error("fatal: can't mount component, container missing"); return; } @@ -194,7 +91,10 @@ async function main() { return; } - ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} refundUrl={refundUrl} />, container); + ReactDOM.render( + <RefundStatusView talerRefundUri={talerRefundUri} />, + container, + ); } document.addEventListener("DOMContentLoaded", () => main()); diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx index 867fb440f..1c50aa1ad 100644 --- a/src/webex/renderHtml.tsx +++ b/src/webex/renderHtml.tsx @@ -62,7 +62,7 @@ export function renderAmount(amount: AmountJson | string) { return <span>{x} {a.currency}</span>; } -export const AmountDisplay = ({amount}: {amount: AmountJson | string}) => renderAmount(amount); +export const AmountView = ({amount}: {amount: AmountJson | string}) => renderAmount(amount); /** diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index d2e8c94f1..7e4d17e37 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -41,6 +41,7 @@ import { SenderWireInfos, TipStatus, WalletBalance, + PurchaseDetails, } from "../walletTypes"; import { @@ -91,7 +92,7 @@ async function callBackend<T extends MessageType>( return new Promise<MessageMap[T]["response"]>((resolve, reject) => { chrome.runtime.sendMessage({ type, detail }, (resp) => { if (typeof resp === "object" && resp && resp.error) { - const e = new WalletApiError(resp.message, resp); + const e = new WalletApiError(resp.error.message, resp.error); reject(e); } else { resolve(resp); @@ -318,8 +319,8 @@ export function getReport(reportUid: string): Promise<any> { * Look up a purchase in the wallet database from * the contract terms hash. */ -export function getPurchase(contractTermsHash: string): Promise<PurchaseRecord> { - return callBackend("get-purchase", { contractTermsHash }); +export function getPurchaseDetails(contractTermsHash: string): Promise<PurchaseDetails> { + return callBackend("get-purchase-details", { contractTermsHash }); } @@ -356,7 +357,7 @@ export function downloadProposal(url: string): Promise<number> { /** * Download a refund and accept it. */ -export function acceptRefund(refundUrl: string): Promise<string> { +export function applyRefund(refundUrl: string): Promise<string> { return callBackend("accept-refund", { refundUrl }); } diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 570a37586..ea43f65c2 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -62,7 +62,12 @@ function handleMessage( function assertNotFound(t: never): never { console.error(`Request type ${t as string} unknown`); console.error(`Request detail was ${detail}`); - return { error: "request unknown", requestType: type } as never; + return { + error: { + message: `request type ${t as string} unknown`, + requestType: type, + }, + } as never; } function needsWallet(): Wallet { if (!currentWallet) { @@ -264,12 +269,12 @@ function handleMessage( return; case "get-report": return logging.getReport(detail.reportUid); - case "get-purchase": { + case "get-purchase-details": { const contractTermsHash = detail.contractTermsHash; if (!contractTermsHash) { throw Error("contractTermsHash missing"); } - return needsWallet().getPurchase(contractTermsHash); + return needsWallet().getPurchaseDetails(contractTermsHash); } case "accept-refund": return needsWallet().applyRefund(detail.refundUrl); @@ -343,9 +348,10 @@ async function dispatch( } try { sendResponse({ - error: "exception", - message: e.message, - stack, + error: { + message: e.message, + stack, + } }); } catch (e) { console.log(e); |