aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-10-07 14:34:31 +0200
committerFlorian Dold <florian.dold@gmail.com>2016-10-07 17:10:29 +0200
commitcb424d369919ef29f9a3729fedab5323b1883aca (patch)
treea8b61cf198ec91bd3a2f9da5ec4e921440590f7d
parentd10f6e024dd23590ba948dfa6b3850abb6317663 (diff)
refactor reserve creation dialog
-rw-r--r--lib/module-trampoline.js4
-rw-r--r--pages/confirm-create-reserve.html8
-rw-r--r--pages/confirm-create-reserve.tsx503
3 files changed, 284 insertions, 231 deletions
diff --git a/lib/module-trampoline.js b/lib/module-trampoline.js
index 73e8ca72d..3eb94e36a 100644
--- a/lib/module-trampoline.js
+++ b/lib/module-trampoline.js
@@ -35,7 +35,7 @@ System.config({
// Register mithril as a module,
// but only if it is ambient.
-if (m) {
+if (typeof m !== "undefined") {
let mod = System.newModule({default: m});
let modName = "mithril";
System.set(modName, mod);
@@ -55,7 +55,7 @@ document.addEventListener("DOMContentLoaded", function(event) {
function execMain(m) {
if (m.main) {
console.log("executing module main");
- m.main();
+ let res = m.main();
} else {
console.warn("module does not export a main() function");
}
diff --git a/pages/confirm-create-reserve.html b/pages/confirm-create-reserve.html
index b8a825bd1..165ac32f4 100644
--- a/pages/confirm-create-reserve.html
+++ b/pages/confirm-create-reserve.html
@@ -7,11 +7,15 @@
<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/preact.js"></script>
+
+ <!-- i18n -->
<script src="../lib/vendor/jed.js"></script>
<script src="../lib/i18n.js"></script>
<script src="../i18n/strings.js"></script>
+
+ <!-- module loading -->
+ <script src="../lib/vendor/system-csp-production.src.js"></script>
<script src="../lib/module-trampoline.js"></script>
<style>
diff --git a/pages/confirm-create-reserve.tsx b/pages/confirm-create-reserve.tsx
index 8e8067052..666f8c68a 100644
--- a/pages/confirm-create-reserve.tsx
+++ b/pages/confirm-create-reserve.tsx
@@ -22,18 +22,16 @@
* @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";
+let h = preact.h;
+
/**
* Execute something after a delay, with the possibility
* to reset the delay.
@@ -63,73 +61,232 @@ class DelayTimer {
}
}
+interface StateHolder<T> {
+ (): T;
+ (newState: T): void;
+}
-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);
+/**
+ * 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 {
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;
+ 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);
this.timer = new DelayTimer(800, () => this.update());
- this.url(suggestedExchangeUrl);
+ this.url(props.suggestedExchangeUrl || null);
this.update();
}
- private update() {
+ 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() {
this.timer.stop();
const doUpdate = () => {
- this.reserveCreationInfo = undefined;
+ this.reserveCreationInfo(null);
if (!this.url()) {
this.statusString = i18n`Error: URL is empty`;
m.redraw(true);
return;
}
- this.statusString = null;
- let parsedUrl = URI(this.url());
+ this.statusString(null);
+ let parsedUrl = URI(this.url()!);
if (parsedUrl.is("relative")) {
this.statusString = i18n`Error: URL may not be relative`;
- m.redraw(true);
+ this.forceUpdate();
return;
}
- m.redraw(true);
+ this.forceUpdate();
console.log("doing get exchange info");
- getReserveCreationInfo(this.url(), this.amount)
+ getReserveCreationInfo(this.url()!, this.props.amount)
.then((r: ReserveCreationInfo) => {
console.log("get exchange info resolved");
this.isValidExchange = true;
- this.reserveCreationInfo = r;
+ 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}`;
+ 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})`;
+ this.statusString(`Error: ${resp.error} (${resp.hint})`);
}
- m.endComputation();
});
};
@@ -140,14 +297,14 @@ class Controller {
reset() {
this.isValidExchange = false;
- this.statusString = null;
- this.reserveCreationInfo = undefined;
+ this.statusString(null);
+ this.reserveCreationInfo(null);
}
- confirmReserve(rci: ReserveCreationInfo,
- exchange: string,
- amount: AmountJson,
- callback_url: string) {
+ confirmReserveImpl(rci: ReserveCreationInfo,
+ exchange: string,
+ amount: AmountJson,
+ callback_url: string) {
const d = {exchange, amount};
const cb = (rawResp: any) => {
if (!rawResp) {
@@ -173,9 +330,9 @@ class Controller {
document.location.href = url.href();
} else {
this.reset();
- this.statusString = (
- `Oops, something went wrong.` +
- `The wallet responded with error status (${rawResp.error}).`);
+ this.statusString(
+ `Oops, something went wrong.` +
+ `The wallet responded with error status (${rawResp.error}).`);
}
};
chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
@@ -186,188 +343,74 @@ class Controller {
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;
+ 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>;
}
- }, "back to simple view");
- yield m("br");
+ yield (
+ <button className="accept" disabled={!this.isValidExchange}
+ onClick={() => this.confirmReserve()}>
+ Accept fees and withdraw
+ </button>
+ );
+ yield <span className="spacer"/>;
- yield m("input", {
- className: "url",
- type: "text",
- spellcheck: false,
- value: ctrl.url(),
- oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)),
- });
+ yield (
+ <button className="linky"
+ onClick={() => this.complexViewRequested(true)}/>
+ );
- yield m("br");
+ yield <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());
-}
+ yield (
+ <input className="url" type="text" spellCheck={false}
+ value={this.url()!}
+ onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/>
+ );
+ yield <br/>;
-function renderReserveCreationDetails(rci: ReserveCreationInfo) {
- let denoms = rci.selectedDenoms;
-
- let countByPub: {[s: string]: number} = {};
- let uniq: Denomination[] = [];
+ if (this.statusString()) {
+ yield <p>{this.statusString()}</p>;
+ } else if (!this.reserveCreationInfo()) {
+ yield <p>Checking URL, please wait ...</p>;
+ }
- denoms.forEach((x: Denomination) => {
- let c = countByPub[x.denom_pub] || 0;
- if (c == 0) {
- uniq.push(x);
+ 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>
+ );
+ }
+ }
}
- 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)),
- ]);
+ return Array.from(f.call(this));
}
-
- 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() {
+export async function main() {
const url = URI(document.location.href);
const query: any = URI.parseQuery(url.query());
const amount = AmountJson.checked(JSON.parse(query.amount));
@@ -375,16 +418,22 @@ export function main() {
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);
- });
+ 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);
+ }
} \ No newline at end of file