wallet-core/popup/popup.tsx

478 lines
12 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
*/
"use strict";
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
2016-09-12 20:25:56 +02:00
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
2016-09-29 01:40:29 +02:00
import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet";
2016-10-19 18:40:29 +02:00
import {
AmountJson, WalletBalance, Amounts,
WalletBalanceEntry
} from "../lib/wallet/types";
2016-10-12 02:55:53 +02:00
import {abbrev, prettyAmount} from "../lib/wallet/renderHtml";
2016-02-01 15:10:20 +01:00
declare var i18n: any;
2016-09-12 20:25:56 +02:00
function onUpdateNotification(f: () => void) {
2016-02-18 23:41:29 +01:00
let port = chrome.runtime.connect({name: "notifications"});
port.onMessage.addListener((msg, port) => {
f();
});
}
2016-10-10 00:37:08 +02:00
class Router extends preact.Component<any,any> {
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 () => {
2016-10-13 02:36:33 +02:00
let i = Router.routeHandlers.indexOf(f);
2016-10-10 00:37:08 +02:00
this.routeHandlers = this.routeHandlers.splice(i, 1);
}
}
static routeHandlers: any[] = [];
componentWillMount() {
console.log("router mounted");
window.onhashchange = () => {
2016-10-10 02:36:12 +02:00
this.setState({});
2016-10-10 00:37:08 +02:00
for (let f of Router.routeHandlers) {
f();
}
}
}
componentWillUnmount() {
console.log("router unmounted");
}
render(props: any, state: any): JSX.Element {
let route = window.location.hash.substring(1);
console.log("rendering route", route);
let defaultChild: JSX.Element|null = null;
for (let child of props.children) {
if (child.attributes["default"]) {
defaultChild = child;
}
if (child.attributes["route"] == route) {
return <div>{child}</div>;
}
}
if (defaultChild == null) {
throw Error("unknown route");
}
console.log("rendering default route");
Router.setRoute(defaultChild.attributes["route"]);
return <div>{defaultChild}</div>;
}
}
2016-01-26 17:21:17 +01:00
export function main() {
console.log("popup main");
2016-10-10 00:37:08 +02:00
let el = (
<div>
<WalletNavBar />
2016-10-10 03:16:12 +02:00
<div style="margin:1em">
2016-10-19 18:40:29 +02:00
<Router>
<WalletBalanceView route="/balance" default/>
<WalletHistory route="/history"/>
<WalletDebug route="/debug"/>
</Router>
2016-10-10 03:16:12 +02:00
</div>
2016-10-10 00:37:08 +02:00
</div>
);
preact.render(el, document.getElementById("content")!);
}
2016-10-10 00:37:08 +02:00
interface TabProps extends preact.ComponentProps {
target: string;
}
2016-10-10 00:37:08 +02:00
function Tab(props: TabProps) {
let cssClass = "";
2016-10-10 00:37:08 +02:00
if (props.target == Router.getRoute()) {
cssClass = "active";
}
2016-10-10 00:37:08 +02:00
let onClick = (e: Event) => {
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
class WalletNavBar extends preact.Component<any,any> {
cancelSubscription: any;
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 class="nav" id="header">
<Tab target="/balance">
Balance
</Tab>
<Tab target="/history">
History
</Tab>
<Tab target="/debug">
Debug
</Tab>
</div>);
2016-02-18 23:41:29 +01:00
}
}
2016-10-10 00:37:08 +02:00
function ExtensionLink(props: any) {
let onClick = (e: Event) => {
chrome.tabs.create({
2016-10-10 00:37:08 +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}
</a>)
}
2016-10-19 18:40:29 +02:00
class WalletBalanceView extends preact.Component<any, any> {
balance: WalletBalance;
2016-10-10 00:37:08 +02:00
gotError = false;
2016-09-14 15:20:18 +02:00
2016-10-10 00:37:08 +02:00
componentWillMount() {
this.updateBalance();
2016-02-18 23:41:29 +01:00
2016-10-10 00:37:08 +02:00
onUpdateNotification(() => this.updateBalance());
}
2016-02-18 23:41:29 +01:00
2016-10-10 00:37:08 +02:00
updateBalance() {
chrome.runtime.sendMessage({type: "balances"}, (resp) => {
if (resp.error) {
this.gotError = true;
console.error("could not retrieve balances", resp);
2016-10-18 02:40:46 +02:00
this.setState({});
2016-10-10 00:37:08 +02:00
return;
}
this.gotError = false;
console.log("got wallet", resp);
2016-10-19 18:40:29 +02:00
this.balance = resp;
2016-10-18 02:40:46 +02:00
this.setState({});
2016-10-10 00:37:08 +02:00
});
2016-02-18 23:41:29 +01:00
}
2016-10-19 18:40:29 +02:00
renderEmpty(): JSX.Element {
2016-10-10 03:16:12 +02:00
let helpLink = (
<ExtensionLink target="pages/help/empty-wallet.html">
help
</ExtensionLink>
);
2016-10-19 23:27:46 +02:00
return <div>You have no balance to show. Need some
{" "}{helpLink}{" "}
2016-10-19 18:40:29 +02:00
getting started?</div>;
}
formatPending(entry: WalletBalanceEntry): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
console.log("available: ", entry.pendingIncoming ? prettyAmount(entry.available) : null);
console.log("incoming: ", entry.pendingIncoming ? prettyAmount(entry.pendingIncoming) : null);
if (Amounts.isNonZero(entry.pendingIncoming)) {
incoming = (
<span>
<span style="color: darkgreen">
{"+"}
{prettyAmount(entry.pendingIncoming)}
</span>
{" "}
incoming
</span>);
}
if (Amounts.isNonZero(entry.pendingPayment)) {
payment = (
<span>
<span style="color: darkblue">
{prettyAmount(entry.pendingPayment)}
</span>
{" "}
being spent
</span>);
}
let l = [incoming, payment].filter((x) => x !== undefined);
if (l.length == 0) {
return <span />;
}
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 {
2016-10-19 18:40:29 +02:00
let wallet = this.balance;
2016-10-10 00:37:08 +02:00
if (this.gotError) {
2016-03-02 00:47:00 +01:00
return i18n`Error: could not retrieve balance information.`;
}
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 listing = Object.keys(wallet).map((key) => {
2016-10-19 18:40:29 +02:00
let entry: WalletBalanceEntry = wallet[key];
return (
<p>
{prettyAmount(entry.available)}
{" "}
{this.formatPending(entry)}
2016-10-19 18:40:29 +02:00
</p>
);
2016-10-10 00:37:08 +02:00
});
if (listing.length > 0) {
2016-10-10 00:37:08 +02:00
return <div>{listing}</div>;
}
2016-02-23 18:28:12 +01:00
2016-10-10 03:16:12 +02:00
return this.renderEmpty();
}
2016-02-18 23:41:29 +01:00
}
2016-09-28 23:41:34 +02:00
function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail;
const t = historyItem.timestamp;
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 (
<p>
2016-10-12 02:55:53 +02:00
{i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${prettyAmount(
2016-10-10 00:37:08 +02:00
d.requestedAmount)}.`}
</p>
);
2016-10-10 03:16:12 +02:00
case "confirm-reserve": {
// FIXME: eventually remove compat fix
let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
2016-10-12 02:55:53 +02:00
let amount = prettyAmount(d.requestedAmount);
2016-10-10 03:16:12 +02:00
let pub = abbrev(d.reservePub);
2016-10-10 00:37:08 +02:00
return (
<p>
2016-10-10 03:16:12 +02:00
{i18n.parts`Started to withdraw ${amount} from ${exchange} (${pub}).`}
2016-10-10 00:37:08 +02:00
</p>
);
2016-10-10 03:16:12 +02:00
}
2016-09-29 01:40:29 +02:00
case "offer-contract": {
let link = chrome.extension.getURL("view-contract.html");
2016-10-10 00:37:08 +02:00
let linkElem = <a href={link}>{abbrev(d.contractHash)}</a>;
let merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
return (
<p>
{i18n.parts`Merchant ${merchantElem} offered contract ${linkElem}.`}
</p>
);
2016-09-29 01:40:29 +02:00
}
2016-10-10 03:16:12 +02:00
case "depleted-reserve": {
let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
2016-10-12 02:55:53 +02:00
let amount = prettyAmount(d.requestedAmount);
2016-10-10 03:16:12 +02:00
let pub = abbrev(d.reservePub);
2016-10-10 00:37:08 +02:00
return (<p>
2016-10-10 03:16:12 +02:00
{i18n.parts`Withdrew ${amount} from ${exchange} (${pub}).`}
2016-10-10 00:37:08 +02:00
</p>);
2016-10-10 03:16:12 +02:00
}
2016-09-29 01:40:29 +02:00
case "pay": {
2016-02-01 15:10:20 +01:00
let url = substituteFulfillmentUrl(d.fulfillmentUrl,
{H_contract: d.contractHash});
2016-10-10 00:37:08 +02:00
let merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
let fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>;
return (
<p>
2016-10-12 02:55:53 +02:00
{i18n.parts`Paid ${prettyAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`}
2016-10-10 00:37:08 +02:00
</p>);
2016-09-29 01:40:29 +02:00
}
default:
2016-10-10 00:37:08 +02:00
return (<p>i18n`Unknown event (${historyItem.type})`</p>);
}
}
2016-10-10 00:37:08 +02:00
class WalletHistory extends preact.Component<any, any> {
myHistory: any[];
gotError = 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
2016-10-10 00:37:08 +02:00
update() {
chrome.runtime.sendMessage({type: "get-history"}, (resp) => {
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");
let history: HistoryRecord[] = this.myHistory;
if (this.gotError) {
2016-03-02 00:47:00 +01:00
return i18n`Error: could not retrieve event history`;
}
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
let subjectMemo: {[s: string]: boolean} = {};
let listing: any[] = [];
for (let record of history.reverse()) {
2016-09-29 01:40:29 +02:00
if (record.subjectId && subjectMemo[record.subjectId]) {
continue;
}
if (record.level != undefined && record.level < HistoryLevel.User) {
continue;
}
2016-09-28 23:41:34 +02:00
subjectMemo[record.subjectId as string] = true;
2016-09-29 01:40:29 +02:00
2016-10-10 00:37:08 +02:00
let item = (
<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>;
}
2016-10-10 00:37:08 +02:00
return <p>{i18n`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?")) {
chrome.runtime.sendMessage({type: "reset"});
window.close();
}
}
2016-10-10 00:37:08 +02:00
function WalletDebug(props: any) {
return (<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("popup/popup.html")}>
wallet tab
</button>
<button onClick={openExtensionPage("pages/show-db.html")}>
show db
</button>
2016-10-13 02:36:33 +02:00
<button onClick={openExtensionPage("pages/tree.html")}>
show tree
</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) {
2016-01-26 17:21:17 +01:00
return function() {
chrome.tabs.create({
"url": chrome.extension.getURL(page)
});
}
}
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) {
2016-02-01 15:10:20 +01:00
return function() {
chrome.tabs.create({
"url": page
});
}
}