diff options
Diffstat (limited to 'src/webex/pages/confirm-create-reserve.tsx')
-rw-r--r-- | src/webex/pages/confirm-create-reserve.tsx | 526 |
1 files changed, 0 insertions, 526 deletions
diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx deleted file mode 100644 index 2d4f41dfe..000000000 --- a/src/webex/pages/confirm-create-reserve.tsx +++ /dev/null @@ -1,526 +0,0 @@ -/* - 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 { canonicalizeBaseUrl } from "../../helpers"; -import * as i18n from "../../i18n"; - -import { AmountJson } from "../../amounts"; -import * as Amounts from "../../amounts"; - -import { - CurrencyRecord, -} from "../../dbTypes"; -import { - CreateReserveResponse, - ReserveCreationInfo, -} from "../../walletTypes"; - -import { ImplicitStateComponent, StateHolder } from "../components"; -import { - WalletApiError, - createReserve, - getCurrency, - getExchangeInfo, - getReserveCreationInfo, -} from "../wxApi"; - -import { - WithdrawDetailView, - renderAmount, -} from "../renderHtml"; - -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import URI = require("urijs"); - - -function delay<T>(delayMs: number, value: T): Promise<T> { - return new Promise<T>((resolve, reject) => { - setTimeout(() => resolve(value), delayMs); - }); -} - -class EventTrigger { - private triggerResolve: any; - private 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)]); - } -} - - -interface ExchangeSelectionProps { - suggestedExchangeUrl: string; - amount: AmountJson; - callback_url: string; - wt_types: string[]; - currencyRecord: CurrencyRecord|null; - sender_wire: string | undefined; -} - -interface ManualSelectionProps { - onSelect(url: string): void; - initialUrl: string; -} - -class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> { - private url: StateHolder<string> = this.makeState(""); - private errorMessage: StateHolder<string|null> = this.makeState(null); - private isOkay: StateHolder<boolean> = this.makeState(false); - private updateEvent = new EventTrigger(); - constructor(p: ManualSelectionProps) { - super(p); - this.url(p.initialUrl); - this.update(); - } - render() { - return ( - <div className="pure-g pure-form pure-form-stacked"> - <div className="pure-u-1"> - <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)} - onChange={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)} /> - </div> - <div className="pure-u-1"> - <button className="pure-button button-success" - disabled={!this.isOkay()} - onClick={() => this.props.onSelect(this.url())}> - {i18n.str`Select`} - </button> - <span> </span> - {this.errorMessage()} - </div> - </div> - ); - } - - async update() { - this.errorMessage(null); - this.isOkay(false); - if (!this.url()) { - return; - } - const parsedUrl = new URI(this.url()!); - if (parsedUrl.is("relative")) { - this.errorMessage(i18n.str`Error: URL may not be relative`); - this.isOkay(false); - return; - } - try { - const url = canonicalizeBaseUrl(this.url()!); - await getExchangeInfo(url); - console.log("getExchangeInfo returned"); - this.isOkay(true); - } catch (e) { - if (!(e instanceof WalletApiError)) { - // maybe it's something more serious, don't handle here! - throw e; - } - console.log(`got error "${e.message} "with detail`, e.detail); - this.errorMessage(i18n.str`Invalid exchange URL (${e.message})`); - } - } - - async onUrlChanged(s: string) { - this.url(s); - this.errorMessage(null); - this.isOkay(false); - this.updateEvent.trigger(); - const waited = await this.updateEvent.wait(200); - if (waited) { - // Run the actual update if nobody else preempted us. - this.update(); - } - } -} - - -class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> { - private statusString: StateHolder<string|null> = this.makeState(null); - private reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState( - null); - private url: StateHolder<string|null> = this.makeState(null); - - private selectingExchange: StateHolder<boolean> = this.makeState(false); - - constructor(props: ExchangeSelectionProps) { - super(props); - const prefilledExchangesUrls = []; - if (props.currencyRecord) { - const exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl); - prefilledExchangesUrls.push(...exchanges); - } - if (props.suggestedExchangeUrl) { - prefilledExchangesUrls.push(props.suggestedExchangeUrl); - } - if (prefilledExchangesUrls.length !== 0) { - this.url(prefilledExchangesUrls[0]); - this.forceReserveUpdate(); - } else { - this.selectingExchange(true); - } - } - - renderFeeStatus() { - const rci = this.reserveCreationInfo(); - if (rci) { - const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount; - let trustMessage; - if (rci.isTrusted) { - trustMessage = ( - <i18n.Translate wrap="p"> - The exchange is trusted by the wallet. - </i18n.Translate> - ); - } else if (rci.isAudited) { - trustMessage = ( - <i18n.Translate wrap="p"> - The exchange is audited by a trusted auditor. - </i18n.Translate> - ); - } else { - trustMessage = ( - <i18n.Translate wrap="p"> - Warning: The exchange is neither directly trusted nor audited by a trusted auditor. - If you withdraw from this exchange, it will be trusted in the future. - </i18n.Translate> - ); - } - return ( - <div> - <i18n.Translate wrap="p"> - Using exchange provider <strong>{this.url()}</strong>. - The exchange provider will charge - {" "}<span>{renderAmount(totalCost)}</span>{" "} - in fees. - </i18n.Translate> - {trustMessage} - </div> - ); - } - if (this.url() && !this.statusString()) { - const shortName = new URI(this.url()!).host(); - return ( - <i18n.Translate wrap="p"> - Waiting for a response from - <span> </span> - <em>{shortName}</em> - </i18n.Translate> - ); - } - if (this.statusString()) { - return ( - <p> - <strong style={{color: "red"}}>{this.statusString()}</strong> - </p> - ); - } - return ( - <p> - {i18n.str`Information about fees will be available when an exchange provider is selected.`} - </p> - ); - } - - renderUpdateStatus() { - const rci = this.reserveCreationInfo(); - if (!rci) { - return null; - } - if (!rci.versionMatch) { - return null; - } - if (rci.versionMatch.compatible) { - return null; - } - if (rci.versionMatch.currentCmp === -1) { - return ( - <p className="errorbox"> - <i18n.Translate wrap="span"> - Your wallet (protocol version <span>{rci.walletVersion}</span>) might be outdated.<span> </span> - The exchange has a higher, incompatible - protocol version (<span>{rci.exchangeVersion}</span>). - </i18n.Translate> - </p> - ); - } - if (rci.versionMatch.currentCmp === 1) { - return ( - <p className="errorbox"> - <i18n.Translate wrap="span"> - The chosen exchange (protocol version <span>{rci.exchangeVersion}</span> might be outdated.<span> </span> - The exchange has a lower, incompatible - protocol version than your wallet (protocol version <span>{rci.walletVersion}</span>). - </i18n.Translate> - </p> - ); - } - throw Error("not reached"); - } - - renderConfirm() { - return ( - <div> - {this.renderFeeStatus()} - <p> - <button className="pure-button button-success" - disabled={this.reserveCreationInfo() === null} - onClick={() => this.confirmReserve()}> - {i18n.str`Accept fees and withdraw`} - </button> - { " " } - <button className="pure-button button-secondary" - onClick={() => this.selectingExchange(true)}> - {i18n.str`Change Exchange Provider`} - </button> - </p> - {this.renderUpdateStatus()} - <WithdrawDetailView rci={this.reserveCreationInfo()} /> - </div> - ); - } - - select(url: string) { - this.reserveCreationInfo(null); - this.url(url); - this.selectingExchange(false); - this.forceReserveUpdate(); - } - - renderSelect() { - const exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || []; - console.log(exchanges); - return ( - <div> - {i18n.str`Please select an exchange. You can review the details before after your selection.`} - - {this.props.suggestedExchangeUrl && ( - <div> - <h2>Bank Suggestion</h2> - <button className="pure-button button-success" onClick={() => this.select(this.props.suggestedExchangeUrl)}> - <i18n.Translate wrap="span"> - Select <strong>{this.props.suggestedExchangeUrl}</strong> - </i18n.Translate> - </button> - </div> - )} - - {exchanges.length > 0 && ( - <div> - <h2>Known Exchanges</h2> - {exchanges.map((e) => ( - <button key={e.baseUrl} className="pure-button button-success" onClick={() => this.select(e.baseUrl)}> - <i18n.Translate> - Select <strong>{e.baseUrl}</strong> - </i18n.Translate> - </button> - ))} - </div> - )} - - <h2>i18n.str`Manual Selection`</h2> - <ManualSelection initialUrl={this.url() || ""} onSelect={(url: string) => this.select(url)} /> - </div> - ); - } - - render(): JSX.Element { - return ( - <div> - <i18n.Translate wrap="p"> - You are about to withdraw - {" "}<strong>{renderAmount(this.props.amount)}</strong>{" "} - from your bank account into your wallet. - </i18n.Translate> - {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()} - </div> - ); - } - - - confirmReserve() { - this.confirmReserveImpl(this.reserveCreationInfo()!, - this.url()!, - this.props.amount, - this.props.callback_url, - this.props.sender_wire); - } - - /** - * Do an update of the reserve creation info, without any debouncing. - */ - async forceReserveUpdate() { - this.reserveCreationInfo(null); - try { - const url = canonicalizeBaseUrl(this.url()!); - const r = await getReserveCreationInfo(url, - this.props.amount); - console.log("get exchange info resolved"); - this.reserveCreationInfo(r); - console.dir(r); - } catch (e) { - console.log("get exchange info rejected", e); - this.statusString(`Error: ${e.message}`); - // Re-try every 5 seconds as long as there is a problem - setTimeout(() => this.statusString() ? this.forceReserveUpdate() : undefined, 5000); - } - } - - async confirmReserveImpl(rci: ReserveCreationInfo, - exchange: string, - amount: AmountJson, - callback_url: string, - sender_wire: string | undefined) { - const rawResp = await createReserve({ - amount, - exchange: canonicalizeBaseUrl(exchange), - senderWire: sender_wire, - }); - if (!rawResp) { - throw Error("empty response"); - } - // FIXME: filter out types that bank/exchange don't have in common - const exchangeWireAccounts = []; - - for (let acct of rci.exchangeWireAccounts) { - const payto = new URI(acct); - if (payto.scheme() != "payto") { - console.warn("unknown wire account URI scheme", acct); - continue; - } - if (this.props.wt_types.includes(payto.authority())) { - exchangeWireAccounts.push(acct); - } - } - - const chosenAcct = exchangeWireAccounts[0]; - - if (!chosenAcct) { - throw Error("no exchange account matches the bank's supported types"); - } - - if (!rawResp.error) { - const resp = CreateReserveResponse.checked(rawResp); - const q: {[name: string]: string|number} = { - amount_currency: amount.currency, - amount_fraction: amount.fraction, - amount_value: amount.value, - exchange: resp.exchange, - exchange_wire_details: chosenAcct, - reserve_pub: resp.reservePub, - }; - const url = new 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.statusString( - i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`); - } - } - - renderStatus(): any { - if (this.statusString()) { - return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>; - } else if (!this.reserveCreationInfo()) { - return <p>{i18n.str`Checking URL, please wait ...`}</p>; - } - return ""; - } -} - -async function main() { - try { - const url = new URI(document.location.href); - const query: any = URI.parseQuery(url.query()); - let amount; - try { - amount = AmountJson.checked(JSON.parse(query.amount)); - } catch (e) { - throw Error(i18n.str`Can't parse amount: ${e.message}`); - } - const callback_url = query.callback_url; - let wt_types; - try { - wt_types = JSON.parse(query.wt_types); - } catch (e) { - throw Error(i18n.str`Can't parse wire_types: ${e.message}`); - } - - let sender_wire; - if (query.sender_wire) { - let senderWireUri = new URI(query.sender_wire); - if (senderWireUri.scheme() != "payto") { - throw Error("sender wire info must be a payto URI"); - } - sender_wire = query.sender_wire; - } - - const suggestedExchangeUrl = query.suggested_exchange_url; - const currencyRecord = await getCurrency(amount.currency); - - const args = { - amount, - callback_url, - currencyRecord, - sender_wire, - suggestedExchangeUrl, - wt_types, - }; - - 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 = i18n.str`Fatal error: "${e.message}".`; - console.error("got error", e); - } -} - -document.addEventListener("DOMContentLoaded", () => { - main(); -}); |