/* This file is part of TALER (C) 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 */ /** * Popup shown to the user when they click * the Taler browser action button. * * @author Florian Dold */ /** * Imports. */ import * as i18n from "../../i18n"; import { runOnceWhenReady } from "./common"; import { AmountJson } from "../../amounts"; import * as Amounts from "../../amounts"; import { HistoryRecord, WalletBalance, WalletBalanceEntry, } from "../../walletTypes"; import { abbrev, renderAmount, PageLink } from "../renderHtml"; import * as wxApi from "../wxApi"; import * as React from "react"; import * as ReactDOM from "react-dom"; import URI = require("urijs"); function onUpdateNotification(f: () => void): () => void { const port = chrome.runtime.connect({ name: "notifications" }); const listener = () => { f(); }; port.onMessage.addListener(listener); return () => { port.onMessage.removeListener(listener); }; } class Router extends React.Component { static setRoute(s: string): void { window.location.hash = s; } static getRoute(): string { // Omit the '#' at the beginning return window.location.hash.substring(1); } static onRoute(f: any): () => void { Router.routeHandlers.push(f); return () => { const i = Router.routeHandlers.indexOf(f); this.routeHandlers = this.routeHandlers.splice(i, 1); }; } private static routeHandlers: any[] = []; componentWillMount() { console.log("router mounted"); window.onhashchange = () => { this.setState({}); for (const f of Router.routeHandlers) { f(); } }; } componentWillUnmount() { console.log("router unmounted"); } render(): JSX.Element { const route = window.location.hash.substring(1); console.log("rendering route", route); let defaultChild: React.ReactChild | null = null; let foundChild: React.ReactChild | null = null; React.Children.forEach(this.props.children, child => { const childProps: any = (child as any).props; if (!childProps) { return; } if (childProps.default) { defaultChild = child as React.ReactChild; } if (childProps.route === route) { foundChild = child as React.ReactChild; } }); const c: React.ReactChild | null = foundChild || defaultChild; if (!c) { throw Error("unknown route"); } Router.setRoute((c as any).props.route); return
{c}
; } } interface TabProps { target: string; children?: React.ReactNode; } function Tab(props: TabProps) { let cssClass = ""; if (props.target === Router.getRoute()) { cssClass = "active"; } const onClick = (e: React.MouseEvent) => { Router.setRoute(props.target); e.preventDefault(); }; return ( {props.children} ); } class WalletNavBar extends React.Component { private cancelSubscription: any; componentWillMount() { this.cancelSubscription = Router.onRoute(() => { this.setState({}); }); } componentWillUnmount() { if (this.cancelSubscription) { this.cancelSubscription(); } } render() { console.log("rendering nav bar"); return ( ); } } function ExtensionLink(props: any) { const onClick = (e: React.MouseEvent) => { chrome.tabs.create({ url: chrome.extension.getURL(props.target), }); e.preventDefault(); }; return ( {props.children} ); } /** * Render an amount as a large number with a small currency symbol. */ function bigAmount(amount: AmountJson): JSX.Element { const v = amount.value + amount.fraction / Amounts.fractionalBase; return ( {v}{" "} {amount.currency} ); } function EmptyBalanceView() { return (
You have no balance to show. Need some{" "} help getting started?
); } class WalletBalanceView extends React.Component { private balance: WalletBalance; private gotError = false; private canceler: (() => void) | undefined = undefined; private unmount = false; componentWillMount() { this.canceler = onUpdateNotification(() => this.updateBalance()); this.updateBalance(); } componentWillUnmount() { console.log("component WalletBalanceView will unmount"); if (this.canceler) { this.canceler(); } this.unmount = true; } async updateBalance() { let balance: WalletBalance; try { balance = await wxApi.getBalance(); } catch (e) { if (this.unmount) { return; } this.gotError = true; console.error("could not retrieve balances", e); this.setState({}); return; } if (this.unmount) { return; } this.gotError = false; console.log("got balance", balance); this.balance = balance; this.setState({}); } formatPending(entry: WalletBalanceEntry): JSX.Element { let incoming: JSX.Element | undefined; let payment: JSX.Element | undefined; console.log( "available: ", entry.pendingIncoming ? renderAmount(entry.available) : null, ); console.log( "incoming: ", entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null, ); if (Amounts.isNonZero(entry.pendingIncoming)) { incoming = ( {"+"} {renderAmount(entry.pendingIncoming)} {" "} incoming ); } if (Amounts.isNonZero(entry.pendingPayment)) { payment = ( {"-"} {renderAmount(entry.pendingPayment)} {" "} being spent ); } const l = [incoming, payment].filter(x => x !== undefined); if (l.length === 0) { return ; } if (l.length === 1) { return ({l}); } return ( ({l[0]}, {l[1]}) ); } render(): JSX.Element { const wallet = this.balance; if (this.gotError) { return (

{i18n.str`Error: could not retrieve balance information.`}

Click here for help and diagnostics.

); } if (!wallet) { return ; } console.log(wallet); const listing = Object.keys(wallet.byCurrency).map(key => { const entry: WalletBalanceEntry = wallet.byCurrency[key]; return (

{bigAmount(entry.available)} {this.formatPending(entry)}

); }); return
{listing.length > 0 ? listing : }
; } } function formatHistoryItem(historyItem: HistoryRecord) { const d = historyItem.detail; console.log("hist item", historyItem); switch (historyItem.type) { case "create-reserve": return ( Bank requested reserve ({abbrev(d.reservePub)}) for{" "} {renderAmount(d.requestedAmount)}. ); case "confirm-reserve": { const exchange = new URI(d.exchangeBaseUrl).host(); const pub = abbrev(d.reservePub); return ( Started to withdraw {renderAmount(d.requestedAmount)} from {exchange} ({pub}). ); } case "offer-contract": { return ( Merchant {abbrev(d.merchantName, 15)} offered contract{" "} {abbrev(d.contractTermsHash)}. ); } case "depleted-reserve": { const exchange = d.exchangeBaseUrl ? new URI(d.exchangeBaseUrl).host() : "??"; const amount = renderAmount(d.requestedAmount); const pub = abbrev(d.reservePub); return ( Withdrew {amount} from {exchange} ( {pub}). ); } case "pay": { const url = d.fulfillmentUrl; const merchantElem = {abbrev(d.merchantName, 15)}; const fulfillmentLinkElem = ( view product ); return ( Paid {renderAmount(d.amount)} to merchant{" "} {merchantElem}. ( {fulfillmentLinkElem}) ); } case "refund": { const merchantElem = {abbrev(d.merchantName, 15)}; return ( Merchant {merchantElem} gave a refund over{" "} {renderAmount(d.refundAmount)}. ); } case "tip": { const tipPageUrl = new URI( chrome.extension.getURL("/src/webex/pages/tip.html"), ); const params = { tip_id: d.tipId, merchant_domain: d.merchantDomain }; const url = tipPageUrl.query(params).href(); const tipLink = {i18n.str`tip`}; // i18n: Tip return ( <> Merchant {d.merchantDomain} gave a{" "} {tipLink} of {renderAmount(d.amount)}. {" "} {d.accepted ? null : ( You did not accept the tip yet. )} ); } default: return

{i18n.str`Unknown event (${historyItem.type})`}

; } } class WalletHistory extends React.Component { private myHistory: any[]; private gotError = false; private unmounted = false; componentWillMount() { this.update(); onUpdateNotification(() => this.update()); } componentWillUnmount() { console.log("history component unmounted"); this.unmounted = true; } update() { chrome.runtime.sendMessage({ type: "get-history" }, resp => { if (this.unmounted) { return; } console.log("got history response"); if (resp.error) { this.gotError = true; console.error("could not retrieve history", resp); this.setState({}); return; } this.gotError = false; console.log("got history", resp.history); this.myHistory = resp.history; this.setState({}); }); } render(): JSX.Element { console.log("rendering history"); const history: HistoryRecord[] = this.myHistory; if (this.gotError) { return i18n.str`Error: could not retrieve event history`; } if (!history) { // We're not ready yet return ; } const listing: any[] = []; for (const record of history.reverse()) { const item = (
{new Date(record.timestamp).toString()}
{formatHistoryItem(record)}
); listing.push(item); } if (listing.length > 0) { return
{listing}
; } return

{i18n.str`Your wallet has no events recorded.`}

; } } function reload() { try { chrome.runtime.reload(); window.close(); } catch (e) { // Functionality missing in firefox, ignore! } } function confirmReset() { if ( confirm( "Do you want to IRREVOCABLY DESTROY everything inside your" + " wallet and LOSE ALL YOUR COINS?", ) ) { wxApi.resetDb(); window.close(); } } function WalletDebug(props: any) { return (

Debug tools:


); } function openExtensionPage(page: string) { return () => { chrome.tabs.create({ url: chrome.extension.getURL(page), }); }; } function openTab(page: string) { return (evt: React.SyntheticEvent) => { evt.preventDefault(); chrome.tabs.create({ url: page, }); }; } const el = (
); runOnceWhenReady(() => { ReactDOM.render(el, document.getElementById("content")!); // Will be used by the backend to detect when the popup gets closed, // so we can clear notifications chrome.runtime.connect({ name: "popup" }); });