/* 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 { AmountJson, Amounts, BalancesResponse, Balance, classifyTalerUri, TalerUriType, TransactionsResponse, Transaction, TransactionType, AmountString, Timestamp, amountFractionalBase, } from "@gnu-taler/taler-util"; import { format } from "date-fns"; import { Component, ComponentChildren, Fragment, JSX } from "preact"; import { route, Route, Router } from 'preact-router'; import { Match } from 'preact-router/match'; import { useEffect, useState } from "preact/hooks"; import * as i18n from "../i18n"; import { PageLink, renderAmount } from "../renderHtml"; import * as wxApi from "../wxApi"; import { PermissionsCheckbox, useExtendedPermissions, Diagnostics } from "./welcome"; interface TabProps { target: string; current?: string; children?: ComponentChildren; } function Tab(props: TabProps): JSX.Element { let cssClass = ""; if (props.current === props.target) { cssClass = "active"; } return ( {props.children} ); } function WalletNavBar({ current }: { current?: string }) { 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 / amountFractionalBase; return ( {v}{" "} {amount.currency} ); } function EmptyBalanceView(): JSX.Element { return ( You have no balance to show. Need some{" "} help getting started? ); } class WalletBalanceView extends Component { private balance?: BalancesResponse; private gotError = false; private canceler: (() => void) | undefined = undefined; private unmount = false; private updateBalanceRunning = false; componentWillMount(): void { this.canceler = wxApi.onUpdateNotification(() => this.updateBalance()); this.updateBalance(); } componentWillUnmount(): void { console.log("component WalletBalanceView will unmount"); if (this.canceler) { this.canceler(); } this.unmount = true; } async updateBalance(): Promise { if (this.updateBalanceRunning) { return; } this.updateBalanceRunning = true; let balance: BalancesResponse; try { balance = await wxApi.getBalance(); } catch (e) { if (this.unmount) { return; } this.gotError = true; console.error("could not retrieve balances", e); this.setState({}); return; } finally { this.updateBalanceRunning = false; } if (this.unmount) { return; } this.gotError = false; console.log("got balance", balance); this.balance = balance; this.setState({}); } formatPending(entry: Balance): JSX.Element { let incoming: JSX.Element | undefined; let payment: JSX.Element | undefined; const available = Amounts.parseOrThrow(entry.available); const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming); const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing); console.log( "available: ", entry.pendingIncoming ? renderAmount(entry.available) : null, ); console.log( "incoming: ", entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null, ); if (!Amounts.isZero(pendingIncoming)) { incoming = ( {"+"} {renderAmount(entry.pendingIncoming)} {" "} incoming ); } 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 = wallet.balances.map((entry) => { const av = Amounts.parseOrThrow(entry.available); return (

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

); }); return listing.length > 0 ? (
{listing}
) : ( ); } } interface TransactionAmountProps { debitCreditIndicator: "debit" | "credit" | "unknown"; amount: AmountString | "unknown"; pending: boolean; } function TransactionAmount(props: TransactionAmountProps): JSX.Element { const [currency, amount] = props.amount.split(":"); let sign: string; switch (props.debitCreditIndicator) { case "credit": sign = "+"; break; case "debit": sign = "-"; break; case "unknown": sign = ""; } const style: JSX.AllCSSProperties = { marginLeft: "auto", display: "flex", flexDirection: "column", alignItems: "center", alignSelf: "center" }; if (props.pending) { style.color = "gray"; } return (
{sign} {amount}
{currency}
); } interface TransactionLayoutProps { debitCreditIndicator: "debit" | "credit" | "unknown"; amount: AmountString | "unknown"; timestamp: Timestamp; title: string; id: string; subtitle: string; iconPath: string; pending: boolean; } function TransactionLayout(props: TransactionLayoutProps): JSX.Element { const date = new Date(props.timestamp.t_ms); const dateStr = date.toLocaleString([], { dateStyle: "medium", timeStyle: "short", } as any); return (
{dateStr}
{props.title} {props.pending ? ( (Pending) ) : null}
{props.subtitle}
); } function TransactionItem(props: { tx: Transaction }): JSX.Element { const tx = props.tx; switch (tx.type) { case TransactionType.Withdrawal: return ( ); case TransactionType.Payment: return ( ); case TransactionType.Refund: return ( ); case TransactionType.Tip: return ( ); case TransactionType.Refresh: return ( ); case TransactionType.Deposit: return ( ); } } function WalletHistory(props: any): JSX.Element { const [transactions, setTransactions] = useState< TransactionsResponse | undefined >(undefined); useEffect(() => { const fetchData = async (): Promise => { const res = await wxApi.getTransactions(); setTransactions(res); }; fetchData(); }, []); if (!transactions) { return
Loading ...
; } const txs = [...transactions.transactions].reverse(); return (
{txs.map((tx, i) => ( ))}
); } interface WalletTransactionProps { transaction?: Transaction, onDelete: () => void, onBack: () => void, } export function WalletTransactionView({ transaction, onDelete, onBack }: WalletTransactionProps) { if (!transaction) { return
Loading ...
; } function Footer() { return
} function Pending() { if (!transaction?.pending) return null return (pending...) } function CommonFields() { if (!transaction) return null; return Amount deduce {transaction.amountRaw} Amount received {transaction.amountEffective} Exchange fee {Amounts.stringify( Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), ).amount )} When {transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')} } if (transaction.type === TransactionType.Withdrawal) { return (

Withdrawal

From {transaction.exchangeBaseUrl}

); } if (transaction.type === TransactionType.Payment) { return (

Payment ({transaction.proposalId.substring(0, 10)}...)

To {transaction.info.merchant.name}

{transaction.info.products && transaction.info.products.length > 0 && }
Order id {transaction.info.orderId}
Summary {transaction.info.summary}
Products
    {transaction.info.products.map(p =>
  1. {p.description}
  2. )}
); } if (transaction.type === TransactionType.Deposit) { return (

Deposit ({transaction.depositGroupId})

To {transaction.targetPaytoUri}

); } if (transaction.type === TransactionType.Refresh) { return (

Refresh

From {transaction.exchangeBaseUrl}

); } if (transaction.type === TransactionType.Tip) { return (

Tip

From {transaction.merchantBaseUrl}

); } const TRANSACTION_FROM_REFUND = /[a-z]*:([\w]{10}).*/ if (transaction.type === TransactionType.Refund) { return (

Refund ({TRANSACTION_FROM_REFUND.exec(transaction.refundedTransactionId)![1]}...)

From {transaction.info.merchant.name}

{transaction.info.products && transaction.info.products.length > 0 && }
Order id {transaction.info.orderId}
Summary {transaction.info.summary}
Products
    {transaction.info.products.map(p =>
  1. {p.description}
  2. )}
); } return
} function WalletTransaction({ tid }: { tid: string }): JSX.Element { const [transaction, setTransaction] = useState< Transaction | undefined >(undefined); useEffect(() => { const fetchData = async (): Promise => { const res = await wxApi.getTransactions(); const ts = res.transactions.filter(t => t.transactionId === tid) if (ts.length === 1) { setTransaction(ts[0]); } else { route(Pages.history) } }; fetchData(); }, []); return wxApi.deleteTransaction(tid).then(_ => history.go(-1))} onBack={() => { history.go(-1) }} /> } function WalletSettings() { const [permissionsEnabled, togglePermissions] = useExtendedPermissions() return (

Permissions

{/*

Developer mode

*/}
); } export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean, onToggle: () => void }): JSX.Element { return (
(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)
); } function reload(): void { try { chrome.runtime.reload(); window.close(); } catch (e) { // Functionality missing in firefox, ignore! } } async function confirmReset(): Promise { if ( confirm( "Do you want to IRREVOCABLY DESTROY everything inside your" + " wallet and LOSE ALL YOUR COINS?", ) ) { await 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("static/popup.html#/withdraw", { talerWithdrawUri: talerUri, }); case TalerUriType.TalerPay: return makeExtensionUrlWithParams("static/popup.html#/pay", { talerPayUri: talerUri, }); case TalerUriType.TalerTip: return makeExtensionUrlWithParams("static/popup.html#/tip", { talerTipUri: talerUri, }); case TalerUriType.TalerRefund: return makeExtensionUrlWithParams("static/popup.html#/refund", { 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://'") || document.querySelector("a[href^='taler+http://'"); 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]); }, ); }); } export 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 (
{({ path }: any) => }
); } enum Pages { balance = '/popup/balance', transaction = '/popup/transaction/:tid', settings = '/popup/settings', debug = '/popup/debug', history = '/popup/history', } export function Redirect({ to }: { to: string }): null { useEffect(() => { route(to, true) }) return null }