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-11 11:29:57 +01:00
|
|
|
import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers";
|
|
|
|
import {AmountJson, CreateReserveResponse} from "../lib/wallet/types";
|
2016-02-22 23:13:28 +01:00
|
|
|
import {ReserveCreationInfo, Amounts} from "../lib/wallet/types";
|
2016-02-18 22:50:17 +01:00
|
|
|
import {Denomination} from "../lib/wallet/types";
|
2016-02-19 04:23:00 +01:00
|
|
|
import {getReserveCreationInfo} from "../lib/wallet/wxApi";
|
2016-02-11 11:29:57 +01:00
|
|
|
|
2015-12-20 20:34:20 +01:00
|
|
|
"use strict";
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
let h = preact.h;
|
|
|
|
|
2016-02-11 11:29:57 +01:00
|
|
|
/**
|
|
|
|
* Execute something after a delay, with the possibility
|
|
|
|
* to reset the delay.
|
|
|
|
*/
|
|
|
|
class DelayTimer {
|
|
|
|
ms: number;
|
2016-09-12 20:25:56 +02:00
|
|
|
f: () => void;
|
|
|
|
timerId: number|undefined = undefined;
|
2016-02-11 11:29:57 +01:00
|
|
|
|
2016-09-12 20:25:56 +02:00
|
|
|
constructor(ms: number, f: () => void) {
|
2016-02-11 11:29:57 +01:00
|
|
|
this.f = f;
|
|
|
|
this.ms = ms;
|
|
|
|
}
|
|
|
|
|
|
|
|
bump() {
|
2016-02-15 11:29:58 +01:00
|
|
|
this.stop();
|
2016-02-11 11:29:57 +01:00
|
|
|
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() {
|
2016-09-12 20:25:56 +02:00
|
|
|
if (this.timerId != undefined) {
|
2016-02-15 11:29:58 +01:00
|
|
|
window.clearTimeout(this.timerId);
|
|
|
|
}
|
|
|
|
}
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
2015-12-20 20:34:20 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
interface StateHolder<T> {
|
|
|
|
(): T;
|
|
|
|
(newState: T): void;
|
|
|
|
}
|
2016-01-26 17:21:17 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
/**
|
|
|
|
* Component that doesn't hold its state in one object,
|
|
|
|
* but has multiple state holders.
|
|
|
|
*/
|
|
|
|
abstract class ImplicitStateComponent<PropType> extends preact.Component<PropType, void> {
|
|
|
|
makeState<StateType>(initial: StateType): StateHolder<StateType> {
|
|
|
|
let state: StateType = initial;
|
|
|
|
return (s?: StateType): StateType => {
|
|
|
|
if (s !== undefined) {
|
|
|
|
state = s;
|
|
|
|
// In preact, this will always schedule a (debounced) redraw
|
|
|
|
this.setState({} as any);
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
<tr>
|
|
|
|
<td>{countByPub[denom.denom_pub] + "x"}</td>
|
|
|
|
<td>{amountToPretty(denom.value)}</td>
|
|
|
|
<td>{amountToPretty(denom.fee_withdraw)}</td>
|
|
|
|
<td>{amountToPretty(denom.fee_refresh)}</td>
|
|
|
|
<td>{amountToPretty(denom.fee_deposit)}</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let withdrawFeeStr = amountToPretty(rci.withdrawFee);
|
|
|
|
let overheadStr = amountToPretty(rci.overhead);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p>{`Withdrawal fees: ${withdrawFeeStr}`}</p>
|
|
|
|
<p>{`Rounding loss: ${overheadStr}`}</p>
|
|
|
|
<table>
|
|
|
|
<thead>
|
|
|
|
<th># Coins</th>
|
|
|
|
<th>Value</th>
|
|
|
|
<th>Withdraw Fee</th>
|
|
|
|
<th>Refresh Fee</th>
|
|
|
|
<th>Deposit fee</th>
|
|
|
|
</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);
|
|
|
|
}
|
|
|
|
|
|
|
|
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> {
|
|
|
|
complexViewRequested: StateHolder<boolean> = this.makeState(false);
|
|
|
|
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);
|
|
|
|
|
|
|
|
private timer: DelayTimer;
|
|
|
|
|
|
|
|
isValidExchange: boolean;
|
|
|
|
|
|
|
|
constructor(props: ExchangeSelectionProps) {
|
|
|
|
super(props);
|
2016-02-11 11:29:57 +01:00
|
|
|
this.timer = new DelayTimer(800, () => this.update());
|
2016-10-07 14:34:31 +02:00
|
|
|
this.url(props.suggestedExchangeUrl || null);
|
2016-02-15 11:29:58 +01:00
|
|
|
this.update();
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
render(props: ExchangeSelectionProps): JSX.Element {
|
|
|
|
|
|
|
|
console.log("props", props);
|
|
|
|
|
|
|
|
let header = (
|
|
|
|
<p>
|
|
|
|
{"You are about to withdraw "}
|
|
|
|
<strong>{amountToPretty(props.amount)}</strong>
|
|
|
|
{" from your bank account into your wallet."}
|
|
|
|
</p>
|
|
|
|
);
|
|
|
|
|
|
|
|
if (this.complexViewRequested() || !props.suggestedExchangeUrl) {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{header}
|
|
|
|
{this.viewComplex()}
|
|
|
|
</div>);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{header}
|
|
|
|
{this.viewSimple()}
|
|
|
|
</div>);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
viewSimple() {
|
|
|
|
let advancedButton = (
|
|
|
|
<button className="linky"
|
|
|
|
onClick={() => this.complexViewRequested(true)}>
|
|
|
|
advanced options
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
if (this.statusString()) {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p>Error: {this.statusString()}</p>
|
|
|
|
{advancedButton}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
else if (this.reserveCreationInfo() != undefined) {
|
|
|
|
let {overhead, withdrawFee} = this.reserveCreationInfo()!;
|
|
|
|
let totalCost = Amounts.add(overhead, withdrawFee).amount;
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p>{`Withdraw fees: ${amountToPretty(totalCost)}`}</p>
|
|
|
|
<button className="accept"
|
|
|
|
onClick={() => this.confirmReserve()}>
|
|
|
|
Accept fees and withdraw
|
|
|
|
</button>
|
|
|
|
<span className="spacer"/>
|
|
|
|
{advancedButton}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return <p>Please wait...</p>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
confirmReserve() {
|
|
|
|
this.confirmReserveImpl(this.reserveCreationInfo()!,
|
|
|
|
this.url()!,
|
|
|
|
this.props.amount,
|
|
|
|
this.props.callback_url);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
update() {
|
2016-02-15 11:29:58 +01:00
|
|
|
this.timer.stop();
|
2016-02-11 11:29:57 +01:00
|
|
|
const doUpdate = () => {
|
2016-10-07 14:34:31 +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);
|
2016-02-11 11:29:57 +01:00
|
|
|
return;
|
|
|
|
}
|
2016-10-07 14:34:31 +02:00
|
|
|
this.statusString(null);
|
|
|
|
let parsedUrl = URI(this.url()!);
|
2016-02-11 11:29:57 +01:00
|
|
|
if (parsedUrl.is("relative")) {
|
2016-04-27 06:50:38 +02:00
|
|
|
this.statusString = i18n`Error: URL may not be relative`;
|
2016-10-07 14:34:31 +02:00
|
|
|
this.forceUpdate();
|
2016-02-11 11:29:57 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
this.forceUpdate();
|
2016-02-18 22:50:17 +01:00
|
|
|
|
2016-03-01 19:39:17 +01:00
|
|
|
console.log("doing get exchange info");
|
2016-02-18 22:50:17 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
getReserveCreationInfo(this.url()!, this.props.amount)
|
2016-02-18 22:50:17 +01:00
|
|
|
.then((r: ReserveCreationInfo) => {
|
2016-03-01 19:39:17 +01:00
|
|
|
console.log("get exchange info resolved");
|
|
|
|
this.isValidExchange = true;
|
2016-10-07 14:34:31 +02:00
|
|
|
this.reserveCreationInfo(r);
|
2016-02-18 22:50:17 +01:00
|
|
|
console.dir(r);
|
|
|
|
})
|
|
|
|
.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-10-07 14:34:31 +02:00
|
|
|
this.statusString(`Error: request failed with status ${e.httpStatus}`);
|
2016-04-22 18:03:56 +02:00
|
|
|
} else if (e.hasOwnProperty("errorResponse")) {
|
|
|
|
let resp = e.errorResponse;
|
2016-10-07 14:34:31 +02:00
|
|
|
this.statusString(`Error: ${resp.error} (${resp.hint})`);
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
2016-02-18 22:50:17 +01:00
|
|
|
});
|
2016-02-09 21:56:06 +01:00
|
|
|
};
|
|
|
|
|
2016-02-11 11:29:57 +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());
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
2015-12-20 20:34:20 +01:00
|
|
|
|
2016-02-11 11:29:57 +01:00
|
|
|
reset() {
|
2016-03-01 19:39:17 +01:00
|
|
|
this.isValidExchange = false;
|
2016-10-07 14:34:31 +02:00
|
|
|
this.statusString(null);
|
|
|
|
this.reserveCreationInfo(null);
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
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) {
|
2016-03-01 19:39:17 +01:00
|
|
|
const d = {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");
|
|
|
|
}
|
2016-04-06 02:06:57 +02:00
|
|
|
// 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);
|
2016-04-06 02:06:57 +02:00
|
|
|
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,
|
2016-02-11 11:29:57 +01:00
|
|
|
amount_value: amount.value,
|
|
|
|
amount_fraction: amount.fraction,
|
|
|
|
amount_currency: amount.currency,
|
2016-02-09 21:56:06 +01:00
|
|
|
};
|
2016-02-11 11:29:57 +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");
|
|
|
|
}
|
2016-02-11 11:29:57 +01:00
|
|
|
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 {
|
2016-02-11 11:29:57 +01:00
|
|
|
this.reset();
|
2016-10-07 14:34:31 +02: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);
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
onUrlChanged(url: string) {
|
|
|
|
this.reset();
|
2016-02-15 11:29:58 +01:00
|
|
|
this.url(url);
|
2016-02-11 11:29:57 +01:00
|
|
|
this.timer.bump();
|
|
|
|
}
|
2016-04-27 06:50:38 +02:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
viewComplex() {
|
|
|
|
function *f(): IterableIterator<any> {
|
|
|
|
if (this.reserveCreationInfo()) {
|
|
|
|
let {overhead, withdrawFee} = this.reserveCreationInfo()!;
|
|
|
|
let totalCost = Amounts.add(overhead, withdrawFee).amount;
|
|
|
|
yield <p>Withdraw fees: {amountToPretty(totalCost)}</p>;
|
2016-04-27 06:50:38 +02:00
|
|
|
}
|
2016-02-15 11:29:58 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield (
|
|
|
|
<button className="accept" disabled={!this.isValidExchange}
|
|
|
|
onClick={() => this.confirmReserve()}>
|
|
|
|
Accept fees and withdraw
|
|
|
|
</button>
|
|
|
|
);
|
2016-04-27 06:50:38 +02:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield <span className="spacer"/>;
|
2016-04-27 06:50:38 +02:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield (
|
|
|
|
<button className="linky"
|
|
|
|
onClick={() => this.complexViewRequested(true)}/>
|
|
|
|
);
|
2016-04-27 06:50:38 +02:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield <br/>;
|
2016-02-15 11:29:58 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield (
|
|
|
|
<input className="url" type="text" spellCheck={false}
|
|
|
|
value={this.url()!}
|
|
|
|
onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/>
|
|
|
|
);
|
2016-02-11 11:29:57 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
yield <br/>;
|
2016-02-11 11:29:57 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
if (this.statusString()) {
|
|
|
|
yield <p>{this.statusString()}</p>;
|
|
|
|
} else if (!this.reserveCreationInfo()) {
|
|
|
|
yield <p>Checking URL, please wait ...</p>;
|
|
|
|
}
|
2016-04-27 06:03:04 +02:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
if (this.reserveCreationInfo()) {
|
|
|
|
if (this.detailCollapsed()) {
|
|
|
|
yield (
|
|
|
|
<button className="linky"
|
|
|
|
onClick={() => this.detailCollapsed(false)}>
|
|
|
|
show more details
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
yield (
|
|
|
|
<button className="linky"
|
|
|
|
onClick={() => this.detailCollapsed(true)}>
|
|
|
|
hide details
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
yield (
|
|
|
|
<div>
|
|
|
|
{renderReserveCreationDetails(this.reserveCreationInfo()!)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2016-04-27 06:03:04 +02:00
|
|
|
}
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
return Array.from(f.call(this));
|
2016-02-18 22:50:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
export async function main() {
|
2016-02-15 11:29:58 +01:00
|
|
|
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;
|
2016-04-06 02:06:57 +02:00
|
|
|
const wt_types = JSON.parse(query.wt_types);
|
2016-02-15 11:29:58 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
try {
|
|
|
|
const suggestedExchangeUrl = await getSuggestedExchange(amount.currency);
|
|
|
|
let args = {
|
|
|
|
wt_types,
|
|
|
|
suggestedExchangeUrl,
|
|
|
|
callback_url,
|
|
|
|
amount
|
|
|
|
};
|
|
|
|
|
|
|
|
preact.render(<ExchangeSelection {...args} />, document.getElementById(
|
|
|
|
"exchange-selection")!);
|
|
|
|
|
|
|
|
} 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);
|
|
|
|
}
|
2016-09-14 15:20:18 +02:00
|
|
|
}
|