2016-01-24 02:29:13 +01:00
|
|
|
/*
|
|
|
|
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
|
2016-07-07 17:59:29 +02:00
|
|
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
2016-01-24 02:29:13 +01:00
|
|
|
*/
|
|
|
|
|
2016-03-01 19:46:20 +01:00
|
|
|
/**
|
|
|
|
* Popup shown to the user when they click
|
|
|
|
* the Taler browser action button.
|
|
|
|
*
|
|
|
|
* @author Florian Dold
|
|
|
|
*/
|
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
2019-12-12 20:53:15 +01:00
|
|
|
import * as i18n from "../i18n";
|
2018-01-03 14:42:06 +01:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
import { AmountJson } from "../../util/amounts";
|
|
|
|
import * as Amounts from "../../util/amounts";
|
2018-01-03 14:42:06 +01:00
|
|
|
|
2020-03-30 12:39:32 +02:00
|
|
|
import { WalletBalance, WalletBalanceEntry } from "../../types/walletTypes";
|
2017-05-28 23:15:41 +02:00
|
|
|
|
2020-04-07 10:07:32 +02:00
|
|
|
import { abbrev, renderAmount, PageLink } from "../renderHtml";
|
2017-06-05 03:20:28 +02:00
|
|
|
import * as wxApi from "../wxApi";
|
2017-05-28 23:15:41 +02:00
|
|
|
|
2020-01-27 14:11:39 +01:00
|
|
|
import React, { Fragment } from "react";
|
2019-12-12 20:53:15 +01:00
|
|
|
import { HistoryEvent } from "../../types/history";
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2020-01-27 14:11:39 +01:00
|
|
|
import moment from "moment";
|
|
|
|
import { Timestamp } from "../../util/time";
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
function onUpdateNotification(f: () => void): () => void {
|
2019-09-05 16:10:53 +02:00
|
|
|
const port = chrome.runtime.connect({ name: "notifications" });
|
2020-04-06 20:02:01 +02:00
|
|
|
const listener = (): void => {
|
2016-02-18 23:41:29 +01:00
|
|
|
f();
|
2016-11-13 08:16:12 +01:00
|
|
|
};
|
|
|
|
port.onMessage.addListener(listener);
|
|
|
|
return () => {
|
|
|
|
port.onMessage.removeListener(listener);
|
2017-05-29 15:18:48 +02:00
|
|
|
};
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
class Router extends React.Component<any, any> {
|
2016-10-10 00:37:08 +02:00
|
|
|
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 {
|
2016-10-13 02:36:33 +02:00
|
|
|
Router.routeHandlers.push(f);
|
2016-10-10 00:37:08 +02:00
|
|
|
return () => {
|
2017-05-29 15:18:48 +02:00
|
|
|
const i = Router.routeHandlers.indexOf(f);
|
2016-10-10 00:37:08 +02:00
|
|
|
this.routeHandlers = this.routeHandlers.splice(i, 1);
|
2017-05-29 15:18:48 +02:00
|
|
|
};
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
private static routeHandlers: any[] = [];
|
2016-10-10 00:37:08 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillMount(): void {
|
2016-10-10 00:37:08 +02:00
|
|
|
console.log("router mounted");
|
|
|
|
window.onhashchange = () => {
|
2016-10-10 02:36:12 +02:00
|
|
|
this.setState({});
|
2017-05-29 15:18:48 +02:00
|
|
|
for (const f of Router.routeHandlers) {
|
2016-10-10 00:37:08 +02:00
|
|
|
f();
|
|
|
|
}
|
2017-05-29 15:18:48 +02:00
|
|
|
};
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
render(): JSX.Element {
|
2017-05-29 15:18:48 +02:00
|
|
|
const route = window.location.hash.substring(1);
|
2016-10-10 00:37:08 +02:00
|
|
|
console.log("rendering route", route);
|
2019-09-05 16:10:53 +02:00
|
|
|
let defaultChild: React.ReactChild | null = null;
|
|
|
|
let foundChild: React.ReactChild | null = null;
|
2020-03-30 12:39:32 +02:00
|
|
|
React.Children.forEach(this.props.children, (child) => {
|
2017-05-29 15:18:48 +02:00
|
|
|
const childProps: any = (child as any).props;
|
2016-11-13 08:16:12 +01:00
|
|
|
if (!childProps) {
|
|
|
|
return;
|
|
|
|
}
|
2017-05-29 15:18:48 +02:00
|
|
|
if (childProps.default) {
|
2019-05-07 23:46:50 +02:00
|
|
|
defaultChild = child as React.ReactChild;
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
2017-05-29 15:18:48 +02:00
|
|
|
if (childProps.route === route) {
|
2019-05-07 23:46:50 +02:00
|
|
|
foundChild = child as React.ReactChild;
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
2017-05-29 15:18:48 +02:00
|
|
|
});
|
2017-10-15 19:28:35 +02:00
|
|
|
const c: React.ReactChild | null = foundChild || defaultChild;
|
|
|
|
if (!c) {
|
2016-10-10 00:37:08 +02:00
|
|
|
throw Error("unknown route");
|
|
|
|
}
|
2017-10-15 19:28:35 +02:00
|
|
|
Router.setRoute((c as any).props.route);
|
|
|
|
return <div>{c}</div>;
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
interface TabProps {
|
2016-10-10 00:37:08 +02:00
|
|
|
target: string;
|
2016-11-13 08:16:12 +01:00
|
|
|
children?: React.ReactNode;
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function Tab(props: TabProps): JSX.Element {
|
2016-01-24 02:29:13 +01:00
|
|
|
let cssClass = "";
|
2017-05-29 15:18:48 +02:00
|
|
|
if (props.target === Router.getRoute()) {
|
2016-01-24 02:29:13 +01:00
|
|
|
cssClass = "active";
|
|
|
|
}
|
2020-04-06 20:02:01 +02:00
|
|
|
const onClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
|
2016-10-10 00:37:08 +02:00
|
|
|
Router.setRoute(props.target);
|
|
|
|
e.preventDefault();
|
|
|
|
};
|
|
|
|
return (
|
|
|
|
<a onClick={onClick} href={props.target} className={cssClass}>
|
|
|
|
{props.children}
|
|
|
|
</a>
|
|
|
|
);
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
class WalletNavBar extends React.Component<any, any> {
|
|
|
|
private cancelSubscription: any;
|
2016-10-10 00:37:08 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillMount(): void {
|
2016-10-10 00:37:08 +02:00
|
|
|
this.cancelSubscription = Router.onRoute(() => {
|
|
|
|
this.setState({});
|
|
|
|
});
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillUnmount(): void {
|
2016-10-10 00:37:08 +02:00
|
|
|
if (this.cancelSubscription) {
|
|
|
|
this.cancelSubscription();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
render(): JSX.Element {
|
2016-10-10 00:37:08 +02:00
|
|
|
console.log("rendering nav bar");
|
|
|
|
return (
|
2016-11-13 08:16:12 +01:00
|
|
|
<div className="nav" id="header">
|
2019-09-05 16:10:53 +02:00
|
|
|
<Tab target="/balance">{i18n.str`Balance`}</Tab>
|
|
|
|
<Tab target="/history">{i18n.str`History`}</Tab>
|
|
|
|
<Tab target="/debug">{i18n.str`Debug`}</Tab>
|
|
|
|
</div>
|
|
|
|
);
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
2016-11-28 08:19:06 +01:00
|
|
|
return (
|
|
|
|
<span>
|
2020-03-30 12:39:32 +02:00
|
|
|
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
|
2016-11-28 08:19:06 +01:00
|
|
|
<span>{amount.currency}</span>
|
2019-09-05 16:10:53 +02:00
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function EmptyBalanceView(): JSX.Element {
|
2019-09-05 16:10:53 +02:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<i18n.Translate wrap="p">
|
|
|
|
You have no balance to show. Need some{" "}
|
|
|
|
<PageLink pageName="welcome.html">help</PageLink> getting started?
|
|
|
|
</i18n.Translate>
|
2016-11-28 08:19:06 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
class WalletBalanceView extends React.Component<any, any> {
|
2017-05-29 15:18:48 +02:00
|
|
|
private balance: WalletBalance;
|
|
|
|
private gotError = false;
|
|
|
|
private canceler: (() => void) | undefined = undefined;
|
|
|
|
private unmount = false;
|
2016-09-14 15:20:18 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillMount(): void {
|
2016-11-13 08:16:12 +01:00
|
|
|
this.canceler = onUpdateNotification(() => this.updateBalance());
|
2016-10-10 00:37:08 +02:00
|
|
|
this.updateBalance();
|
2016-11-13 08:16:12 +01:00
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillUnmount(): void {
|
2016-11-13 08:16:12 +01:00
|
|
|
console.log("component WalletBalanceView will unmount");
|
|
|
|
if (this.canceler) {
|
|
|
|
this.canceler();
|
|
|
|
}
|
|
|
|
this.unmount = true;
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
async updateBalance(): Promise<void> {
|
2017-08-14 04:16:12 +02:00
|
|
|
let balance: WalletBalance;
|
|
|
|
try {
|
|
|
|
balance = await wxApi.getBalance();
|
|
|
|
} catch (e) {
|
2016-11-13 08:16:12 +01:00
|
|
|
if (this.unmount) {
|
|
|
|
return;
|
|
|
|
}
|
2017-08-14 04:16:12 +02:00
|
|
|
this.gotError = true;
|
|
|
|
console.error("could not retrieve balances", e);
|
2016-10-18 02:40:46 +02:00
|
|
|
this.setState({});
|
2017-08-14 04:16:12 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.unmount) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.gotError = false;
|
|
|
|
console.log("got balance", balance);
|
|
|
|
this.balance = balance;
|
|
|
|
this.setState({});
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
|
|
|
|
2016-10-19 22:49:03 +02:00
|
|
|
formatPending(entry: WalletBalanceEntry): JSX.Element {
|
|
|
|
let incoming: JSX.Element | undefined;
|
|
|
|
let payment: JSX.Element | undefined;
|
|
|
|
|
2019-09-05 16:10:53 +02:00
|
|
|
console.log(
|
|
|
|
"available: ",
|
|
|
|
entry.pendingIncoming ? renderAmount(entry.available) : null,
|
|
|
|
);
|
|
|
|
console.log(
|
|
|
|
"incoming: ",
|
|
|
|
entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
|
|
|
|
);
|
2016-10-19 22:49:03 +02:00
|
|
|
|
|
|
|
if (Amounts.isNonZero(entry.pendingIncoming)) {
|
|
|
|
incoming = (
|
2016-11-23 01:14:45 +01:00
|
|
|
<i18n.Translate wrap="span">
|
2019-09-05 16:10:53 +02:00
|
|
|
<span style={{ color: "darkgreen" }}>
|
2016-10-19 22:49:03 +02:00
|
|
|
{"+"}
|
2017-06-04 17:56:55 +02:00
|
|
|
{renderAmount(entry.pendingIncoming)}
|
2019-09-05 16:10:53 +02:00
|
|
|
</span>{" "}
|
2016-10-19 22:49:03 +02:00
|
|
|
incoming
|
2019-09-05 16:10:53 +02:00
|
|
|
</i18n.Translate>
|
2016-11-23 01:14:45 +01:00
|
|
|
);
|
2016-10-19 22:49:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (Amounts.isNonZero(entry.pendingPayment)) {
|
|
|
|
payment = (
|
2016-11-23 01:14:45 +01:00
|
|
|
<i18n.Translate wrap="span">
|
2019-09-05 16:10:53 +02:00
|
|
|
<span style={{ color: "red" }}>
|
2018-02-05 13:26:06 +01:00
|
|
|
{"-"}
|
2017-06-04 17:56:55 +02:00
|
|
|
{renderAmount(entry.pendingPayment)}
|
2019-09-05 16:10:53 +02:00
|
|
|
</span>{" "}
|
2016-10-19 22:49:03 +02:00
|
|
|
being spent
|
2016-11-23 01:14:45 +01:00
|
|
|
</i18n.Translate>
|
|
|
|
);
|
2016-10-19 22:49:03 +02:00
|
|
|
}
|
|
|
|
|
2020-03-30 12:39:32 +02:00
|
|
|
const l = [incoming, payment].filter((x) => x !== undefined);
|
2017-05-29 15:18:48 +02:00
|
|
|
if (l.length === 0) {
|
2016-10-19 22:49:03 +02:00
|
|
|
return <span />;
|
|
|
|
}
|
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
if (l.length === 1) {
|
|
|
|
return <span>({l})</span>;
|
2016-10-19 22:49:03 +02:00
|
|
|
}
|
2019-09-05 16:10:53 +02:00
|
|
|
return (
|
|
|
|
<span>
|
|
|
|
({l[0]}, {l[1]})
|
|
|
|
</span>
|
|
|
|
);
|
2016-10-10 03:16:12 +02:00
|
|
|
}
|
|
|
|
|
2016-10-10 00:37:08 +02:00
|
|
|
render(): JSX.Element {
|
2017-05-29 15:18:48 +02:00
|
|
|
const wallet = this.balance;
|
2016-10-10 00:37:08 +02:00
|
|
|
if (this.gotError) {
|
2019-09-05 16:10:53 +02:00
|
|
|
return (
|
2020-03-30 12:39:32 +02:00
|
|
|
<div className="balance">
|
2019-09-05 16:10:53 +02:00
|
|
|
<p>{i18n.str`Error: could not retrieve balance information.`}</p>
|
|
|
|
<p>
|
2019-09-05 16:23:54 +02:00
|
|
|
Click <PageLink pageName="welcome.html">here</PageLink> for help and
|
|
|
|
diagnostics.
|
2019-09-05 16:10:53 +02:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
);
|
2016-03-02 00:47:00 +01:00
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
if (!wallet) {
|
2016-10-20 01:37:00 +02:00
|
|
|
return <span></span>;
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-10-10 00:37:08 +02:00
|
|
|
console.log(wallet);
|
2020-03-30 12:39:32 +02:00
|
|
|
const listing = Object.keys(wallet.byCurrency).map((key) => {
|
2017-08-14 04:16:12 +02:00
|
|
|
const entry: WalletBalanceEntry = wallet.byCurrency[key];
|
2016-10-19 18:40:29 +02:00
|
|
|
return (
|
|
|
|
<p>
|
2019-09-05 16:10:53 +02:00
|
|
|
{bigAmount(entry.available)} {this.formatPending(entry)}
|
2016-10-19 18:40:29 +02:00
|
|
|
</p>
|
|
|
|
);
|
2016-10-10 00:37:08 +02:00
|
|
|
});
|
2020-03-30 12:39:32 +02:00
|
|
|
return listing.length > 0 ? (
|
|
|
|
<div className="balance">{listing}</div>
|
|
|
|
) : (
|
|
|
|
<EmptyBalanceView />
|
|
|
|
);
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function Icon({ l }: { l: string }): JSX.Element {
|
2020-01-27 14:11:39 +01:00
|
|
|
return <div className={"icon"}>{l}</div>;
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function formatAndCapitalize(text: string): string {
|
2020-01-27 14:11:39 +01:00
|
|
|
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,
|
2020-03-30 12:39:32 +02:00
|
|
|
negative = false,
|
2020-04-06 20:02:01 +02:00
|
|
|
}: HistoryItemProps): JSX.Element {
|
|
|
|
function formatDate(timestamp: number | "never"): string | null {
|
2020-01-27 14:11:39 +01:00
|
|
|
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 (
|
|
|
|
<div className="historyItem">
|
|
|
|
{icon ? <Icon l={icon} /> : null}
|
|
|
|
<div className="historyContent">
|
|
|
|
{title ? <div className={"historyTitle"}>{title}</div> : null}
|
|
|
|
{text ? <div className={"historyText"}>{text}</div> : null}
|
|
|
|
{small ? <div className={"historySmall"}>{small}</div> : null}
|
|
|
|
</div>
|
|
|
|
<div className={"historyLeft"}>
|
|
|
|
<div className={"historyAmount"}>
|
|
|
|
{amountElement ? (
|
|
|
|
<div className={`${negative ? "negative" : "positive"}`}>
|
|
|
|
{amountElement}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{invalidElement ? (
|
|
|
|
<div className={"secondary"}>
|
|
|
|
{i18n.str`Invalid `}{" "}
|
|
|
|
<span className={"negative"}>{invalidElement}</span>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{feesElement ? (
|
|
|
|
<div className={"secondary"}>
|
|
|
|
{i18n.str`Fees `}{" "}
|
|
|
|
<span className={"negative"}>{feesElement}</span>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</div>
|
|
|
|
<div className="historyDate">{formatDate(timestamp.t_ms)}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function amountDiff(
|
|
|
|
total: string | Amounts.AmountJson,
|
2020-03-30 12:39:32 +02:00
|
|
|
partial: string | Amounts.AmountJson,
|
2020-01-27 14:11:39 +01:00
|
|
|
): Amounts.AmountJson | string {
|
2020-04-06 17:45:41 +02:00
|
|
|
const a = typeof total === "string" ? Amounts.parse(total) : total;
|
|
|
|
const b = typeof partial === "string" ? Amounts.parse(partial) : partial;
|
2020-01-27 14:11:39 +01:00
|
|
|
if (a && b) {
|
|
|
|
return Amounts.sub(a, b).amount;
|
|
|
|
} else {
|
|
|
|
return total;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseSummary(summary: string) {
|
2020-04-06 17:45:41 +02:00
|
|
|
const parsed = summary.split(/: (.+)/);
|
2020-01-27 14:11:39 +01:00
|
|
|
return {
|
|
|
|
merchant: parsed[0],
|
2020-03-30 12:39:32 +02:00
|
|
|
item: parsed[1],
|
2020-01-27 14:11:39 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function formatHistoryItem(historyItem: HistoryEvent): JSX.Element {
|
2016-01-24 02:29:13 +01:00
|
|
|
switch (historyItem.type) {
|
2020-01-27 14:11:39 +01:00
|
|
|
case "refreshed": {
|
2016-10-10 00:37:08 +02:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
small={i18n.str`Refresh sessions has completed`}
|
|
|
|
fees={amountDiff(
|
|
|
|
historyItem.amountRefreshedRaw,
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.amountRefreshedEffective,
|
2020-01-27 14:11:39 +01:00
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
case "order-refused": {
|
|
|
|
const { merchant, item } = parseSummary(
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.orderShortInfo.summary,
|
2016-10-10 00:37:08 +02:00
|
|
|
);
|
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
icon={"X"}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
small={i18n.str`Order Refused`}
|
|
|
|
title={merchant}
|
|
|
|
text={abbrev(item, 30)}
|
|
|
|
/>
|
2016-10-10 00:37:08 +02:00
|
|
|
);
|
2016-10-10 03:16:12 +02:00
|
|
|
}
|
2020-01-27 14:11:39 +01:00
|
|
|
|
|
|
|
case "order-redirected": {
|
|
|
|
const { merchant, item } = parseSummary(
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.newOrderShortInfo.summary,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
2016-10-10 00:37:08 +02:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
icon={"⟲"}
|
|
|
|
small={i18n.str`Order redirected`}
|
|
|
|
text={abbrev(item, 40)}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={merchant}
|
|
|
|
/>
|
2016-10-10 00:37:08 +02:00
|
|
|
);
|
2016-09-29 01:40:29 +02:00
|
|
|
}
|
2020-01-27 14:11:39 +01:00
|
|
|
|
|
|
|
case "payment-aborted": {
|
|
|
|
const { merchant, item } = parseSummary(
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.orderShortInfo.summary,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
2016-11-23 01:14:45 +01:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
amount={historyItem.orderShortInfo.amount}
|
|
|
|
fees={historyItem.amountLost}
|
|
|
|
icon={"P"}
|
|
|
|
small={i18n.str`Payment aborted`}
|
|
|
|
text={abbrev(item, 40)}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={merchant}
|
|
|
|
/>
|
2016-11-23 01:14:45 +01:00
|
|
|
);
|
2016-10-10 03:16:12 +02:00
|
|
|
}
|
2020-01-27 14:11:39 +01:00
|
|
|
|
|
|
|
case "payment-sent": {
|
2020-01-31 15:22:13 +01:00
|
|
|
const url = historyItem.orderShortInfo.fulfillmentUrl;
|
2020-01-27 14:11:39 +01:00
|
|
|
const { merchant, item } = parseSummary(
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.orderShortInfo.summary,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
|
|
|
const fees = amountDiff(
|
|
|
|
historyItem.amountPaidWithFees,
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.orderShortInfo.amount,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
2019-09-05 16:10:53 +02:00
|
|
|
const fulfillmentLinkElem = (
|
2020-01-27 14:11:39 +01:00
|
|
|
<Fragment>
|
2020-03-30 12:39:32 +02:00
|
|
|
<a href={url} onClick={openTab(url)}>
|
2020-01-27 14:11:39 +01:00
|
|
|
{item ? abbrev(item, 30) : null}
|
|
|
|
</a>
|
|
|
|
</Fragment>
|
2019-09-05 16:10:53 +02:00
|
|
|
);
|
2016-10-10 00:37:08 +02:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
amount={historyItem.orderShortInfo.amount}
|
|
|
|
fees={fees}
|
|
|
|
icon={"P"}
|
|
|
|
negative={true}
|
|
|
|
small={i18n.str`Payment Sent`}
|
|
|
|
text={fulfillmentLinkElem}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={merchant}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
case "order-accepted": {
|
2020-01-31 15:22:13 +01:00
|
|
|
const url = historyItem.orderShortInfo.fulfillmentUrl;
|
2020-01-27 14:11:39 +01:00
|
|
|
const { merchant, item } = parseSummary(
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.orderShortInfo.summary,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
|
|
|
const fulfillmentLinkElem = (
|
|
|
|
<Fragment>
|
2020-03-30 12:39:32 +02:00
|
|
|
<a href={url} onClick={openTab(url)}>
|
2020-01-27 14:11:39 +01:00
|
|
|
{item ? abbrev(item, 40) : null}
|
|
|
|
</a>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
<HistoryItem
|
|
|
|
negative={true}
|
|
|
|
amount={historyItem.orderShortInfo.amount}
|
|
|
|
icon={"P"}
|
|
|
|
small={i18n.str`Order accepted`}
|
|
|
|
text={fulfillmentLinkElem}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={merchant}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
case "reserve-balance-updated": {
|
|
|
|
return (
|
|
|
|
<HistoryItem
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
small={i18n.str`Reserve balance updated`}
|
|
|
|
/>
|
2016-11-26 17:27:33 +01:00
|
|
|
);
|
2016-09-29 01:40:29 +02:00
|
|
|
}
|
2017-10-15 18:30:02 +02:00
|
|
|
case "refund": {
|
2020-01-27 14:11:39 +01:00
|
|
|
const merchantElem = (
|
|
|
|
<em>{abbrev(historyItem.orderShortInfo.summary, 25)}</em>
|
|
|
|
);
|
2017-10-15 18:30:02 +02:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
icon={"R"}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
small={i18n.str`Payment refund`}
|
|
|
|
text={merchantElem}
|
|
|
|
amount={historyItem.amountRefundedRaw}
|
|
|
|
invalid={historyItem.amountRefundedInvalid}
|
|
|
|
fees={amountDiff(
|
|
|
|
amountDiff(
|
|
|
|
historyItem.amountRefundedRaw,
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.amountRefundedInvalid,
|
2020-01-27 14:11:39 +01:00
|
|
|
),
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.amountRefundedEffective,
|
2020-01-27 14:11:39 +01:00
|
|
|
)}
|
|
|
|
/>
|
2017-10-15 18:30:02 +02:00
|
|
|
);
|
|
|
|
}
|
2020-01-27 14:11:39 +01:00
|
|
|
case "withdrawn": {
|
|
|
|
const exchange = new URL(historyItem.exchangeBaseUrl).host;
|
|
|
|
const fees = amountDiff(
|
|
|
|
historyItem.amountWithdrawnRaw,
|
2020-03-30 12:39:32 +02:00
|
|
|
historyItem.amountWithdrawnEffective,
|
2020-01-27 14:11:39 +01:00
|
|
|
);
|
|
|
|
return (
|
|
|
|
<HistoryItem
|
|
|
|
amount={historyItem.amountWithdrawnRaw}
|
|
|
|
fees={fees}
|
|
|
|
icon={"w"}
|
|
|
|
small={i18n.str`Withdrawn`}
|
|
|
|
title={exchange}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
case "tip-accepted": {
|
|
|
|
return (
|
|
|
|
<HistoryItem
|
|
|
|
icon={"T"}
|
|
|
|
negative={true}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={<i18n.Translate wrap={Fragment}>Tip Accepted</i18n.Translate>}
|
|
|
|
amount={historyItem.tipAmountRaw}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
case "tip-declined": {
|
2017-12-12 15:38:03 +01:00
|
|
|
return (
|
2020-01-27 14:11:39 +01:00
|
|
|
<HistoryItem
|
|
|
|
icon={"T"}
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
title={<i18n.Translate wrap={Fragment}>Tip Declined</i18n.Translate>}
|
|
|
|
amount={historyItem.tipAmountRaw}
|
|
|
|
/>
|
2017-12-12 15:38:03 +01:00
|
|
|
);
|
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
default:
|
2020-01-27 14:11:39 +01:00
|
|
|
return (
|
|
|
|
<HistoryItem
|
|
|
|
timestamp={historyItem.timestamp}
|
|
|
|
small={i18n.str`${formatAndCapitalize(historyItem.type)}`}
|
|
|
|
/>
|
|
|
|
);
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
const HistoryComponent = (props: any): JSX.Element => {
|
2020-01-27 14:11:39 +01:00
|
|
|
const record = props.record;
|
|
|
|
return formatHistoryItem(record);
|
|
|
|
};
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
class WalletHistory extends React.Component<any, any> {
|
2017-05-29 15:18:48 +02:00
|
|
|
private myHistory: any[];
|
|
|
|
private gotError = false;
|
|
|
|
private unmounted = false;
|
2020-01-27 14:11:39 +01:00
|
|
|
private hidenTypes: string[] = [
|
|
|
|
"order-accepted",
|
|
|
|
"order-redirected",
|
|
|
|
"refreshed",
|
|
|
|
"reserve-balance-updated",
|
|
|
|
"exchange-updated",
|
2020-03-30 12:39:32 +02:00
|
|
|
"exchange-added",
|
2020-01-27 14:11:39 +01:00
|
|
|
];
|
2020-03-30 12:39:32 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillMount(): void {
|
2016-10-10 00:37:08 +02:00
|
|
|
this.update();
|
2020-01-27 14:11:39 +01:00
|
|
|
this.setState({ filter: true });
|
2016-10-10 00:37:08 +02:00
|
|
|
onUpdateNotification(() => this.update());
|
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
componentWillUnmount(): void {
|
2016-11-13 08:16:12 +01:00
|
|
|
console.log("history component unmounted");
|
|
|
|
this.unmounted = true;
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
update(): void {
|
2020-03-30 12:39:32 +02:00
|
|
|
chrome.runtime.sendMessage({ type: "get-history" }, (resp) => {
|
2016-11-13 08:16:12 +01:00
|
|
|
if (this.unmounted) {
|
|
|
|
return;
|
|
|
|
}
|
2016-10-10 00:37:08 +02:00
|
|
|
console.log("got history response");
|
|
|
|
if (resp.error) {
|
|
|
|
this.gotError = true;
|
|
|
|
console.error("could not retrieve history", resp);
|
2016-10-18 02:58:46 +02:00
|
|
|
this.setState({});
|
2016-10-10 00:37:08 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.gotError = false;
|
|
|
|
console.log("got history", resp.history);
|
|
|
|
this.myHistory = resp.history;
|
2016-10-18 02:58:46 +02:00
|
|
|
this.setState({});
|
2016-10-10 00:37:08 +02:00
|
|
|
});
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
|
|
|
|
2016-10-10 00:37:08 +02:00
|
|
|
render(): JSX.Element {
|
|
|
|
console.log("rendering history");
|
2019-11-21 23:09:43 +01:00
|
|
|
const history: HistoryEvent[] = this.myHistory;
|
2016-10-10 00:37:08 +02:00
|
|
|
if (this.gotError) {
|
2020-04-07 10:07:32 +02:00
|
|
|
return <span>i18n.str`Error: could not retrieve event history`</span>;
|
2016-03-02 00:47:00 +01:00
|
|
|
}
|
2016-10-10 00:37:08 +02:00
|
|
|
|
2016-01-24 02:29:13 +01:00
|
|
|
if (!history) {
|
2016-10-10 00:37:08 +02:00
|
|
|
// We're not ready yet
|
|
|
|
return <span />;
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-09-28 23:41:34 +02:00
|
|
|
|
2017-05-29 15:18:48 +02:00
|
|
|
const listing: any[] = [];
|
2020-03-30 12:39:32 +02:00
|
|
|
const messages = history.reverse().filter((hEvent) => {
|
|
|
|
if (!this.state.filter) return true;
|
|
|
|
return this.hidenTypes.indexOf(hEvent.type) === -1;
|
|
|
|
});
|
2020-01-27 14:11:39 +01:00
|
|
|
|
|
|
|
for (const record of messages) {
|
2020-03-30 12:39:32 +02:00
|
|
|
const item = <HistoryComponent key={record.eventId} record={record} />;
|
2016-09-29 01:40:29 +02:00
|
|
|
listing.push(item);
|
2016-09-28 23:41:34 +02:00
|
|
|
}
|
|
|
|
|
2016-01-24 02:29:13 +01:00
|
|
|
if (listing.length > 0) {
|
2020-01-27 14:11:39 +01:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div className="container">{listing}</div>
|
|
|
|
<div className="option">
|
|
|
|
Filtered list{" "}
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={this.state.filter}
|
|
|
|
onChange={() => this.setState({ filter: !this.state.filter })}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2017-05-29 15:18:48 +02:00
|
|
|
return <p>{i18n.str`Your wallet has no events recorded.`}</p>;
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-02-18 23:41:29 +01:00
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function reload(): void {
|
2016-01-26 17:21:17 +01:00
|
|
|
try {
|
|
|
|
chrome.runtime.reload();
|
|
|
|
window.close();
|
|
|
|
} catch (e) {
|
|
|
|
// Functionality missing in firefox, ignore!
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function confirmReset(): void {
|
2019-09-05 16:10:53 +02:00
|
|
|
if (
|
|
|
|
confirm(
|
|
|
|
"Do you want to IRREVOCABLY DESTROY everything inside your" +
|
|
|
|
" wallet and LOSE ALL YOUR COINS?",
|
|
|
|
)
|
|
|
|
) {
|
2017-06-05 03:20:28 +02:00
|
|
|
wxApi.resetDb();
|
2016-01-24 02:29:13 +01:00
|
|
|
window.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function WalletDebug(props: any): JSX.Element {
|
2019-09-05 16:10:53 +02:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p>Debug tools:</p>
|
2020-04-07 10:07:32 +02:00
|
|
|
<button onClick={openExtensionPage("/popup.html")}>wallet tab</button>
|
|
|
|
<button onClick={openExtensionPage("/benchmark.html")}>benchmark</button>
|
|
|
|
<button onClick={openExtensionPage("/show-db.html")}>show db</button>
|
|
|
|
<button onClick={openExtensionPage("/tree.html")}>show tree</button>
|
2019-09-05 16:10:53 +02:00
|
|
|
<br />
|
|
|
|
<button onClick={confirmReset}>reset</button>
|
|
|
|
<button onClick={reload}>reload chrome extension</button>
|
|
|
|
</div>
|
|
|
|
);
|
2016-10-10 00:37:08 +02:00
|
|
|
}
|
2016-01-24 02:29:13 +01:00
|
|
|
|
2016-09-12 20:25:56 +02:00
|
|
|
function openExtensionPage(page: string) {
|
2017-05-29 15:18:48 +02:00
|
|
|
return () => {
|
2016-01-26 17:21:17 +01:00
|
|
|
chrome.tabs.create({
|
2017-05-29 15:18:48 +02:00
|
|
|
url: chrome.extension.getURL(page),
|
2016-11-26 17:27:33 +01:00
|
|
|
});
|
2017-05-29 15:18:48 +02:00
|
|
|
};
|
2016-01-24 02:29:13 +01:00
|
|
|
}
|
2016-01-26 17:21:17 +01:00
|
|
|
|
2016-09-12 20:25:56 +02:00
|
|
|
function openTab(page: string) {
|
2017-06-05 02:00:03 +02:00
|
|
|
return (evt: React.SyntheticEvent<any>) => {
|
2017-06-05 00:52:22 +02:00
|
|
|
evt.preventDefault();
|
2016-02-01 15:10:20 +01:00
|
|
|
chrome.tabs.create({
|
2017-05-29 15:18:48 +02:00
|
|
|
url: page,
|
2016-11-26 17:27:33 +01:00
|
|
|
});
|
2017-05-29 15:18:48 +02:00
|
|
|
};
|
2016-02-01 15:10:20 +01:00
|
|
|
}
|
2017-04-20 03:09:25 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
function WalletPopup(): JSX.Element {
|
2019-09-05 16:23:54 +02:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<WalletNavBar />
|
|
|
|
<div style={{ margin: "1em" }}>
|
|
|
|
<Router>
|
|
|
|
<WalletBalanceView route="/balance" default />
|
|
|
|
<WalletHistory route="/history" />
|
|
|
|
<WalletDebug route="/debug" />
|
|
|
|
</Router>
|
|
|
|
</div>
|
2017-04-20 03:09:25 +02:00
|
|
|
</div>
|
2019-09-05 16:23:54 +02:00
|
|
|
);
|
|
|
|
}
|
2017-04-20 03:09:25 +02:00
|
|
|
|
2020-04-06 20:02:01 +02:00
|
|
|
export function createPopup(): JSX.Element {
|
2019-09-05 16:10:53 +02:00
|
|
|
chrome.runtime.connect({ name: "popup" });
|
2020-03-30 12:39:32 +02:00
|
|
|
return <WalletPopup />;
|
2020-04-07 10:07:32 +02:00
|
|
|
}
|