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

421 lines
12 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-11-13 23:30:18 +01:00
import {amountToPretty, canonicalizeBaseUrl} from "src/helpers";
import {
AmountJson, CreateReserveResponse,
ReserveCreationInfo, Amounts,
Denomination, DenominationRecord,
2016-11-13 23:30:18 +01:00
} from "src/types";
import {getReserveCreationInfo} from "src/wxApi";
import {ImplicitStateComponent, StateHolder} from "src/components";
2016-11-27 22:13:24 +01:00
import * as i18n from "src/i18n";
2015-12-20 20:34:20 +01:00
"use strict";
2016-10-07 14:34:31 +02:00
2016-10-07 17:10:22 +02:00
function delay<T>(delayMs: number, value: T): Promise<T> {
return new Promise<T>((resolve, reject) => {
setTimeout(() => resolve(value), delayMs);
});
}
class EventTrigger {
triggerResolve: any;
triggerPromise: Promise<boolean>;
constructor() {
this.reset();
}
2016-10-07 17:10:22 +02:00
private reset() {
this.triggerPromise = new Promise<boolean>((resolve, reject) => {
this.triggerResolve = resolve;
});
2015-12-20 20:34:20 +01:00
}
2016-02-15 11:29:58 +01:00
2016-10-07 17:10:22 +02:00
trigger() {
this.triggerResolve(false);
this.reset();
}
async wait(delayMs: number): Promise<boolean> {
return await Promise.race([this.triggerPromise, delay(delayMs, true)]);
2016-02-15 11:29:58 +01:00
}
}
2015-12-20 20:34:20 +01:00
2016-10-07 17:10:22 +02:00
function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
if (!rci) {
2016-10-10 03:32:18 +02:00
return <p>
Details will be displayed when a valid exchange provider URL is entered.</p>
2016-10-07 17:10:22 +02:00
}
2016-10-07 14:34:31 +02:00
let denoms = rci.selectedDenoms;
let countByPub: {[s: string]: number} = {};
let uniq: DenominationRecord[] = [];
2016-10-07 14:34:31 +02:00
denoms.forEach((x: DenominationRecord) => {
let c = countByPub[x.denomPub] || 0;
2016-10-07 14:34:31 +02:00
if (c == 0) {
uniq.push(x);
}
c += 1;
countByPub[x.denomPub] = c;
2016-10-07 14:34:31 +02:00
});
function row(denom: DenominationRecord) {
2016-10-07 14:34:31 +02:00
return (
<tr>
<td>{countByPub[denom.denomPub] + "x"}</td>
2016-10-07 14:34:31 +02:00
<td>{amountToPretty(denom.value)}</td>
<td>{amountToPretty(denom.feeWithdraw)}</td>
<td>{amountToPretty(denom.feeRefresh)}</td>
<td>{amountToPretty(denom.feeDeposit)}</td>
2016-10-07 14:34:31 +02:00
</tr>
);
}
let withdrawFeeStr = amountToPretty(rci.withdrawFee);
let overheadStr = amountToPretty(rci.overhead);
return (
<div>
2016-11-27 22:13:24 +01:00
<p>{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}</p>
<p>{i18n.str`Rounding loss: ${overheadStr}`}</p>
2016-10-07 14:34:31 +02:00
<table>
<thead>
2016-11-27 22:13:24 +01:00
<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>
2016-10-07 14:34:31 +02:00
</thead>
<tbody>
{uniq.map(row)}
</tbody>
</table>
</div>
);
}
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);
}
2016-10-07 17:10:22 +02:00
function WithdrawFee(props: {reserveCreationInfo: ReserveCreationInfo|null}): JSX.Element {
if (props.reserveCreationInfo) {
let {overhead, withdrawFee} = props.reserveCreationInfo;
let totalCost = Amounts.add(overhead, withdrawFee).amount;
2016-11-27 22:13:24 +01:00
return <p>{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}</p>;
2016-10-07 17:10:22 +02:00
}
return <p />;
}
2016-10-07 14:34:31 +02:00
interface ExchangeSelectionProps {
2016-04-27 06:50:38 +02:00
suggestedExchangeUrl: string;
2016-10-07 14:34:31 +02:00
amount: AmountJson;
callback_url: string;
wt_types: string[];
}
class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
statusString: StateHolder<string|null> = this.makeState(null);
reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
null);
url: StateHolder<string|null> = this.makeState(null);
detailCollapsed: StateHolder<boolean> = this.makeState(true);
2016-10-07 17:10:22 +02:00
updateEvent = new EventTrigger();
2016-10-07 14:34:31 +02:00
constructor(props: ExchangeSelectionProps) {
super(props);
2016-10-07 17:10:22 +02:00
this.onUrlChanged(props.suggestedExchangeUrl || null);
2016-11-23 16:50:30 +01:00
this.forceReserveUpdate();
}
2016-10-07 14:34:31 +02:00
2016-10-07 17:10:22 +02:00
renderAdvanced(): JSX.Element {
2016-10-13 20:02:42 +02:00
if (this.detailCollapsed() && this.url() !== null && !this.statusString()) {
2016-10-07 14:34:31 +02:00
return (
2016-10-07 17:10:22 +02:00
<button className="linky"
onClick={() => this.detailCollapsed(false)}>
2016-11-27 22:13:24 +01:00
{i18n.str`view fee structure / select different exchange provider`}
2016-10-07 17:10:22 +02:00
</button>
);
2016-10-07 14:34:31 +02:00
}
return (
<div>
2016-10-07 17:10:22 +02:00
<h2>Provider Selection</h2>
<label>URL: </label>
<input className="url" type="text" spellCheck={false}
value={this.url()!}
key="exchange-url-input"
onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/>
<br />
{this.renderStatus()}
2016-11-27 22:13:24 +01:00
<h2>{i18n.str`Detailed Fee Structure`}</h2>
2016-10-07 17:10:22 +02:00
{renderReserveCreationDetails(this.reserveCreationInfo())}
</div>)
2016-10-07 14:34:31 +02:00
}
2016-10-07 17:10:22 +02:00
renderFee() {
if (!this.reserveCreationInfo()) {
return "??";
}
let rci = this.reserveCreationInfo()!;
let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
return `${amountToPretty(totalCost)}`;
}
2016-10-07 14:34:31 +02:00
2016-10-10 03:32:18 +02:00
renderFeeStatus() {
if (this.reserveCreationInfo()) {
return (
2016-11-23 01:14:45 +01:00
<i18n.Translate wrap="p">
2016-10-10 03:32:18 +02:00
The exchange provider will charge
{" "}
2016-11-23 01:14:45 +01:00
<span>{this.renderFee()}</span>
2016-10-10 03:32:18 +02:00
{" "}
in fees.
2016-11-23 01:14:45 +01:00
</i18n.Translate>
2016-10-10 03:32:18 +02:00
);
}
if (this.url() && !this.statusString()) {
let shortName = URI(this.url()!).host();
2016-11-23 01:14:45 +01:00
return (
<i18n.Translate wrap="p">
Waiting for a response from
{" "}
<em>{shortName}</em>
</i18n.Translate>
);
2016-10-10 03:32:18 +02:00
}
2016-10-13 20:02:42 +02:00
if (this.statusString()) {
return (
<p>
2016-11-27 22:13:24 +01:00
<strong style={{color: "red"}}>{i18n.str`A problem occured, see below.`}</strong>
2016-10-13 20:02:42 +02:00
</p>
);
}
2016-10-10 03:32:18 +02:00
return (
<p>
2016-11-27 22:13:24 +01:00
{i18n.str`Information about fees will be available when an exchange provider is selected.`}
2016-10-10 03:32:18 +02:00
</p>
);
}
render(): JSX.Element {
2016-10-07 17:10:22 +02:00
return (
<div>
2016-11-23 01:14:45 +01:00
<i18n.Translate wrap="p">
2016-10-07 17:10:22 +02:00
{"You are about to withdraw "}
<strong>{amountToPretty(this.props.amount)}</strong>
2016-10-07 17:10:22 +02:00
{" from your bank account into your wallet."}
2016-11-23 01:14:45 +01:00
</i18n.Translate>
2016-10-10 03:32:18 +02:00
{this.renderFeeStatus()}
2016-10-07 17:10:22 +02:00
<button className="accept"
disabled={this.reserveCreationInfo() == null}
onClick={() => this.confirmReserve()}>
2016-11-27 22:13:24 +01:00
{i18n.str`Accept fees and withdraw`}
2016-10-07 17:10:22 +02:00
</button>
<br/>
{this.renderAdvanced()}
</div>
2016-10-07 14:34:31 +02:00
);
}
confirmReserve() {
this.confirmReserveImpl(this.reserveCreationInfo()!,
this.url()!,
this.props.amount,
this.props.callback_url);
}
2016-10-07 17:10:22 +02:00
/**
* Do an update of the reserve creation info, without any debouncing.
*/
async forceReserveUpdate() {
this.reserveCreationInfo(null);
if (!this.url()) {
2016-11-27 22:13:24 +01:00
this.statusString(i18n.str`Error: URL is empty`);
this.detailCollapsed(false);
2016-10-07 17:10:22 +02:00
return;
}
2016-10-07 14:34:31 +02:00
2016-10-07 17:10:22 +02:00
this.statusString(null);
let parsedUrl = URI(this.url()!);
if (parsedUrl.is("relative")) {
2016-11-27 22:13:24 +01:00
this.statusString(i18n.str`Error: URL may not be relative`);
2016-11-23 16:47:20 +01:00
this.detailCollapsed(false);
2016-10-07 17:10:22 +02:00
return;
}
2016-02-15 11:29:58 +01:00
2016-10-07 17:10:22 +02:00
try {
let url = canonicalizeBaseUrl(this.url()!);
let r = await getReserveCreationInfo(url,
2016-10-07 17:10:22 +02:00
this.props.amount);
console.log("get exchange info resolved");
this.reserveCreationInfo(r);
console.dir(r);
} catch (e) {
console.log("get exchange info rejected");
if (e.hasOwnProperty("httpStatus")) {
this.statusString(`Error: request failed with status ${e.httpStatus}`);
this.detailCollapsed(false);
2016-10-07 17:10:22 +02:00
} else if (e.hasOwnProperty("errorResponse")) {
let resp = e.errorResponse;
this.statusString(`Error: ${resp.error} (${resp.hint})`);
this.detailCollapsed(false);
2016-10-07 17:10:22 +02:00
}
}
}
2015-12-20 20:34:20 +01:00
reset() {
2016-10-07 14:34:31 +02:00
this.statusString(null);
this.reserveCreationInfo(null);
}
2016-02-09 21:56:06 +01:00
2016-10-07 14:34:31 +02:00
confirmReserveImpl(rci: ReserveCreationInfo,
exchange: string,
amount: AmountJson,
callback_url: string) {
const d = {exchange: canonicalizeBaseUrl(exchange), amount};
2016-09-12 20:25:56 +02:00
const cb = (rawResp: any) => {
2016-02-09 21:56:06 +01:00
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-10-07 14:34:31 +02:00
this.statusString(
2016-11-27 22:13:24 +01:00
i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`);
this.detailCollapsed(false);
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);
}
2016-10-07 17:10:22 +02:00
async onUrlChanged(url: string|null) {
this.reset();
2016-02-15 11:29:58 +01:00
this.url(url);
2016-10-07 17:10:22 +02:00
if (url == undefined) {
return;
}
this.updateEvent.trigger();
let waited = await this.updateEvent.wait(200);
if (waited) {
// Run the actual update if nobody else preempted us.
this.forceReserveUpdate();
this.forceUpdate();
}
}
2016-04-27 06:50:38 +02:00
2016-10-07 17:10:22 +02:00
renderStatus(): any {
if (this.statusString()) {
return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>;
2016-10-07 17:10:22 +02:00
} else if (!this.reserveCreationInfo()) {
2016-11-27 22:13:24 +01:00
return <p>{i18n.str`Checking URL, please wait ...`}</p>;
2016-04-27 06:03:04 +02:00
}
2016-10-07 17:10:22 +02:00
return "";
2016-02-18 22:50:17 +01:00
}
}
2016-10-07 14:34:31 +02:00
export async function main() {
try {
2016-11-19 16:33:29 +01:00
const url = URI(document.location.href);
const query: any = URI.parseQuery(url.query());
let amount;
try {
amount = AmountJson.checked(JSON.parse(query.amount));
} catch (e) {
2016-11-27 22:13:24 +01:00
throw Error(i18n.str`Can't parse amount: ${e.message}`);
2016-11-19 16:33:29 +01:00
}
const callback_url = query.callback_url;
const bank_url = query.bank_url;
let wt_types;
try {
wt_types = JSON.parse(query.wt_types);
} catch (e) {
2016-11-27 22:13:24 +01:00
throw Error(i18n.str`Can't parse wire_types: ${e.message}`);
2016-11-19 16:33:29 +01:00
}
let suggestedExchangeUrl = await getSuggestedExchange(amount.currency);
if (!suggestedExchangeUrl && query.suggested_exchange_url) {
suggestedExchangeUrl = query.suggested_exchange_url;
}
2016-10-07 14:34:31 +02:00
let args = {
wt_types,
suggestedExchangeUrl,
callback_url,
amount
};
ReactDOM.render(<ExchangeSelection {...args} />, document.getElementById(
2016-10-07 14:34:31 +02:00
"exchange-selection")!);
} catch (e) {
// TODO: provide more context information, maybe factor it out into a
// TODO:generic error reporting function or component.
2016-11-27 22:13:24 +01:00
document.body.innerText = i18n.str`Fatal error: "${e.message}".`;
2016-10-07 14:34:31 +02:00
console.error(`got error "${e.message}"`, e);
}
2016-10-10 04:01:14 +02:00
}