/*
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,
i18n,
} from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { Component, ComponentChildren, Fragment, JSX } from "preact";
import { route } from 'preact-router';
import { useEffect, useState } from "preact/hooks";
import { Diagnostics } from "../components/Diagnostics";
import { PermissionsCheckbox } from "../components/PermissionsCheckbox";
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
import { PageLink, renderAmount } from "../renderHtml";
import * as wxApi from "../wxApi";
export enum Pages {
balance = '/balance',
settings = '/settings',
debug = '/debug',
history = '/history',
transaction = '/transaction/:tid',
}
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}
);
}
export 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?
);
}
export 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.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 (
);
}
}
export 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...)
}
if (transaction.type === TransactionType.Withdrawal) {
return (
Withdrawal
From {transaction.exchangeBaseUrl}
Amount subtracted
{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.Payment) {
return (
Payment ({transaction.proposalId.substring(0, 10)}...)
To {transaction.info.merchant.name}
Order id
{transaction.info.orderId}
Summary
{transaction.info.summary}
{transaction.info.products && transaction.info.products.length > 0 &&
Products
{transaction.info.products.map(p =>
{p.description}
)}
}
Order amount
{transaction.amountRaw}
Order amount and fees
{transaction.amountEffective}
Exchange fee
{Amounts.stringify(
Amounts.sub(
Amounts.parseOrThrow(transaction.amountEffective),
Amounts.parseOrThrow(transaction.amountRaw),
).amount
)}
When
{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}
);
}
if (transaction.type === TransactionType.Deposit) {
return (
Deposit ({transaction.depositGroupId})
To {transaction.targetPaytoUri}
Amount deposit
{transaction.amountRaw}
Amount deposit and fees
{transaction.amountEffective}
Exchange fee
{Amounts.stringify(
Amounts.sub(
Amounts.parseOrThrow(transaction.amountEffective),
Amounts.parseOrThrow(transaction.amountRaw),
).amount
)}
When
{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}
);
}
if (transaction.type === TransactionType.Refresh) {
return (
Refresh
From {transaction.exchangeBaseUrl}
Amount refreshed
{transaction.amountRaw}
Fees
{transaction.amountEffective}
When
{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}
);
}
if (transaction.type === TransactionType.Tip) {
return (
Tip
From {transaction.merchantBaseUrl}
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')}
);
}
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}
Order id
{transaction.info.orderId}
Summary
{transaction.info.summary}
{transaction.info.products && transaction.info.products.length > 0 &&
Products
{transaction.info.products.map(p =>
{p.description}
)}
}
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')}
);
}
return
}
export 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) }}
/>
}
export function WalletSettings() {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
return (
Permissions
{/*
Developer mode
*/}
);
}
export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean, onToggle: () => void }): JSX.Element {
return (
Automatically open wallet based on page content
(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();
}
}
export function WalletDebug(props: any): JSX.Element {
return (
Debug tools:
wallet tab
reset
reload chrome extension
);
}
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;
}
export function actionForTalerUri(talerUri: string): string | undefined {
const uriType = classifyTalerUri(talerUri);
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
talerWithdrawUri: talerUri,
});
case TalerUriType.TalerPay:
return makeExtensionUrlWithParams("static/wallet.html#/pay", {
talerPayUri: talerUri,
});
case TalerUriType.TalerTip:
return makeExtensionUrlWithParams("static/wallet.html#/tip", {
talerTipUri: talerUri,
});
case TalerUriType.TalerRefund:
return makeExtensionUrlWithParams("static/wallet.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;
}
export 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.
//
// {
// window.open(talerActionUrl, "_blank");
// }}
// >
// Open
//
//
//
// setDismissed(true)}>Dismiss
//
//
// );
// }
// return (
//
//
{({ path }: any) => }
//
//
//
//
//
//
//
//
//
//
// );
// }