/* 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 { AmountJson } from "../../util/amounts"; import * as Amounts from "../../util/amounts"; import { WalletBalance, WalletBalanceEntry } from "../../types/walletTypes"; import { abbrev, renderAmount, PageLink } from "../renderHtml"; import * as wxApi from "../wxApi"; import React, { Fragment, useState, useEffect } from "react"; import { HistoryEvent } from "../../types/history"; import moment from "moment"; import { Timestamp } from "../../util/time"; import { classifyTalerUri, TalerUriType } from "../../util/taleruri"; import { PermissionsCheckbox } from "./welcome"; // FIXME: move to newer react functions /* eslint-disable react/no-deprecated */ function onUpdateNotification(f: () => void): () => void { const port = chrome.runtime.connect({ name: "notifications" }); const listener = (): void => { 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(): void { console.log("router mounted"); window.onhashchange = () => { this.setState({}); for (const f of Router.routeHandlers) { f(); } }; } 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): JSX.Element { let cssClass = ""; if (props.target === Router.getRoute()) { cssClass = "active"; } const onClick = (e: React.MouseEvent): void => { Router.setRoute(props.target); e.preventDefault(); }; return ( {props.children} ); } class WalletNavBar extends React.Component { private cancelSubscription: any; componentWillMount(): void { this.cancelSubscription = Router.onRoute(() => { this.setState({}); }); } componentWillUnmount(): void { if (this.cancelSubscription) { this.cancelSubscription(); } } render(): JSX.Element { console.log("rendering nav bar"); return ( ); } } /** * 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(): JSX.Element { 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(): void { this.canceler = onUpdateNotification(() => this.updateBalance()); this.updateBalance(); } componentWillUnmount(): void { console.log("component WalletBalanceView will unmount"); if (this.canceler) { this.canceler(); } this.unmount = true; } async updateBalance(): Promise { 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 Icon({ l }: { l: string }): JSX.Element { return
{l}
; } function formatAndCapitalize(text: string): string { text = text.replace("-", " "); text = text.replace(/^./, text[0].toUpperCase()); return text; } type HistoryItemProps = { title?: string | JSX.Element; text?: string | JSX.Element; small?: string | JSX.Element; amount?: string | AmountJson; fees?: string | AmountJson; invalid?: string | AmountJson; icon?: string; timestamp: Timestamp; negative?: boolean; }; function HistoryItem({ title, text, small, amount, fees, invalid, timestamp, icon, negative = false, }: HistoryItemProps): JSX.Element { function formatDate(timestamp: number | "never"): string | null { if (timestamp !== "never") { const itemDate = moment(timestamp); if (itemDate.isBetween(moment().subtract(2, "days"), moment())) { return itemDate.fromNow(); } return itemDate.format("lll"); } return null; } let invalidElement, amountElement, feesElement; if (amount) { amountElement = renderAmount(amount); } if (fees) { fees = typeof fees === "string" ? Amounts.parse(fees) : fees; if (fees && Amounts.isNonZero(fees)) { feesElement = renderAmount(fees); } } if (invalid) { invalid = typeof invalid === "string" ? Amounts.parse(invalid) : invalid; if (invalid && Amounts.isNonZero(invalid)) { invalidElement = renderAmount(invalid); } } return (
{icon ? : null}
{title ?
{title}
: null} {text ?
{text}
: null} {small ?
{small}
: null}
{amountElement ? (
{amountElement}
) : null} {invalidElement ? (
{i18n.str`Invalid `}{" "} {invalidElement}
) : null} {feesElement ? (
{i18n.str`Fees `}{" "} {feesElement}
) : null}
{formatDate(timestamp.t_ms)}
); } function amountDiff( total: string | Amounts.AmountJson, partial: string | Amounts.AmountJson, ): Amounts.AmountJson | string { const a = typeof total === "string" ? Amounts.parse(total) : total; const b = typeof partial === "string" ? Amounts.parse(partial) : partial; if (a && b) { return Amounts.sub(a, b).amount; } else { return total; } } function parseSummary(summary: string): { item: string; merchant: string } { const parsed = summary.split(/: (.+)/); return { merchant: parsed[0], item: parsed[1], }; } function formatHistoryItem(historyItem: HistoryEvent): JSX.Element { switch (historyItem.type) { case "refreshed": { return ( ); } case "order-refused": { const { merchant, item } = parseSummary( historyItem.orderShortInfo.summary, ); return ( ); } case "order-redirected": { const { merchant, item } = parseSummary( historyItem.newOrderShortInfo.summary, ); return ( ); } case "payment-aborted": { const { merchant, item } = parseSummary( historyItem.orderShortInfo.summary, ); return ( ); } case "payment-sent": { const url = historyItem.orderShortInfo.fulfillmentUrl; const { merchant, item } = parseSummary( historyItem.orderShortInfo.summary, ); const fees = amountDiff( historyItem.amountPaidWithFees, historyItem.orderShortInfo.amount, ); const fulfillmentLinkElem = ( {item ? abbrev(item, 30) : null} ); return ( ); } case "order-accepted": { const url = historyItem.orderShortInfo.fulfillmentUrl; const { merchant, item } = parseSummary( historyItem.orderShortInfo.summary, ); const fulfillmentLinkElem = ( {item ? abbrev(item, 40) : null} ); return ( ); } case "reserve-balance-updated": { return ( ); } case "refund": { const merchantElem = ( {abbrev(historyItem.orderShortInfo.summary, 25)} ); return ( ); } case "withdrawn": { const exchange = new URL(historyItem.exchangeBaseUrl).host; const fees = amountDiff( historyItem.amountWithdrawnRaw, historyItem.amountWithdrawnEffective, ); return ( ); } case "tip-accepted": { return ( Tip Accepted} amount={historyItem.tipAmountRaw} /> ); } case "tip-declined": { return ( Tip Declined} amount={historyItem.tipAmountRaw} /> ); } default: return ( ); } } const HistoryComponent = (props: any): JSX.Element => { const record = props.record; return formatHistoryItem(record); }; class WalletSettings extends React.Component { render(): JSX.Element { return

Permissions

; } } class WalletHistory extends React.Component { private myHistory: any[]; private gotError = false; private unmounted = false; private hidenTypes: string[] = [ "order-accepted", "order-redirected", "refreshed", "reserve-balance-updated", "exchange-updated", "exchange-added", ]; componentWillMount(): void { this.update(); this.setState({ filter: true }); onUpdateNotification(() => this.update()); } componentWillUnmount(): void { console.log("history component unmounted"); this.unmounted = true; } update(): void { 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: HistoryEvent[] = 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[] = []; const messages = history.reverse().filter((hEvent) => { if (!this.state.filter) return true; return this.hidenTypes.indexOf(hEvent.type) === -1; }); for (const record of messages) { const item = ; listing.push(item); } if (listing.length > 0) { return (
{listing}
Filtered list{" "} this.setState({ filter: !this.state.filter })} />
); } return

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

; } } function reload(): void { try { chrome.runtime.reload(); window.close(); } catch (e) { // Functionality missing in firefox, ignore! } } function confirmReset(): void { 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): JSX.Element { 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, }); }; } function makeExtensionUrlWithParams( url: string, params?: { [name: string]: string | undefined }, ): string { const innerUrl = new URL(chrome.extension.getURL("/" + url)); if (params) { for (const key in params) { const p = params[key]; if (p) { innerUrl.searchParams.set(key, p); } } } return innerUrl.href; } function actionForTalerUri(talerUri: string): string | undefined { const uriType = classifyTalerUri(talerUri); switch (uriType) { case TalerUriType.TalerWithdraw: return makeExtensionUrlWithParams("withdraw.html", { talerWithdrawUri: talerUri, }); case TalerUriType.TalerPay: return makeExtensionUrlWithParams("pay.html", { talerPayUri: talerUri, }); case TalerUriType.TalerTip: return makeExtensionUrlWithParams("tip.html", { talerTipUri: talerUri, }); case TalerUriType.TalerRefund: return makeExtensionUrlWithParams("refund.html", { talerRefundUri: talerUri, }); case TalerUriType.TalerNotifyReserve: // FIXME: implement break; default: console.warn( "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", ); break; } return undefined; } async function findTalerUriInActiveTab(): Promise { return new Promise((resolve, reject) => { chrome.tabs.executeScript( { code: ` (() => { let x = document.querySelector("a[href^='taler://'"); return x ? x.href.toString() : null; })(); `, allFrames: false, }, (result) => { if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError); resolve(undefined); return; } console.log("got result", result); resolve(result[0]); }, ); }); } function WalletPopup(): JSX.Element { const [talerActionUrl, setTalerActionUrl] = useState( undefined, ); const [dismissed, setDismissed] = useState(false); useEffect(() => { async function check(): Promise { const talerUri = await findTalerUriInActiveTab(); if (talerUri) { const actionUrl = actionForTalerUri(talerUri); setTalerActionUrl(actionUrl); } } check(); }); if (talerActionUrl && !dismissed) { return (

Taler Action

This page has a Taler action.

); } return (
); } export function createPopup(): JSX.Element { //chrome.runtime.connect({ name: "popup" }); return ; }