wallet-core/pages/confirm-create-reserve.tsx

397 lines
11 KiB
TypeScript
Raw Normal View History

2015-12-25 22:42:14 +01:00
/*
This file is part of TALER
2016-01-26 17:21:17 +01:00
(C) 2015-2016 GNUnet e.V.
2015-12-25 22:42:14 +01:00
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
2016-07-07 17:59:29 +02:00
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
2015-12-25 22:42:14 +01:00
*/
2016-03-01 19:46:20 +01:00
/**
* Page shown to the user to confirm creation
* of a reserve, usually requested by the bank.
*
* @author Florian Dold
*/
2016-02-15 11:29:58 +01:00
/// <reference path="../lib/decl/mithril.d.ts" />
import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers";
import {AmountJson, CreateReserveResponse} from "../lib/wallet/types";
2016-02-15 11:29:58 +01:00
import m from "mithril";
2016-03-01 19:39:17 +01:00
import {IExchangeInfo} from "../lib/wallet/types";
import {ReserveCreationInfo, Amounts} from "../lib/wallet/types";
2016-02-18 22:50:17 +01:00
import MithrilComponent = _mithril.MithrilComponent;
import {Denomination} from "../lib/wallet/types";
2016-02-19 04:23:00 +01:00
import {getReserveCreationInfo} from "../lib/wallet/wxApi";
2015-12-20 20:34:20 +01:00
"use strict";
/**
* 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;
}
bump() {
2016-02-15 11:29:58 +01:00
this.stop();
const handler = () => {
this.f();
};
this.timerId = window.setTimeout(handler, this.ms);
2015-12-20 20:34:20 +01:00
}
2016-02-15 11:29:58 +01:00
stop() {
if (this.timerId !== null) {
window.clearTimeout(this.timerId);
}
}
}
2015-12-20 20:34:20 +01:00
2016-01-26 17:21:17 +01:00
class Controller {
2016-02-15 11:29:58 +01:00
url = m.prop<string>();
statusString = null;
2016-03-01 19:39:17 +01:00
isValidExchange = false;
2016-02-18 22:50:17 +01:00
reserveCreationInfo: ReserveCreationInfo = null;
private timer: DelayTimer;
private request: XMLHttpRequest;
2016-02-15 11:29:58 +01:00
amount: AmountJson;
callbackUrl: string;
wtTypes: string[];
2016-02-18 22:50:17 +01:00
detailCollapsed = m.prop<boolean>(true);
2016-04-27 06:50:38 +02:00
suggestedExchangeUrl: string;
complexViewRequested = false;
urlOkay = false;
2015-12-20 20:34:20 +01:00
2016-04-27 06:50:38 +02:00
constructor(suggestedExchangeUrl: string,
amount: AmountJson,
callbackUrl: string,
wt_types: string[]) {
2016-02-18 22:50:17 +01:00
console.log("creating main controller");
2016-04-27 06:50:38 +02:00
this.suggestedExchangeUrl = suggestedExchangeUrl;
2016-02-15 11:29:58 +01:00
this.amount = amount;
this.callbackUrl = callbackUrl;
this.wtTypes = wt_types;
this.timer = new DelayTimer(800, () => this.update());
2016-04-27 06:50:38 +02:00
this.url(suggestedExchangeUrl);
2016-02-15 11:29:58 +01:00
this.update();
}
2016-02-15 11:29:58 +01:00
private update() {
this.timer.stop();
const doUpdate = () => {
2016-04-27 06:50:38 +02:00
this.reserveCreationInfo = null;
2016-02-15 11:29:58 +01:00
if (!this.url()) {
2016-04-27 06:50:38 +02:00
this.statusString = i18n`Error: URL is empty`;
m.redraw(true);
return;
}
2016-02-15 11:29:58 +01:00
this.statusString = null;
let parsedUrl = URI(this.url());
if (parsedUrl.is("relative")) {
2016-04-27 06:50:38 +02:00
this.statusString = i18n`Error: URL may not be relative`;
m.redraw(true);
return;
}
2016-02-18 22:50:17 +01:00
m.redraw(true);
2016-03-01 19:39:17 +01:00
console.log("doing get exchange info");
2016-02-18 22:50:17 +01:00
getReserveCreationInfo(this.url(), this.amount)
.then((r: ReserveCreationInfo) => {
2016-03-01 19:39:17 +01:00
console.log("get exchange info resolved");
this.isValidExchange = true;
2016-02-18 22:50:17 +01:00
this.reserveCreationInfo = r;
console.dir(r);
m.endComputation();
})
.catch((e) => {
2016-03-01 19:39:17 +01:00
console.log("get exchange info rejected");
2016-02-18 22:50:17 +01:00
if (e.hasOwnProperty("httpStatus")) {
2016-04-27 06:50:38 +02:00
this.statusString = `Error: request failed with status ${this.request.status}`;
} else if (e.hasOwnProperty("errorResponse")) {
let resp = e.errorResponse;
2016-04-27 06:50:38 +02:00
this.statusString = `Error: ${resp.error} (${resp.hint})`;
}
2016-02-18 22:50:17 +01:00
m.endComputation();
});
2016-02-09 21:56:06 +01:00
};
doUpdate();
2016-02-15 11:29:58 +01:00
2016-04-27 06:50:38 +02:00
console.log("got update", this.url());
}
2015-12-20 20:34:20 +01:00
reset() {
2016-03-01 19:39:17 +01:00
this.isValidExchange = false;
2016-02-15 11:29:58 +01:00
this.statusString = null;
2016-02-18 22:50:17 +01:00
this.reserveCreationInfo = null;
if (this.request) {
this.request.abort();
this.request = null;
2016-02-09 21:56:06 +01:00
}
}
2016-02-09 21:56:06 +01:00
confirmReserve(rci: ReserveCreationInfo,
exchange: string,
amount: AmountJson,
callback_url: string) {
2016-03-01 19:39:17 +01:00
const d = {exchange, amount};
2016-02-09 21:56:06 +01:00
const cb = (rawResp) => {
if (!rawResp) {
throw Error("empty response");
}
// FIXME: filter out types that bank/exchange don't have in common
let wire_details = rci.wireInfo;
2016-02-09 21:56:06 +01:00
if (!rawResp.error) {
const resp = CreateReserveResponse.checked(rawResp);
let q: {[name: string]: string|number} = {
wire_details: JSON.stringify(wire_details),
2016-03-01 19:39:17 +01:00
exchange: resp.exchange,
2016-02-09 21:56:06 +01:00
reserve_pub: resp.reservePub,
amount_value: amount.value,
amount_fraction: amount.fraction,
amount_currency: amount.currency,
2016-02-09 21:56:06 +01:00
};
let url = URI(callback_url).addQuery(q);
2016-02-09 21:56:06 +01:00
if (!url.is("absolute")) {
throw Error("callback url is not absolute");
}
console.log("going to", url.href());
2016-02-09 21:56:06 +01:00
document.location.href = url.href();
2016-01-26 17:21:17 +01:00
} else {
this.reset();
2016-02-15 11:29:58 +01:00
this.statusString = (
`Oops, something went wrong.` +
`The wallet responded with error status (${rawResp.error}).`);
2016-01-26 17:21:17 +01:00
}
};
2016-02-09 21:56:06 +01:00
chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
}
onUrlChanged(url: string) {
this.reset();
2016-02-15 11:29:58 +01:00
this.url(url);
this.timer.bump();
}
}
2016-04-27 06:50:38 +02:00
function view(ctrl: Controller): any {
2016-02-15 11:29:58 +01:00
let controls = [];
2016-02-18 22:50:17 +01:00
let mx = (x, ...args) => controls.push(m(x, ...args));
2016-02-15 11:29:58 +01:00
mx("p",
2016-04-23 19:34:30 +02:00
i18n.parts`You are about to withdraw ${m("strong", amountToPretty(
ctrl.amount))} from your bank account into your wallet.`);
2016-04-27 06:50:38 +02:00
if (ctrl.complexViewRequested || !ctrl.suggestedExchangeUrl) {
return controls.concat(viewComplex(ctrl));
}
return controls.concat(viewSimple(ctrl));
}
function viewSimple(ctrl: Controller) {
let controls = [];
let mx = (x, ...args) => controls.push(m(x, ...args));
if (ctrl.statusString) {
mx("p", "Error: ", ctrl.statusString);
mx("button.linky", {
onclick: () => {
ctrl.complexViewRequested = true;
}
}, "advanced options");
}
else if (ctrl.reserveCreationInfo) {
mx("button.accept", {
onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo,
ctrl.url(),
ctrl.amount,
ctrl.callbackUrl),
disabled: !ctrl.isValidExchange
},
"Accept fees and withdraw");
mx("span.spacer");
mx("button.linky", {
onclick: () => {
ctrl.complexViewRequested = true;
}
}, "advanced options");
let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead,
ctrl.reserveCreationInfo.withdrawFee).amount;
mx("p", `Withdraw cost: ${amountToPretty(totalCost)}`);
} else {
mx("p", "Please wait ...");
}
2016-02-15 11:29:58 +01:00
2016-04-27 06:50:38 +02:00
return controls;
}
function viewComplex(ctrl: Controller) {
let controls = [];
let mx = (x, ...args) => controls.push(m(x, ...args));
2016-04-23 19:34:30 +02:00
mx("button.accept", {
onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo,
ctrl.url(),
2016-02-15 11:29:58 +01:00
ctrl.amount,
ctrl.callbackUrl),
2016-03-01 19:39:17 +01:00
disabled: !ctrl.isValidExchange
2016-02-15 11:29:58 +01:00
},
2016-04-23 19:34:30 +02:00
"Accept fees and withdraw");
2016-04-27 06:50:38 +02:00
mx("span.spacer");
mx("button.linky", {
onclick: () => {
ctrl.complexViewRequested = false;
}
}, "back to simple view");
mx("br");
mx("input",
{
className: "url",
type: "text",
spellcheck: false,
value: ctrl.url(),
oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)),
});
mx("br");
2016-02-15 11:29:58 +01:00
if (ctrl.statusString) {
mx("p", ctrl.statusString);
2016-04-27 06:50:38 +02:00
} else if (!ctrl.reserveCreationInfo) {
2016-02-15 11:29:58 +01:00
mx("p", "Checking URL, please wait ...");
}
2016-02-18 22:50:17 +01:00
if (ctrl.reserveCreationInfo) {
let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead,
ctrl.reserveCreationInfo.withdrawFee).amount;
mx("p", `Withdraw cost: ${amountToPretty(totalCost)}`);
2016-02-18 22:50:17 +01:00
if (ctrl.detailCollapsed()) {
mx("button.linky", {
onclick: () => {
ctrl.detailCollapsed(false);
}
}, "show more details");
2016-02-18 22:50:17 +01:00
} else {
mx("button.linky", {
onclick: () => {
ctrl.detailCollapsed(true);
}
}, "hide details");
mx("div", {}, renderReserveCreationDetails(ctrl.reserveCreationInfo))
2016-02-18 22:50:17 +01:00
}
}
2016-02-15 11:29:58 +01:00
return m("div", controls);
}
function renderReserveCreationDetails(rci: ReserveCreationInfo) {
let denoms = rci.selectedDenoms;
2016-04-27 06:03:04 +02:00
let countByPub = {};
let uniq = [];
denoms.forEach((x: Denomination) => {
let c = countByPub[x.denom_pub] || 0;
if (c == 0) {
uniq.push(x);
}
c += 1;
countByPub[x.denom_pub] = c;
});
2016-02-18 22:50:17 +01:00
function row(denom: Denomination) {
return m("tr", [
2016-04-27 06:03:04 +02:00
m("td", countByPub[denom.denom_pub] + "x"),
2016-02-18 22:50:17 +01:00
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", `Fee for withdrawal: ${withdrawFeeStr}`),
m("p", `Overhead: ${overheadStr}`),
m("table", [
m("tr", [
2016-04-27 06:03:04 +02:00
m("th", "Count"),
m("th", "Value"),
m("th", "Withdraw Fee"),
m("th", "Refresh Fee"),
m("th", "Deposit Fee"),
]),
2016-04-27 06:03:04 +02:00
uniq.map(row)
])
];
2016-02-18 22:50:17 +01:00
}
2016-03-01 19:39:17 +01:00
function getSuggestedExchange(currency: string): Promise<string> {
2016-02-15 11:29:58 +01:00
// TODO: make this request go to the wallet backend
// Right now, this is a stub.
2016-03-01 19:39:17 +01:00
const defaultExchange = {
2016-03-05 12:17:46 +01:00
"KUDOS": "https://exchange.demo.taler.net",
"PUDOS": "https://exchange.test.taler.net",
};
2016-03-01 19:39:17 +01:00
let exchange = defaultExchange[currency];
2016-02-15 11:29:58 +01:00
2016-03-01 19:39:17 +01:00
if (!exchange) {
exchange = ""
2016-02-15 11:29:58 +01:00
}
2016-03-01 19:39:17 +01:00
return Promise.resolve(exchange);
2016-02-15 11:29:58 +01:00
}
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);
2016-02-15 11:29:58 +01:00
2016-03-01 19:39:17 +01:00
getSuggestedExchange(amount.currency)
.then((suggestedExchangeUrl) => {
const controller = () => new Controller(suggestedExchangeUrl, amount, callback_url, wt_types);
2016-03-01 19:39:17 +01:00
var ExchangeSelection = {controller, view};
m.mount(document.getElementById("exchange-selection"), ExchangeSelection);
2016-02-15 11:29:58 +01:00
})
.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 backend error "${e.message}"`);
});
2016-02-29 03:26:51 +01:00
}