diff options
Diffstat (limited to 'pages')
-rw-r--r-- | pages/confirm-contract.html | 75 | ||||
-rw-r--r-- | pages/confirm-contract.tsx | 134 | ||||
-rw-r--r-- | pages/confirm-create-reserve.html | 76 | ||||
-rw-r--r-- | pages/confirm-create-reserve.tsx | 390 | ||||
-rw-r--r-- | pages/debug.html | 13 | ||||
-rw-r--r-- | pages/help/empty-wallet.html | 30 | ||||
-rw-r--r-- | pages/show-db.html | 15 | ||||
-rw-r--r-- | pages/show-db.ts | 57 |
8 files changed, 790 insertions, 0 deletions
diff --git a/pages/confirm-contract.html b/pages/confirm-contract.html new file mode 100644 index 000000000..e7200910a --- /dev/null +++ b/pages/confirm-contract.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Taler Wallet: Confirm Reserve Creation</title> + + <link rel="stylesheet" type="text/css" href="../style/lang.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <link rel="icon" href="../img/icon.png"> + + <script src="../lib/vendor/URI.js"></script> + <script src="../lib/vendor/mithril.js"></script> + <script src="../lib/vendor/lodash.core.min.js"></script> + <script src="../lib/vendor/system-csp-production.src.js"></script> + <script src="../lib/vendor/jed.js"></script> + <script src="../lib/i18n.js"></script> + <script src="../i18n/strings.js"></script> + <script src="../lib/module-trampoline.js"></script> + + <style> + button.accept { + background-color: #5757D2; + border: 1px solid black; + border-radius: 5px; + margin: 1em 0; + padding: 0.5em; + font-weight: bold; + color: white; + } + button.linky { + background:none!important; + border:none; + padding:0!important; + + font-family:arial,sans-serif; + color:#069; + text-decoration:underline; + cursor:pointer; + } + + input.url { + width: 25em; + } + + + button.accept:disabled { + background-color: #dedbe8; + border: 1px solid white; + border-radius: 5px; + margin: 1em 0; + padding: 0.5em; + font-weight: bold; + color: #2C2C2C; + } + + .errorbox { + border: 1px solid; + display: inline-block; + margin: 1em; + padding: 1em; + font-weight: bold; + background: #FF8A8A; + } + </style> +</head> + +<body> + <section id="main"> + <h1>GNU Taler Wallet</h1> + <article id="contract" class="fade"></article> + </section> +</body> + +</html> diff --git a/pages/confirm-contract.tsx b/pages/confirm-contract.tsx new file mode 100644 index 000000000..f162dca85 --- /dev/null +++ b/pages/confirm-contract.tsx @@ -0,0 +1,134 @@ +/* + This file is part of TALER + (C) 2015 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 entering + * a contract. + * + * @author Florian Dold + */ + + +/// <reference path="../lib/decl/handlebars/handlebars.d.ts" /> +import MithrilComponent = _mithril.MithrilComponent; +import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; +import m from "mithril"; +import {Contract, AmountJson} from "../lib/wallet/types"; +import {renderContract, prettyAmount} from "../lib/wallet/renderHtml"; +"use strict"; + +const Details = { + controller() { + return {collapsed: m.prop(true)}; + }, + view(ctrl: any, contract: Contract) { + if (ctrl.collapsed()) { + return m("div", [ + m("button.linky", { + onclick: () => { + ctrl.collapsed(false); + } + }, "show more details") + ]); + } else { + return m("div", [ + m("button.linky", { + onclick: () => { + ctrl.collapsed(true); + } + }, "show less details"), + m("div", [ + "Accepted exchanges:", + m("ul", + contract.exchanges.map(e => m("li", `${e.url}: ${e.master_pub}`))) + ]) + ]); + } + } +}; + +export function main() { + let url = URI(document.location.href); + let query: any = URI.parseQuery(url.query()); + let offer = JSON.parse(query.offer); + console.dir(offer); + let contract = offer.contract; + let error: string|null = null; + let payDisabled = true; + + var Contract = { + view(ctrl: any) { + return [ + renderContract(contract), + m("button.accept", + {onclick: doPayment, disabled: payDisabled}, + i18n`Confirm Payment`), + (error ? m("p.errorbox", error) : []), + m(Details, contract) + ]; + } + }; + + m.mount(document.getElementById("contract")!, Contract); + + function checkPayment() { + chrome.runtime.sendMessage({type: 'check-pay', detail: {offer}}, (resp) => { + if (resp.error) { + console.log("check-pay error", JSON.stringify(resp)); + switch (resp.error) { + case "coins-insufficient": + error = i18n`You have insufficient funds of the requested currency in your wallet.`; + break; + default: + error = `Error: ${resp.error}`; + break; + } + payDisabled = true; + } else { + payDisabled = false; + error = null; + } + m.redraw(); + window.setTimeout(checkPayment, 300); + }); + } + + checkPayment(); + + + function doPayment() { + let d = {offer}; + chrome.runtime.sendMessage({type: 'confirm-pay', detail: d}, (resp) => { + if (resp.error) { + console.log("confirm-pay error", JSON.stringify(resp)); + switch (resp.error) { + case "coins-insufficient": + error = "You do not have enough coins of the requested currency."; + break; + default: + error = `Error: ${resp.error}`; + break; + } + m.redraw(); + return; + } + let c = d.offer.contract; + console.log("contract", c); + document.location.href = substituteFulfillmentUrl(c.fulfillment_url, + offer); + }); + } +} diff --git a/pages/confirm-create-reserve.html b/pages/confirm-create-reserve.html new file mode 100644 index 000000000..b8a825bd1 --- /dev/null +++ b/pages/confirm-create-reserve.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Taler Wallet: Select Taler Provider</title> + + <link rel="icon" href="../img/icon.png"> + + <script src="../lib/vendor/URI.js"></script> + <script src="../lib/vendor/mithril.js"></script> + <script src="../lib/vendor/system-csp-production.src.js"></script> + <script src="../lib/vendor/jed.js"></script> + <script src="../lib/i18n.js"></script> + <script src="../i18n/strings.js"></script> + <script src="../lib/module-trampoline.js"></script> + + <style> + #main { + border: solid 1px black; + border-radius: 10px; + margin: auto; + max-width: 50%; + padding: 2em; + } + + button.accept { + background-color: #5757D2; + border: 1px solid black; + border-radius: 5px; + margin: 1em 0; + padding: 0.5em; + font-weight: bold; + color: white; + } + button.linky { + background:none!important; + border:none; + padding:0!important; + + font-family:arial,sans-serif; + color:#069; + text-decoration:underline; + cursor:pointer; + } + + input.url { + width: 25em; + } + + table { + border-collapse: collapse; + } + + td { + border-left: 1px solid black; + border-right: 1px solid black; + text-align: center; + padding: 0.3em; + } + + span.spacer { + padding-left: 0.5em; + padding-right: 0.5em; + } + + </style> +</head> + +<body> + <section id="main"> + <h1>GNU Taler Wallet</h1> + <div class="fade" id="exchange-selection"></div> + </section> +</body> + +</html> diff --git a/pages/confirm-create-reserve.tsx b/pages/confirm-create-reserve.tsx new file mode 100644 index 000000000..8e8067052 --- /dev/null +++ b/pages/confirm-create-reserve.tsx @@ -0,0 +1,390 @@ +/* + 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 shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +/// <reference path="../lib/decl/mithril.d.ts" /> + +import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers"; +import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; +import m from "mithril"; +import {ReserveCreationInfo, Amounts} from "../lib/wallet/types"; +import MithrilComponent = _mithril.MithrilComponent; +import {Denomination} from "../lib/wallet/types"; +import {getReserveCreationInfo} from "../lib/wallet/wxApi"; + +"use strict"; + +/** + * Execute something after a delay, with the possibility + * to reset the delay. + */ +class DelayTimer { + ms: number; + f: () => void; + timerId: number|undefined = undefined; + + constructor(ms: number, f: () => void) { + this.f = f; + this.ms = ms; + } + + bump() { + this.stop(); + const handler = () => { + this.f(); + }; + this.timerId = window.setTimeout(handler, this.ms); + } + + stop() { + if (this.timerId != undefined) { + window.clearTimeout(this.timerId); + } + } +} + + +class Controller { + url = m.prop<string>(); + statusString: string | null = null; + isValidExchange = false; + reserveCreationInfo?: ReserveCreationInfo; + private timer: DelayTimer; + amount: AmountJson; + callbackUrl: string; + wtTypes: string[]; + detailCollapsed = m.prop<boolean>(true); + suggestedExchangeUrl: string; + complexViewRequested = false; + urlOkay = false; + + constructor(suggestedExchangeUrl: string, + amount: AmountJson, + callbackUrl: string, + wt_types: string[]) { + console.log("creating main controller"); + this.suggestedExchangeUrl = suggestedExchangeUrl; + this.amount = amount; + this.callbackUrl = callbackUrl; + this.wtTypes = wt_types; + this.timer = new DelayTimer(800, () => this.update()); + this.url(suggestedExchangeUrl); + this.update(); + } + + private update() { + this.timer.stop(); + const doUpdate = () => { + this.reserveCreationInfo = undefined; + if (!this.url()) { + this.statusString = i18n`Error: URL is empty`; + m.redraw(true); + return; + } + this.statusString = null; + let parsedUrl = URI(this.url()); + if (parsedUrl.is("relative")) { + this.statusString = i18n`Error: URL may not be relative`; + m.redraw(true); + return; + } + + m.redraw(true); + + console.log("doing get exchange info"); + + getReserveCreationInfo(this.url(), this.amount) + .then((r: ReserveCreationInfo) => { + console.log("get exchange info resolved"); + this.isValidExchange = true; + this.reserveCreationInfo = r; + console.dir(r); + m.endComputation(); + }) + .catch((e) => { + console.log("get exchange info rejected"); + if (e.hasOwnProperty("httpStatus")) { + this.statusString = `Error: request failed with status ${e.httpStatus}`; + } else if (e.hasOwnProperty("errorResponse")) { + let resp = e.errorResponse; + this.statusString = `Error: ${resp.error} (${resp.hint})`; + } + m.endComputation(); + }); + }; + + doUpdate(); + + console.log("got update", this.url()); + } + + reset() { + this.isValidExchange = false; + this.statusString = null; + this.reserveCreationInfo = undefined; + } + + confirmReserve(rci: ReserveCreationInfo, + exchange: string, + amount: AmountJson, + callback_url: string) { + const d = {exchange, amount}; + const cb = (rawResp: any) => { + if (!rawResp) { + throw Error("empty response"); + } + // FIXME: filter out types that bank/exchange don't have in common + let wire_details = rci.wireInfo; + if (!rawResp.error) { + const resp = CreateReserveResponse.checked(rawResp); + let q: {[name: string]: string|number} = { + wire_details: JSON.stringify(wire_details), + exchange: resp.exchange, + reserve_pub: resp.reservePub, + amount_value: amount.value, + amount_fraction: amount.fraction, + amount_currency: amount.currency, + }; + let url = URI(callback_url).addQuery(q); + if (!url.is("absolute")) { + throw Error("callback url is not absolute"); + } + console.log("going to", url.href()); + document.location.href = url.href(); + } else { + this.reset(); + this.statusString = ( + `Oops, something went wrong.` + + `The wallet responded with error status (${rawResp.error}).`); + } + }; + chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); + } + + onUrlChanged(url: string) { + this.reset(); + this.url(url); + this.timer.bump(); + } +} + +function view(ctrl: Controller): any { + function* f(): IterableIterator<any> { + yield m("p", + i18n.parts`You are about to withdraw ${m("strong", amountToPretty( + ctrl.amount))} from your bank account into your wallet.`); + + if (ctrl.complexViewRequested || !ctrl.suggestedExchangeUrl) { + yield viewComplex(ctrl); + return; + } + yield viewSimple(ctrl); + } + return Array.from(f()); +} + +function viewSimple(ctrl: Controller) { + function *f() { + if (ctrl.statusString) { + yield m("p", "Error: ", ctrl.statusString); + yield m("button.linky", { + onclick: () => { + ctrl.complexViewRequested = true; + } + }, "advanced options"); + } + else if (ctrl.reserveCreationInfo != undefined) { + let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead, + ctrl.reserveCreationInfo.withdrawFee).amount; + yield m("p", `Withdraw fees: ${amountToPretty(totalCost)}`); + + yield m("button.accept", { + onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo!, + ctrl.url(), + ctrl.amount, + ctrl.callbackUrl), + disabled: !ctrl.isValidExchange + }, + "Accept fees and withdraw"); + yield m("span.spacer"); + yield m("button.linky", { + onclick: () => { + ctrl.complexViewRequested = true; + } + }, "advanced options"); + } else { + yield m("p", "Please wait ..."); + } + } + + return Array.from(f()); +} + + +function viewComplex(ctrl: Controller) { + function *f() { + if (ctrl.reserveCreationInfo) { + let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead, + ctrl.reserveCreationInfo.withdrawFee).amount; + yield m("p", `Withdraw fees: ${amountToPretty(totalCost)}`); + } + + yield m("button.accept", { + onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo!, + ctrl.url(), + ctrl.amount, + ctrl.callbackUrl), + disabled: !ctrl.isValidExchange + }, + "Accept fees and withdraw"); + yield m("span.spacer"); + yield m("button.linky", { + onclick: () => { + ctrl.complexViewRequested = false; + } + }, "back to simple view"); + + yield m("br"); + + + yield m("input", { + className: "url", + type: "text", + spellcheck: false, + value: ctrl.url(), + oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), + }); + + yield m("br"); + + if (ctrl.statusString) { + yield m("p", ctrl.statusString); + } else if (!ctrl.reserveCreationInfo) { + yield m("p", "Checking URL, please wait ..."); + } + + if (ctrl.reserveCreationInfo) { + if (ctrl.detailCollapsed()) { + yield m("button.linky", { + onclick: () => { + ctrl.detailCollapsed(false); + } + }, "show more details"); + } else { + yield m("button.linky", { + onclick: () => { + ctrl.detailCollapsed(true); + } + }, "hide details"); + yield m("div", {}, renderReserveCreationDetails(ctrl.reserveCreationInfo)) + } + } + } + return Array.from(f()); +} + + +function renderReserveCreationDetails(rci: ReserveCreationInfo) { + let denoms = rci.selectedDenoms; + + let countByPub: {[s: string]: number} = {}; + let uniq: Denomination[] = []; + + denoms.forEach((x: Denomination) => { + let c = countByPub[x.denom_pub] || 0; + if (c == 0) { + uniq.push(x); + } + c += 1; + countByPub[x.denom_pub] = c; + }); + + function row(denom: Denomination) { + return m("tr", [ + m("td", countByPub[denom.denom_pub] + "x"), + m("td", amountToPretty(denom.value)), + m("td", amountToPretty(denom.fee_withdraw)), + m("td", amountToPretty(denom.fee_refresh)), + m("td", amountToPretty(denom.fee_deposit)), + ]); + } + + let withdrawFeeStr = amountToPretty(rci.withdrawFee); + let overheadStr = amountToPretty(rci.overhead); + return [ + m("p", `Withdrawal fees: ${withdrawFeeStr}`), + m("p", `Rounding loss: ${overheadStr}`), + m("table", [ + m("tr", [ + m("th", "Count"), + m("th", "Value"), + m("th", "Withdraw Fee"), + m("th", "Refresh Fee"), + m("th", "Deposit Fee"), + ]), + uniq.map(row) + ]) + ]; +} + + +function getSuggestedExchange(currency: string): Promise<string> { + // TODO: make this request go to the wallet backend + // Right now, this is a stub. + const defaultExchange: {[s: string]: string} = { + "KUDOS": "https://exchange.demo.taler.net", + "PUDOS": "https://exchange.test.taler.net", + }; + + let exchange = defaultExchange[currency]; + + if (!exchange) { + exchange = "" + } + + return Promise.resolve(exchange); +} + + + +export function main() { + const url = URI(document.location.href); + const query: any = URI.parseQuery(url.query()); + const amount = AmountJson.checked(JSON.parse(query.amount)); + const callback_url = query.callback_url; + const bank_url = query.bank_url; + const wt_types = JSON.parse(query.wt_types); + + getSuggestedExchange(amount.currency) + .then((suggestedExchangeUrl) => { + const controller = function () { return new Controller(suggestedExchangeUrl, amount, callback_url, wt_types); }; + const ExchangeSelection = {controller, view}; + m.mount(document.getElementById("exchange-selection")!, ExchangeSelection); + }) + .catch((e) => { + // TODO: provide more context information, maybe factor it out into a + // TODO:generic error reporting function or component. + document.body.innerText = `Fatal error: "${e.message}".`; + console.error(`got error "${e.message}"`, e); + }); +}
\ No newline at end of file diff --git a/pages/debug.html b/pages/debug.html new file mode 100644 index 000000000..221c7380c --- /dev/null +++ b/pages/debug.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> + <head> + <title>Taler Wallet Debugging</title> + <link rel="icon" href="../img/icon.png"> + </head> + <body> + <h1>Debug Pages</h1> + <a href="show-db.html">Show DB</a> <br> + <a href="../popup/balance-overview.html">Show balance</a> + + </body> +</html> diff --git a/pages/help/empty-wallet.html b/pages/help/empty-wallet.html new file mode 100644 index 000000000..952bd92b7 --- /dev/null +++ b/pages/help/empty-wallet.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>GNU Taler Help - Empty Wallet</title> + <link rel="icon" href="../../img/icon.png"> + <meta name="description" content=""> + <link rel="stylesheet" type="text/css" href="../../style/wallet.css"> + </head> + <body> + <div class="container" id="main"> + <div class="row"> + <div class="col-lg-12"> + <h2 lang="en">Your wallet is empty!</h2> + <p lang="en">You have succeeded with installing the Taler wallet. However, before + you can buy articles using the Taler wallet, you must withdraw electronic coins. + This is typically done by visiting your bank's online banking Web site. There, + you instruct your bank to transfer the funds to a Taler exchange operator. In + return, your wallet will be allowed to withdraw electronic coins.</p> + <p lang="en">At this stage, we are not aware of any regular exchange operators issuing + coins in well-known currencies. However, to see how Taler would work, you + can visit our "fake" bank at + <a href="https://bank.demo.taler.net/">bank.demo.taler.net</a> to + withdraw coins in the "KUDOS" currency that we created just for + demonstrating the system.</p> + </div> + </div> + </div> + </body> +</html> diff --git a/pages/show-db.html b/pages/show-db.html new file mode 100644 index 000000000..024e844ee --- /dev/null +++ b/pages/show-db.html @@ -0,0 +1,15 @@ + +<!doctype html> + +<html> + <head> + <title>Taler Wallet: Reserve Created</title> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + <link rel="icon" href="../img/icon.png"> + <script src="show-db.js"></script> + </head> + <body> + <h1>DB Dump</h1> + <pre id="dump"></pre> + </body> +</html> diff --git a/pages/show-db.ts b/pages/show-db.ts new file mode 100644 index 000000000..71e74388b --- /dev/null +++ b/pages/show-db.ts @@ -0,0 +1,57 @@ +/* + This file is part of TALER + (C) 2015 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/> + */ + + +/** + * Wallet database dump for debugging. + * + * @author Florian Dold + */ + +function replacer(match: string, pIndent: string, pKey: string, pVal: string, + pEnd: string) { + var key = '<span class=json-key>'; + var val = '<span class=json-value>'; + var str = '<span class=json-string>'; + var r = pIndent || ''; + if (pKey) { + r = r + key + pKey.replace(/[": ]/g, '') + '</span>: '; + } + if (pVal) { + r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>'; + } + return r + (pEnd || ''); +} + + +function prettyPrint(obj: any) { + var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg; + return JSON.stringify(obj, null as any, 3) + .replace(/&/g, '&').replace(/\\"/g, '"') + .replace(/</g, '<').replace(/>/g, '>') + .replace(jsonLine, replacer); +} + + +document.addEventListener("DOMContentLoaded", () => { + chrome.runtime.sendMessage({type: 'dump-db'}, (resp) => { + const el = document.getElementById('dump'); + if (!el) { + throw Error(); + } + el.innerHTML = prettyPrint(resp); + }); +}); |