diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-11-30 04:07:36 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-12-01 03:00:09 +0100 |
commit | b8ccc7c990a1542cf80578b41972f9a5b0870af9 (patch) | |
tree | 6f16319f9ce3133c4c4617129a516e692cfc3ac1 /src/webex/pages | |
parent | bc2c4aff8e657c7d5709433f137299491b98d257 (diff) | |
download | wallet-core-b8ccc7c990a1542cf80578b41972f9a5b0870af9.tar.xz |
partial implementation of tipping
Diffstat (limited to 'src/webex/pages')
-rw-r--r-- | src/webex/pages/confirm-create-reserve.tsx | 135 | ||||
-rw-r--r-- | src/webex/pages/tip.html | 24 | ||||
-rw-r--r-- | src/webex/pages/tip.tsx | 155 |
3 files changed, 183 insertions, 131 deletions
diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx index 0e1cb17df..53b0d635f 100644 --- a/src/webex/pages/confirm-create-reserve.tsx +++ b/src/webex/pages/confirm-create-reserve.tsx @@ -22,18 +22,17 @@ * @author Florian Dold */ -import {canonicalizeBaseUrl} from "../../helpers"; +import { canonicalizeBaseUrl } from "../../helpers"; import * as i18n from "../../i18n"; import { AmountJson, Amounts, CreateReserveResponse, CurrencyRecord, - DenominationRecord, ReserveCreationInfo, } from "../../types"; -import {ImplicitStateComponent, StateHolder} from "../components"; +import { ImplicitStateComponent, StateHolder } from "../components"; import { createReserve, getCurrency, @@ -41,9 +40,8 @@ import { getReserveCreationInfo, } from "../wxApi"; -import {Collapsible, renderAmount} from "../renderHtml"; +import { renderAmount, WithdrawDetailView } from "../renderHtml"; -import * as moment from "moment"; import * as React from "react"; import * as ReactDOM from "react-dom"; import URI = require("urijs"); @@ -80,126 +78,6 @@ class EventTrigger { } -function renderAuditorDetails(rci: ReserveCreationInfo|null) { - console.log("rci", rci); - if (!rci) { - return ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - if (rci.exchangeInfo.auditors.length === 0) { - return ( - <p> - The exchange is not audited by any auditors. - </p> - ); - } - return ( - <div> - {rci.exchangeInfo.auditors.map((a) => ( - <div> - <h3>Auditor {a.auditor_url}</h3> - <p>Public key: {a.auditor_pub}</p> - <p>Trusted: {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}</p> - <p>Audits {a.denomination_keys.length} of {rci.numOfferedDenoms} denominations</p> - </div> - ))} - </div> - ); -} - -function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { - if (!rci) { - return ( - <p> - Details will be displayed when a valid exchange provider URL is entered. - </p> - ); - } - - const denoms = rci.selectedDenoms; - - const countByPub: {[s: string]: number} = {}; - const uniq: DenominationRecord[] = []; - - denoms.forEach((x: DenominationRecord) => { - let c = countByPub[x.denomPub] || 0; - if (c === 0) { - uniq.push(x); - } - c += 1; - countByPub[x.denomPub] = c; - }); - - function row(denom: DenominationRecord) { - return ( - <tr> - <td>{countByPub[denom.denomPub] + "x"}</td> - <td>{renderAmount(denom.value)}</td> - <td>{renderAmount(denom.feeWithdraw)}</td> - <td>{renderAmount(denom.feeRefresh)}</td> - <td>{renderAmount(denom.feeDeposit)}</td> - </tr> - ); - } - - function wireFee(s: string) { - return [ - <thead> - <tr> - <th colSpan={3}>Wire Method {s}</th> - </tr> - <tr> - <th>Applies Until</th> - <th>Wire Fee</th> - <th>Closing Fee</th> - </tr> - </thead>, - <tbody> - {rci!.wireFees.feesForType[s].map((f) => ( - <tr> - <td>{moment.unix(f.endStamp).format("llll")}</td> - <td>{renderAmount(f.wireFee)}</td> - <td>{renderAmount(f.closingFee)}</td> - </tr> - ))} - </tbody>, - ]; - } - - const withdrawFee = renderAmount(rci.withdrawFee); - const overhead = renderAmount(rci.overhead); - - return ( - <div> - <h3>Overview</h3> - <p>{i18n.str`Withdrawal fees:`} {withdrawFee}</p> - <p>{i18n.str`Rounding loss:`} {overhead}</p> - <p>{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p> - <h3>Coin Fees</h3> - <table className="pure-table"> - <thead> - <tr> - <th>{i18n.str`# Coins`}</th> - <th>{i18n.str`Value`}</th> - <th>{i18n.str`Withdraw Fee`}</th> - <th>{i18n.str`Refresh Fee`}</th> - <th>{i18n.str`Deposit Fee`}</th> - </tr> - </thead> - <tbody> - {uniq.map(row)} - </tbody> - </table> - <h3>Wire Fees</h3> - <table className="pure-table"> - {Object.keys(rci.wireFees.feesForType).map(wireFee)} - </table> - </div> - ); -} interface ExchangeSelectionProps { @@ -428,12 +306,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { </button> </p> {this.renderUpdateStatus()} - <Collapsible initiallyCollapsed={true} title="Fee and Spending Details"> - {renderReserveCreationDetails(this.reserveCreationInfo())} - </Collapsible> - <Collapsible initiallyCollapsed={true} title="Auditor Details"> - {renderAuditorDetails(this.reserveCreationInfo())} - </Collapsible> + <WithdrawDetailView rci={this.reserveCreationInfo()} /> </div> ); } diff --git a/src/webex/pages/tip.html b/src/webex/pages/tip.html new file mode 100644 index 000000000..72d91a123 --- /dev/null +++ b/src/webex/pages/tip.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>Taler Wallet: Received Tip</title> + + <link rel="icon" href="/img/icon.png"> + <link rel="stylesheet" type="text/css" href="../style/pure.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <script src="/dist/page-common-bundle.js"></script> + <script src="/dist/tip-bundle.js"></script> + +</head> + +<body> + <section id="main"> + <h1>GNU Taler Wallet</h1> + <div id="container"></div> + </section> +</body> + +</html> diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx new file mode 100644 index 000000000..7f3a7c1fe --- /dev/null +++ b/src/webex/pages/tip.tsx @@ -0,0 +1,155 @@ +/* + This file is part of TALER + (C) 2017 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 shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import URI = require("urijs"); + +import * as i18n from "../../i18n"; + +import { + acceptTip, + getTipStatus, +} from "../wxApi"; + +import { renderAmount, WithdrawDetailView } from "../renderHtml"; + +import { Amounts, TipStatus } from "../../types"; + +interface TipDisplayProps { + merchantDomain: string; + tipId: string; +} + +interface TipDisplayState { + tipStatus?: TipStatus; + working: boolean; +} + +class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> { + constructor(props: TipDisplayProps) { + super(props); + this.state = { working: false }; + } + + async update() { + let tipStatus = await getTipStatus(this.props.merchantDomain, this.props.tipId); + this.setState({ tipStatus }); + } + + componentDidMount() { + this.update(); + const port = chrome.runtime.connect(); + port.onMessage.addListener((msg: any) => { + if (msg.notify) { + console.log("got notified"); + this.update(); + } + }); + this.update(); + } + + renderExchangeInfo(ts: TipStatus) { + const rci = ts.rci; + if (!rci) { + return <p>Waiting for info about exchange ...</p> + } + const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + return ( + <div> + <p> + The tip is handled by the exchange <strong>{rci.exchangeInfo.baseUrl}</strong>.{" "} + The exchange provider will charge + {" "} + <strong>{renderAmount(totalCost)}</strong> + {" "}. + </p> + <WithdrawDetailView rci={rci} /> + </div> + ); + } + + accept() { + this.setState({ working: true}); + acceptTip(this.props.merchantDomain, this.props.tipId); + } + + renderButtons() { + return ( + <form className="pure-form"> + <button + className="pure-button pure-button-primary" + type="button" + onClick={() => this.accept()}> + { this.state.working ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span> : null } + Accept tip + </button> + {" "} + <button className="pure-button" type="button" onClick={() => { window.close(); }}>Discard tip</button> + </form> + ); + } + + render(): JSX.Element { + const ts = this.state.tipStatus; + if (!ts) { + return <p>Processing ...</p>; + } + return ( + <div> + <h2>Tip Received!</h2> + <p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <strong>{this.props.merchantDomain}</strong>.</p> + {ts.tip.accepted + ? <p>You've accepted this tip!</p> + : this.renderButtons() + } + {this.renderExchangeInfo(ts)} + </div> + ); + } +} + +async function main() { + try { + const url = new URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + + let merchantDomain = query.merchant_domain; + let tipId = query.tip_id; + let props: TipDisplayProps = { tipId, merchantDomain }; + + ReactDOM.render(<TipDisplay {...props} />, + document.getElementById("container")!); + + } catch (e) { + // TODO: provide more context information, maybe factor it out into a + // TODO:generic error reporting function or component. + document.body.innerText = i18n.str`Fatal error: "${e.message}".`; + console.error(`got error "${e.message}"`, e); + } +} + +document.addEventListener("DOMContentLoaded", () => { + main(); +}); |