diff --git a/extension/lib/wallet/emscriptif.ts b/extension/lib/wallet/emscriptif.ts index 16c883451..b11d845f0 100644 --- a/extension/lib/wallet/emscriptif.ts +++ b/extension/lib/wallet/emscriptif.ts @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, If not, see */ -import {AmountJson} from "./wallet"; +import {AmountJson} from "./types"; import * as EmscWrapper from "../emscripten/emsc"; /** diff --git a/extension/lib/web-common.ts b/extension/lib/wallet/helpers.ts similarity index 57% rename from extension/lib/web-common.ts rename to extension/lib/wallet/helpers.ts index 79ff4b13e..99913e558 100644 --- a/extension/lib/web-common.ts +++ b/extension/lib/wallet/helpers.ts @@ -14,7 +14,13 @@ TALER; see the file COPYING. If not, If not, see */ -import {AmountJson} from "./wallet/wallet"; + +/** + * Smaller helper functions that do not depend + * on the emscripten machinery. + */ + +import {AmountJson} from "./types"; export function substituteFulfillmentUrl(url: string, vars) { url = url.replace("${H_contract}", vars.H_contract); @@ -22,7 +28,38 @@ export function substituteFulfillmentUrl(url: string, vars) { return url; } + export function amountToPretty(amount: AmountJson): string { let x = amount.value + amount.fraction / 1e6; return `${x} ${amount.currency}`; +} + + +/** + * Canonicalize a base url, typically for the mint. + * + * See http://api.taler.net/wallet.html#general + */ +export function canonicalizeBaseUrl(url) { + let x = new URI(url); + if (!x.protocol()) { + x.protocol("https"); + } + x.path(x.path() + "/").normalizePath(); + x.fragment(); + x.query(); + return x.href() +} + + +export function parsePrettyAmount(pretty: string): AmountJson { + const res = /([0-9]+)(.[0-9]+)?\s*(\w+)/.exec(pretty); + if (!res) { + return null; + } + return { + value: parseInt(res[1], 10), + fraction: res[2] ? (parseFloat(`0.${res[2]}`) * 1e-6) : 0, + currency: res[3] + } } \ No newline at end of file diff --git a/extension/lib/wallet/types.ts b/extension/lib/wallet/types.ts new file mode 100644 index 000000000..197aed938 --- /dev/null +++ b/extension/lib/wallet/types.ts @@ -0,0 +1,56 @@ +/* + 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, If not, see + */ + +/** + * Common types that are used by Taler. + * + * Note most types are defined in wallet.ts, types that + * are defined in types.ts are intended to be used by components + * that do not depend on the whole wallet implementation (which depends on + * emscripten). + */ + +import {Checkable} from "./checkable"; + +@Checkable.Class +export class AmountJson { + @Checkable.Number + value: number; + + @Checkable.Number + fraction: number; + + @Checkable.String + currency: string; + + static checked: (obj: any) => AmountJson; +} + + +@Checkable.Class +export class CreateReserveResponse { + /** + * Mint URL where the bank should create the reserve. + * The URL is canonicalized in the response. + */ + @Checkable.String + mint: string; + + @Checkable.String + reservePub: string; + + static checked: (obj: any) => CreateReserveResponse; +} \ No newline at end of file diff --git a/extension/lib/wallet/wallet.ts b/extension/lib/wallet/wallet.ts index 0a9fbe191..f94e9c87e 100644 --- a/extension/lib/wallet/wallet.ts +++ b/extension/lib/wallet/wallet.ts @@ -22,9 +22,11 @@ */ import * as native from "./emscriptif"; +import {AmountJson, CreateReserveResponse} from "./types"; import {HttpResponse, RequestException} from "./http"; import {Query} from "./query"; import {Checkable} from "./checkable"; +import {canonicalizeBaseUrl} from "./helpers"; "use strict"; @@ -73,21 +75,6 @@ export interface Coin { } -@Checkable.Class -export class AmountJson { - @Checkable.Number - value: number; - - @Checkable.Number - fraction: number; - - @Checkable.String - currency: string; - - static checked: (obj: any) => AmountJson; -} - - @Checkable.Class export class CreateReserveRequest { /** @@ -106,22 +93,6 @@ export class CreateReserveRequest { } -@Checkable.Class -export class CreateReserveResponse { - /** - * Mint URL where the bank should create the reserve. - * The URL is canonicalized in the response. - */ - @Checkable.String - mint: string; - - @Checkable.String - reservePub: string; - - static checked: (obj: any) => CreateReserveResponse; -} - - @Checkable.Class export class ConfirmReserveRequest { /** @@ -270,34 +241,6 @@ function isWithdrawableDenom(d: Denomination) { } -/** - * See http://api.taler.net/wallet.html#general - */ -function canonicalizeBaseUrl(url) { - let x = new URI(url); - if (!x.protocol()) { - x.protocol("https"); - } - x.path(x.path() + "/").normalizePath(); - x.fragment(); - x.query(); - return x.href() -} - - -function parsePrettyAmount(pretty: string): AmountJson { - const res = /([0-9]+)(.[0-9]+)?\s*(\w+)/.exec(pretty); - if (!res) { - return null; - } - return { - value: parseInt(res[1], 10), - fraction: res[2] ? (parseFloat(`0.${res[2]}`) * 1e-6) : 0, - currency: res[3] - } -} - - interface HttpRequestLibrary { req(method: string, url: string|uri.URI, @@ -619,7 +562,7 @@ export class Wallet { reservePub: reserveRecord.reserve_pub, } }; - + return Query(this.db) .put("reserves", reserveRecord) .put("history", historyEntry) diff --git a/extension/pages/confirm-contract.js b/extension/pages/confirm-contract.js index d715985b5..5e7d82f98 100644 --- a/extension/pages/confirm-contract.js +++ b/extension/pages/confirm-contract.js @@ -13,11 +13,11 @@ You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, If not, see */ -System.register(["../lib/web-common"], function(exports_1, context_1) { +System.register(["../lib/wallet/helpers"], function(exports_1, context_1) { /// "use strict"; var __moduleName = context_1 && context_1.id; - var web_common_1; + var helpers_1; function prettyAmount(amount) { var v = amount.value + amount.fraction / 1e6; return v.toFixed(2) + " " + amount.currency; @@ -55,15 +55,15 @@ System.register(["../lib/web-common"], function(exports_1, context_1) { } var c = d.offer.contract; console.log("contract", c); - document.location.href = web_common_1.substituteFulfillmentUrl(c.fulfillment_url, offer); + document.location.href = helpers_1.substituteFulfillmentUrl(c.fulfillment_url, offer); }); } } exports_1("main", main); return { setters:[ - function (web_common_1_1) { - web_common_1 = web_common_1_1; + function (helpers_1_1) { + helpers_1 = helpers_1_1; }], execute: function() { } diff --git a/extension/pages/confirm-contract.tsx b/extension/pages/confirm-contract.tsx index 35d050c19..055490175 100644 --- a/extension/pages/confirm-contract.tsx +++ b/extension/pages/confirm-contract.tsx @@ -17,7 +17,7 @@ /// "use strict"; -import {substituteFulfillmentUrl} from "../lib/web-common"; +import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; declare var m: any; diff --git a/extension/pages/confirm-create-reserve.html b/extension/pages/confirm-create-reserve.html index 7af54a828..522efc872 100644 --- a/extension/pages/confirm-create-reserve.html +++ b/extension/pages/confirm-create-reserve.html @@ -4,6 +4,8 @@ Taler Wallet: Select Taler Provider + + @@ -22,27 +24,7 @@
-

- You asked to withdraw (loading...) from your - bank account. -

-

- Please specify the base URL of the Taler mint you want to use. The Taler - mint will process the payments, possibly for a fee. The mint underwrites - electronic coins and will hold matching funds in reserve in its bank - account. Mints are expected to be regularly audited by a trusted party to - ensure that they have sufficient reserves to cover all outstanding - obligations. -

- -
-
- - -
- -
+
diff --git a/extension/pages/confirm-create-reserve.js b/extension/pages/confirm-create-reserve.js index 3ac757249..124abdf22 100644 --- a/extension/pages/confirm-create-reserve.js +++ b/extension/pages/confirm-create-reserve.js @@ -13,69 +13,176 @@ You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, If not, see */ -System.register(["../lib/web-common", "../lib/wallet/wallet"], function(exports_1, context_1) { +System.register(["../lib/wallet/helpers", "../lib/wallet/types"], function(exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; - var web_common_1, wallet_1; + var helpers_1, types_1; + var DelayTimer, Controller; function main() { - function updateAmount() { - var showAmount = document.getElementById("show-amount"); - console.log("Query is " + JSON.stringify(query)); - var amount = wallet_1.AmountJson.checked(JSON.parse(query.amount)); - showAmount.textContent = web_common_1.amountToPretty(amount); - } var url = URI(document.location.href); var query = URI.parseQuery(url.query()); - updateAmount(); - document.getElementById("confirm").addEventListener("click", function (e) { - var d = { - mint: document.getElementById('mint-url').value, - amount: JSON.parse(query.amount) - }; - if (!d.mint) { - // FIXME: indicate error instead! - throw Error("mint missing"); - } - if (!d.amount) { - // FIXME: indicate error instead! - throw Error("amount missing"); - } - var cb = function (rawResp) { - if (!rawResp) { - throw Error("empty response"); - } - if (!rawResp.error) { - var resp = wallet_1.CreateReserveResponse.checked(rawResp); - var q = { - mint: resp.mint, - reserve_pub: resp.reservePub, - amount: query.amount, - }; - var url_1 = URI(query.callback_url).addQuery(q); - if (!url_1.is("absolute")) { - throw Error("callback url is not absolute"); + var amount = types_1.AmountJson.checked(JSON.parse(query.amount)); + var callback_url = query.callback_url; + var MintSelection = { + controller: function () { return new Controller(); }, + view: function (ctrl) { + var controls = []; + var mx = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i - 0] = arguments[_i]; } - document.location.href = url_1.href(); + return controls.push(m.apply(void 0, args)); + }; + mx("p", (_a = ["The bank wants to create a reserve over ", "."], _a.raw = ["The bank wants to create a reserve over ", "."], i18n(_a, helpers_1.amountToPretty(amount)))); + mx("input.url", { + type: "text", + spellcheck: false, + oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), + }); + if (ctrl.isValidMint) { + mx("button", { + onclick: function () { return ctrl.confirmReserve(ctrl.url, amount, callback_url); } + }, "Confirm mint selection"); } - else { - document.body.innerHTML = - "Oops, something went wrong. It looks like the bank could not\n transfer funds to the mint. Please go back to your bank's website\n to check what happened."; + if (ctrl.errorString) { + mx("p", ctrl.errorString); } - }; - chrome.runtime.sendMessage({ type: 'create-reserve', detail: d }, cb); - }); + return m("div", controls); + var _a; + } + }; + m.mount(document.getElementById("mint-selection"), MintSelection); } exports_1("main", main); return { setters:[ - function (web_common_1_1) { - web_common_1 = web_common_1_1; + function (helpers_1_1) { + helpers_1 = helpers_1_1; }, - function (wallet_1_1) { - wallet_1 = wallet_1_1; + function (types_1_1) { + types_1 = types_1_1; }], execute: function() { "use strict"; + /** + * Execute something after a delay, with the possibility + * to reset the delay. + */ + DelayTimer = (function () { + function DelayTimer(ms, f) { + this.timerId = null; + this.f = f; + this.ms = ms; + } + DelayTimer.prototype.bump = function () { + var _this = this; + if (this.timerId !== null) { + window.clearTimeout(this.timerId); + } + var handler = function () { + _this.f(); + }; + this.timerId = window.setTimeout(handler, this.ms); + }; + return DelayTimer; + }()); + Controller = (function () { + function Controller() { + var _this = this; + this.url = null; + this.errorString = null; + this.isValidMint = false; + this.update(); + this.timer = new DelayTimer(800, function () { return _this.update(); }); + } + Controller.prototype.update = function () { + var _this = this; + var doUpdate = function () { + if (!_this.url) { + _this.errorString = (_a = ["Please enter a URL"], _a.raw = ["Please enter a URL"], i18n(_a)); + return; + } + _this.errorString = null; + var parsedUrl = URI(_this.url); + if (parsedUrl.is("relative")) { + _this.errorString = (_b = ["The URL you've entered is not valid (must be absolute)"], _b.raw = ["The URL you've entered is not valid (must be absolute)"], i18n(_b)); + return; + } + var keysUrl = URI("/keys").absoluteTo(helpers_1.canonicalizeBaseUrl(_this.url)); + console.log("requesting keys from '" + keysUrl + "'"); + _this.request = new XMLHttpRequest(); + _this.request.onreadystatechange = function () { + if (_this.request.readyState == XMLHttpRequest.DONE) { + switch (_this.request.status) { + case 200: + _this.isValidMint = true; + break; + case 0: + _this.errorString = "unknown request error"; + break; + default: + _this.errorString = "request failed with status " + _this.request.status; + break; + } + m.redraw(); + } + }; + _this.request.open("get", keysUrl.href()); + _this.request.send(); + var _a, _b; + }; + doUpdate(); + m.redraw(); + console.log("got update"); + }; + Controller.prototype.reset = function () { + this.isValidMint = false; + this.errorString = null; + if (this.request) { + this.request.abort(); + this.request = null; + } + m.redraw(); + }; + Controller.prototype.confirmReserve = function (mint, amount, callback_url) { + var _this = this; + var d = { mint: mint, amount: amount }; + var cb = function (rawResp) { + if (!rawResp) { + throw Error("empty response"); + } + if (!rawResp.error) { + var resp = types_1.CreateReserveResponse.checked(rawResp); + var q = { + mint: resp.mint, + reserve_pub: resp.reservePub, + amount_value: amount.value, + amount_fraction: amount.fraction, + amount_currency: amount.currency, + }; + var 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.errorString = ("Oops, something went wrong." + + ("The wallet responded with error status (" + rawResp.error + ").")); + } + }; + chrome.runtime.sendMessage({ type: 'create-reserve', detail: d }, cb); + }; + Controller.prototype.onUrlChanged = function (url) { + this.reset(); + this.url = url; + this.timer.bump(); + }; + return Controller; + }()); } } }); diff --git a/extension/pages/confirm-create-reserve.tsx b/extension/pages/confirm-create-reserve.tsx index 6a130eeff..8cb46559b 100644 --- a/extension/pages/confirm-create-reserve.tsx +++ b/extension/pages/confirm-create-reserve.tsx @@ -14,40 +14,107 @@ TALER; see the file COPYING. If not, If not, see */ -import {amountToPretty} from "../lib/web-common"; -import {AmountJson, CreateReserveResponse} from "../lib/wallet/wallet"; +import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers"; +import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; + "use strict"; +declare var m: any; -export function main() { - function updateAmount() { - let showAmount = document.getElementById("show-amount"); - console.log("Query is " + JSON.stringify(query)); - let amount = AmountJson.checked(JSON.parse(query.amount)); - showAmount.textContent = amountToPretty(amount); + +/** + * Execute something after a delay, with the possibility + * to reset the delay. + */ +class DelayTimer { + ms: number; + f; + timerId: number = null; + + constructor(ms: number, f) { + this.f = f; + this.ms = ms; } - let url = URI(document.location.href); - let query: any = URI.parseQuery(url.query()); + bump() { + if (this.timerId !== null) { + window.clearTimeout(this.timerId); + } + const handler = () => { + this.f(); + }; + this.timerId = window.setTimeout(handler, this.ms); + } +} - updateAmount(); - document.getElementById("confirm").addEventListener("click", (e) => { - const d = { - mint: (document.getElementById('mint-url') as HTMLInputElement).value, - amount: JSON.parse(query.amount) +class Controller { + url = null; + errorString = null; + isValidMint = false; + private timer: DelayTimer; + private request: XMLHttpRequest; + + constructor() { + this.update(); + this.timer = new DelayTimer(800, () => this.update()); + } + + update() { + const doUpdate = () => { + if (!this.url) { + this.errorString = i18n`Please enter a URL`; + return; + } + this.errorString = null; + let parsedUrl = URI(this.url); + if (parsedUrl.is("relative")) { + this.errorString = i18n`The URL you've entered is not valid (must be absolute)`; + return; + } + + const keysUrl = URI("/keys").absoluteTo(canonicalizeBaseUrl(this.url)); + + console.log(`requesting keys from '${keysUrl}'`); + + this.request = new XMLHttpRequest(); + this.request.onreadystatechange = () => { + if (this.request.readyState == XMLHttpRequest.DONE) { + switch (this.request.status) { + case 200: + this.isValidMint = true; + break; + case 0: + this.errorString = `unknown request error`; + break; + default: + this.errorString = `request failed with status ${this.request.status}`; + break; + } + m.redraw(); + } + }; + this.request.open("get", keysUrl.href()); + this.request.send(); }; - if (!d.mint) { - // FIXME: indicate error instead! - throw Error("mint missing"); - } + doUpdate(); + m.redraw(); + console.log("got update"); + } - if (!d.amount) { - // FIXME: indicate error instead! - throw Error("amount missing"); + reset() { + this.isValidMint = false; + this.errorString = null; + if (this.request) { + this.request.abort(); + this.request = null; } + m.redraw(); + } + confirmReserve(mint: string, amount: AmountJson, callback_url: string) { + const d = {mint, amount}; const cb = (rawResp) => { if (!rawResp) { throw Error("empty response"); @@ -57,20 +124,72 @@ export function main() { let q = { mint: resp.mint, reserve_pub: resp.reservePub, - amount: query.amount, + amount_value: amount.value, + amount_fraction: amount.fraction, + amount_currency: amount.currency, }; - let url = URI(query.callback_url).addQuery(q); + 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 { - document.body.innerHTML = - `Oops, something went wrong. It looks like the bank could not - transfer funds to the mint. Please go back to your bank's website - to check what happened.`; + this.reset(); + this.errorString = ( + `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(); + } +} + + +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; + + var MintSelection = { + controller: () => new Controller(), + view(ctrl: Controller) { + let controls = []; + let mx = (...args) => controls.push(m(...args)); + + mx("p", + i18n`The bank wants to create a reserve over ${amountToPretty( + amount)}.`); + mx("input.url", + { + type: "text", + spellcheck: false, + oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)), + }); + + if (ctrl.isValidMint) { + mx("button", { + onclick: () => ctrl.confirmReserve(ctrl.url, + amount, + callback_url) + }, + "Confirm mint selection"); + } + + if (ctrl.errorString) { + mx("p", ctrl.errorString); + } + + return m("div", controls); + } + }; + + m.mount(document.getElementById("mint-selection"), MintSelection); } \ No newline at end of file diff --git a/extension/popup/popup.tsx b/extension/popup/popup.tsx index ebd77dd2e..97ea438bd 100644 --- a/extension/popup/popup.tsx +++ b/extension/popup/popup.tsx @@ -20,7 +20,7 @@ "use strict"; -import {substituteFulfillmentUrl} from "../lib/web-common"; +import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; declare var m: any; declare var i18n: any; diff --git a/extension/tsconfig.json b/extension/tsconfig.json index 5c2883da4..69fab261d 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -12,12 +12,13 @@ "files": [ "lib/i18n.ts", "lib/refs.ts", - "lib/web-common.ts", "lib/wallet/checkable.ts", "lib/wallet/db.ts", "lib/wallet/emscriptif.ts", + "lib/wallet/helpers.ts", "lib/wallet/http.ts", "lib/wallet/query.ts", + "lib/wallet/types.ts", "lib/wallet/wallet.ts", "lib/wallet/wxmessaging.ts", "background/main.ts",