diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-11-13 23:30:18 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-11-13 23:31:17 +0100 |
commit | f3fb8be7db6de87dae40d41bd5597a735c800ca1 (patch) | |
tree | 1a061db04de8f5bb5a6b697fa56a9948f67fac2f /src/pages/confirm-create-reserve.tsx | |
parent | 200d83c3886149ebb3f018530302079e12a81f6b (diff) |
restructuring
Diffstat (limited to 'src/pages/confirm-create-reserve.tsx')
-rw-r--r-- | src/pages/confirm-create-reserve.tsx | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx new file mode 100644 index 000000000..372f11a4b --- /dev/null +++ b/src/pages/confirm-create-reserve.tsx @@ -0,0 +1,397 @@ +/* + This file is part of TALER + (C) 2015-2016 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, see <http://www.gnu.org/licenses/> + */ + + +/** + * Page shown to the user to confirm creation + * of a reserve, usually requested by the bank. + * + * @author Florian Dold + */ + +import {amountToPretty, canonicalizeBaseUrl} from "src/helpers"; +import { + AmountJson, CreateReserveResponse, + ReserveCreationInfo, Amounts, + Denomination, +} from "src/types"; +import {getReserveCreationInfo} from "src/wxApi"; +import {ImplicitStateComponent, StateHolder} from "src/components"; + +"use strict"; + + +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(); + } + + private reset() { + this.triggerPromise = new Promise<boolean>((resolve, reject) => { + this.triggerResolve = resolve; + }); + } + + trigger() { + this.triggerResolve(false); + this.reset(); + } + + async wait(delayMs: number): Promise<boolean> { + return await Promise.race([this.triggerPromise, delay(delayMs, true)]); + } +} + + +function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { + if (!rci) { + return <p> + Details will be displayed when a valid exchange provider URL is entered.</p> + } + + 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); +} + + +function WithdrawFee(props: {reserveCreationInfo: ReserveCreationInfo|null}): JSX.Element { + if (props.reserveCreationInfo) { + let {overhead, withdrawFee} = props.reserveCreationInfo; + let totalCost = Amounts.add(overhead, withdrawFee).amount; + return <p>Withdraw fees: {amountToPretty(totalCost)}</p>; + } + return <p />; +} + + +interface ExchangeSelectionProps { + suggestedExchangeUrl: string; + 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); + + updateEvent = new EventTrigger(); + + constructor(props: ExchangeSelectionProps) { + super(props); + this.onUrlChanged(props.suggestedExchangeUrl || null); + } + + + renderAdvanced(): JSX.Element { + if (this.detailCollapsed() && this.url() !== null && !this.statusString()) { + return ( + <button className="linky" + onClick={() => this.detailCollapsed(false)}> + view fee structure / select different exchange provider + </button> + ); + } + return ( + <div> + <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()} + <h2>Detailed Fee Structure</h2> + {renderReserveCreationDetails(this.reserveCreationInfo())} + </div>) + } + + renderFee() { + if (!this.reserveCreationInfo()) { + return "??"; + } + let rci = this.reserveCreationInfo()!; + let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; + return `${amountToPretty(totalCost)}`; + } + + renderFeeStatus() { + if (this.reserveCreationInfo()) { + return ( + <p> + The exchange provider will charge + {" "} + {this.renderFee()} + {" "} + in fees. + </p> + ); + } + if (this.url() && !this.statusString()) { + let shortName = URI(this.url()!).host(); + return <p> + Waiting for a response from + {" "} + <em>{shortName}</em> + </p>; + } + if (this.statusString()) { + return ( + <p> + <strong style={{color: "red"}}>A problem occured, see below.</strong> + </p> + ); + } + return ( + <p> + Information about fees will be available when an exchange provider is selected. + </p> + ); + } + + render(): JSX.Element { + return ( + <div> + <p> + {"You are about to withdraw "} + <strong>{amountToPretty(this.props.amount)}</strong> + {" from your bank account into your wallet."} + </p> + {this.renderFeeStatus()} + <button className="accept" + disabled={this.reserveCreationInfo() == null} + onClick={() => this.confirmReserve()}> + Accept fees and withdraw + </button> + <br/> + {this.renderAdvanced()} + </div> + ); + } + + + confirmReserve() { + this.confirmReserveImpl(this.reserveCreationInfo()!, + this.url()!, + this.props.amount, + this.props.callback_url); + } + + /** + * Do an update of the reserve creation info, without any debouncing. + */ + async forceReserveUpdate() { + this.reserveCreationInfo(null); + if (!this.url()) { + this.statusString(i18n`Error: URL is empty`); + return; + } + + this.statusString(null); + let parsedUrl = URI(this.url()!); + if (parsedUrl.is("relative")) { + this.statusString(i18n`Error: URL may not be relative`); + return; + } + + try { + let r = await getReserveCreationInfo(this.url()!, + 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}`); + } else if (e.hasOwnProperty("errorResponse")) { + let resp = e.errorResponse; + this.statusString(`Error: ${resp.error} (${resp.hint})`); + } + } + } + + reset() { + this.statusString(null); + this.reserveCreationInfo(null); + } + + confirmReserveImpl(rci: ReserveCreationInfo, + exchange: string, + amount: AmountJson, + callback_url: string) { + const d = {exchange, amount}; + const cb = (rawResp: any) => { + if (!rawResp) { + throw Error("empty response"); + } + // FIXME: filter out types that bank/exchange don't have in common + let wire_details = rci.wireInfo; + if (!rawResp.error) { + const resp = CreateReserveResponse.checked(rawResp); + let q: {[name: string]: string|number} = { + wire_details: JSON.stringify(wire_details), + exchange: resp.exchange, + reserve_pub: resp.reservePub, + amount_value: amount.value, + amount_fraction: amount.fraction, + amount_currency: amount.currency, + }; + 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 { + this.reset(); + this.statusString( + `Oops, something went wrong.` + + `The wallet responded with error status (${rawResp.error}).`); + } + }; + chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb); + } + + async onUrlChanged(url: string|null) { + this.reset(); + this.url(url); + 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(); + } + } + + renderStatus(): any { + if (this.statusString()) { + return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>; + } else if (!this.reserveCreationInfo()) { + return <p>Checking URL, please wait ...</p>; + } + return ""; + } +} + +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)); + const callback_url = query.callback_url; + const bank_url = query.bank_url; + const wt_types = JSON.parse(query.wt_types); + + try { + const suggestedExchangeUrl = await getSuggestedExchange(amount.currency); + let args = { + wt_types, + suggestedExchangeUrl, + callback_url, + amount + }; + + ReactDOM.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); + } +} |