wallet-core/src/webex/pages/popup.tsx

576 lines
15 KiB
TypeScript
Raw Normal View History

/*
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-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.
*/
import * as i18n from "../../i18n";
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
2016-10-19 18:40:29 +02:00
import {
2017-05-30 18:33:28 +02:00
HistoryRecord,
WalletBalance,
2017-05-29 15:18:48 +02:00
WalletBalanceEntry,
} from "../../walletTypes";
2017-06-04 17:56:55 +02:00
import { abbrev, renderAmount } from "../renderHtml";
2017-06-05 03:20:28 +02:00
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 {
2017-05-29 15:18:48 +02:00
const port = chrome.runtime.connect({name: "notifications"});
const listener = () => {
2016-02-18 23:41:29 +01:00
f();
};
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
componentWillMount() {
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
}
componentWillUnmount() {
console.log("router unmounted");
}
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);
let defaultChild: React.ReactChild|null = null;
let foundChild: React.ReactChild|null = null;
React.Children.forEach(this.props.children, (child) => {
2017-05-29 15:18:48 +02:00
const childProps: any = (child as any).props;
if (!childProps) {
return;
}
2017-05-29 15:18:48 +02:00
if (childProps.default) {
2016-10-10 00:37:08 +02:00
defaultChild = child;
}
2017-05-29 15:18:48 +02:00
if (childProps.route === route) {
foundChild = child;
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
}
}
interface TabProps {
2016-10-10 00:37:08 +02:00
target: string;
children?: React.ReactNode;
2016-10-10 00:37:08 +02:00
}
2016-10-10 00:37:08 +02:00
function Tab(props: TabProps) {
let cssClass = "";
2017-05-29 15:18:48 +02:00
if (props.target === Router.getRoute()) {
cssClass = "active";
}
2017-05-29 15:18:48 +02:00
const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
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-10-10 00:37:08 +02: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
componentWillMount() {
this.cancelSubscription = Router.onRoute(() => {
this.setState({});
});
}
2016-02-18 23:41:29 +01:00
2016-10-10 00:37:08 +02:00
componentWillUnmount() {
if (this.cancelSubscription) {
this.cancelSubscription();
}
}
render() {
console.log("rendering nav bar");
return (
<div className="nav" id="header">
2016-10-10 00:37:08 +02:00
<Tab target="/balance">
2016-11-27 22:13:24 +01:00
{i18n.str`Balance`}
2016-10-10 00:37:08 +02:00
</Tab>
<Tab target="/history">
2016-11-27 22:13:24 +01:00
{i18n.str`History`}
2016-10-10 00:37:08 +02:00
</Tab>
<Tab target="/debug">
2016-11-27 22:13:24 +01:00
{i18n.str`Debug`}
2016-10-10 00:37:08 +02:00
</Tab>
</div>);
2016-02-18 23:41:29 +01:00
}
}
2016-10-10 00:37:08 +02:00
function ExtensionLink(props: any) {
2017-05-29 15:18:48 +02:00
const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
chrome.tabs.create({
2017-05-29 15:18:48 +02:00
url: chrome.extension.getURL(props.target),
});
e.preventDefault();
2016-10-10 00:37:08 +02:00
};
return (
2016-10-10 03:16:12 +02:00
<a onClick={onClick} href={props.target}>
2016-10-10 00:37:08 +02:00
{props.children}
2017-05-29 15:18:48 +02:00
</a>
);
}
2016-11-28 08:19:06 +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>
<span style={{fontSize: "300%"}}>{v}</span>
{" "}
<span>{amount.currency}</span>
</span>
);
}
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
2016-10-10 00:37:08 +02:00
componentWillMount() {
this.canceler = onUpdateNotification(() => this.updateBalance());
2016-10-10 00:37:08 +02:00
this.updateBalance();
}
2016-02-18 23:41:29 +01:00
componentWillUnmount() {
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
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);
2016-10-18 02:40:46 +02:00
this.setState({});
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 18:40:29 +02:00
renderEmpty(): JSX.Element {
2017-05-29 15:18:48 +02:00
const helpLink = (
<ExtensionLink target="/src/webex/pages/help/empty-wallet.html">
2016-11-27 22:13:24 +01:00
{i18n.str`help`}
2016-10-10 03:16:12 +02:00
</ExtensionLink>
);
2016-11-17 02:58:27 +01:00
return (
<div>
2017-04-29 00:05:39 +02:00
<i18n.Translate wrap="p">
2016-11-17 02:58:27 +01:00
You have no balance to show. Need some
2016-11-23 01:14:45 +01:00
{" "}<span>{helpLink}</span>{" "}
2016-11-17 02:58:27 +01:00
getting started?
</i18n.Translate>
</div>
);
2016-10-19 18:40:29 +02:00
}
formatPending(entry: WalletBalanceEntry): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
2017-06-04 17:56:55 +02:00
console.log("available: ", entry.pendingIncoming ? renderAmount(entry.available) : null);
console.log("incoming: ", entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null);
if (Amounts.isNonZero(entry.pendingIncoming)) {
incoming = (
2016-11-23 01:14:45 +01:00
<i18n.Translate wrap="span">
<span style={{color: "darkgreen"}}>
{"+"}
2017-06-04 17:56:55 +02:00
{renderAmount(entry.pendingIncoming)}
</span>
{" "}
incoming
2016-11-23 01:14:45 +01:00
</i18n.Translate>
);
}
if (Amounts.isNonZero(entry.pendingPayment)) {
payment = (
2016-11-23 01:14:45 +01:00
<i18n.Translate wrap="span">
<span style={{color: "darkblue"}}>
2017-06-04 17:56:55 +02:00
{renderAmount(entry.pendingPayment)}
</span>
{" "}
being spent
2016-11-23 01:14:45 +01:00
</i18n.Translate>
);
}
2017-05-29 15:18:48 +02:00
const l = [incoming, payment].filter((x) => x !== undefined);
if (l.length === 0) {
return <span />;
}
2017-05-29 15:18:48 +02:00
if (l.length === 1) {
return <span>({l})</span>;
}
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) {
2016-11-27 22:13:24 +01:00
return i18n.str`Error: could not retrieve balance information.`;
2016-03-02 00:47:00 +01:00
}
if (!wallet) {
2016-10-20 01:37:00 +02:00
return <span></span>;
}
2016-10-10 00:37:08 +02:00
console.log(wallet);
let paybackAvailable = false;
const listing = Object.keys(wallet.byCurrency).map((key) => {
const entry: WalletBalanceEntry = wallet.byCurrency[key];
2017-05-29 15:18:48 +02:00
if (entry.paybackAmount.value !== 0 || entry.paybackAmount.fraction !== 0) {
paybackAvailable = true;
}
2016-10-19 18:40:29 +02:00
return (
<p>
2016-11-28 08:19:06 +01:00
{bigAmount(entry.available)}
{" "}
{this.formatPending(entry)}
2016-10-19 18:40:29 +02:00
</p>
);
2016-10-10 00:37:08 +02:00
});
const makeLink = (page: string, name: string) => {
const url = chrome.extension.getURL(`/src/webex/pages/${page}`);
return <div><a className="actionLink" href={url} target="_blank">{name}</a></div>;
};
return (
<div>
{listing.length > 0 ? listing : this.renderEmpty()}
{paybackAvailable && makeLink("payback", i18n.str`Payback`)}
{makeLink("return-coins.html#dissolve", i18n.str`Return Electronic Cash to Bank Account`)}
{makeLink("auditors.html", i18n.str`Manage Trusted Auditors and Exchanges`)}
</div>
);
}
2016-02-18 23:41:29 +01:00
}
2016-09-28 23:41:34 +02:00
function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail;
2016-02-22 23:21:41 +01:00
console.log("hist item", historyItem);
switch (historyItem.type) {
case "create-reserve":
2016-10-10 00:37:08 +02:00
return (
<i18n.Translate wrap="p">
2017-05-29 15:18:48 +02:00
Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for
{" "}
2017-06-04 17:56:55 +02:00
<span>{renderAmount(d.requestedAmount)}</span>.
</i18n.Translate>
2016-10-10 00:37:08 +02:00
);
2016-10-10 03:16:12 +02:00
case "confirm-reserve": {
const exchange = (new URI(d.exchangeBaseUrl)).host();
2017-05-29 15:18:48 +02:00
const pub = abbrev(d.reservePub);
2016-10-10 00:37:08 +02:00
return (
<i18n.Translate wrap="p">
Started to withdraw
{" "}{renderAmount(d.requestedAmount)}<span> </span>
from <span>{exchange}</span> (<span>{pub}</span>).
</i18n.Translate>
2016-10-10 00:37:08 +02:00
);
2016-10-10 03:16:12 +02:00
}
2016-09-29 01:40:29 +02:00
case "offer-contract": {
2016-10-10 00:37:08 +02:00
return (
<i18n.Translate wrap="p">
Merchant <em>{abbrev(d.merchantName, 15)}</em> offered<span> </span>
2017-10-17 12:11:54 +02:00
contract <span>{abbrev(d.contractTermsHash)}</span>.
</i18n.Translate>
2016-10-10 00:37:08 +02:00
);
2016-09-29 01:40:29 +02:00
}
2016-10-10 03:16:12 +02:00
case "depleted-reserve": {
2017-05-29 15:18:48 +02:00
const exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??";
2017-06-04 17:56:55 +02:00
const amount = renderAmount(d.requestedAmount);
2017-05-29 15:18:48 +02:00
const pub = abbrev(d.reservePub);
2016-11-23 01:14:45 +01:00
return (
<i18n.Translate wrap="p">
Withdrew <span>{amount}</span> from <span>{exchange}</span> (<span>{pub}</span>).
</i18n.Translate>
2016-11-23 01:14:45 +01:00
);
2016-10-10 03:16:12 +02:00
}
2016-09-29 01:40:29 +02:00
case "pay": {
2017-05-29 15:18:48 +02:00
const url = d.fulfillmentUrl;
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
const fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>;
2016-10-10 00:37:08 +02:00
return (
<i18n.Translate wrap="p">
2017-06-04 17:56:55 +02:00
Paid <span>{renderAmount(d.amount)}</span> to merchant <span>{merchantElem}</span>.
<span> </span>
2017-05-29 15:18:48 +02:00
(<span>{fulfillmentLinkElem}</span>)
</i18n.Translate>
);
2016-09-29 01:40:29 +02:00
}
case "refund": {
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
return (
<i18n.Translate wrap="p">
Merchant <span>{merchantElem}</span> gave a refund over <span>{renderAmount(d.refundAmount)}</span>.
</i18n.Translate>
);
}
2017-12-12 15:38:03 +01:00
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();
return (
<i18n.Translate wrap="p">
Merchant <span>{d.merchantDomain}</span> gave
a <a href={url} onClick={openTab(url)}> tip</a> of <span>{renderAmount(d.amount)}</span>.
2017-12-12 15:38:03 +01:00
<span> </span>
2017-12-12 16:51:13 +01:00
{ d.accepted ? null : <span>You did not accept the tip yet.</span> }
2017-12-12 15:38:03 +01:00
</i18n.Translate>
);
}
default:
2016-11-27 22:13:24 +01:00
return (<p>{i18n.str`Unknown event (${historyItem.type})`}</p>);
}
}
class WalletHistory extends React.Component<any, any> {
2017-05-29 15:18:48 +02:00
private myHistory: any[];
private gotError = false;
private unmounted = false;
2016-02-18 23:41:29 +01:00
2016-10-10 00:37:08 +02:00
componentWillMount() {
this.update();
onUpdateNotification(() => this.update());
}
2016-02-18 23:41:29 +01:00
componentWillUnmount() {
console.log("history component unmounted");
this.unmounted = true;
}
2016-10-10 00:37:08 +02:00
update() {
chrome.runtime.sendMessage({type: "get-history"}, (resp) => {
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");
2017-05-29 15:18:48 +02:00
const history: HistoryRecord[] = this.myHistory;
2016-10-10 00:37:08 +02:00
if (this.gotError) {
2016-11-27 22:13:24 +01:00
return i18n.str`Error: could not retrieve event history`;
2016-03-02 00:47:00 +01:00
}
2016-10-10 00:37:08 +02:00
if (!history) {
2016-10-10 00:37:08 +02:00
// We're not ready yet
return <span />;
}
2016-09-28 23:41:34 +02:00
2017-05-29 15:18:48 +02:00
const listing: any[] = [];
for (const record of history.reverse()) {
const item = (
2016-10-10 00:37:08 +02:00
<div className="historyItem">
<div className="historyDate">
{(new Date(record.timestamp)).toString()}
</div>
{formatHistoryItem(record)}
</div>
);
2016-09-29 01:40:29 +02:00
listing.push(item);
2016-09-28 23:41:34 +02:00
}
if (listing.length > 0) {
2016-10-10 00:37:08 +02:00
return <div className="container">{listing}</div>;
}
2017-05-29 15:18:48 +02:00
return <p>{i18n.str`Your wallet has no events recorded.`}</p>;
}
2016-10-10 00:37:08 +02:00
2016-02-18 23:41:29 +01:00
}
2016-01-26 17:21:17 +01:00
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?")) {
2017-06-05 03:20:28 +02:00
wxApi.resetDb();
window.close();
}
}
2016-10-10 00:37:08 +02:00
function WalletDebug(props: any) {
return (<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("/src/webex/pages/popup.html")}>
2016-10-10 00:37:08 +02:00
wallet tab
</button>
<button onClick={openExtensionPage("/src/webex/pages/show-db.html")}>
2016-10-10 00:37:08 +02:00
show db
</button>
<button onClick={openExtensionPage("/src/webex/pages/tree.html")}>
2016-10-13 02:36:33 +02:00
show tree
</button>
<button onClick={openExtensionPage("/src/webex/pages/logs.html")}>
2016-11-18 04:09:04 +01:00
show logs
</button>
2016-10-10 00:37:08 +02:00
<br />
<button onClick={confirmReset}>
reset
</button>
<button onClick={reload}>
reload chrome extension
</button>
</div>);
}
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),
});
2017-05-29 15:18:48 +02:00
};
}
2016-01-26 17:21:17 +01:00
2016-02-01 15:10:20 +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,
});
2017-05-29 15:18:48 +02:00
};
2016-02-01 15:10:20 +01:00
}
2017-05-29 15:18:48 +02:00
const el = (
<div>
<WalletNavBar />
<div style={{margin: "1em"}}>
<Router>
<WalletBalanceView route="/balance" default/>
<WalletHistory route="/history"/>
<WalletDebug route="/debug"/>
</Router>
</div>
</div>
);
document.addEventListener("DOMContentLoaded", () => {
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"});
2017-05-29 15:18:48 +02:00
});