aboutsummaryrefslogtreecommitdiff
path: root/popup/popup.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'popup/popup.tsx')
-rw-r--r--popup/popup.tsx438
1 files changed, 438 insertions, 0 deletions
diff --git a/popup/popup.tsx b/popup/popup.tsx
new file mode 100644
index 000000000..c4727d598
--- /dev/null
+++ b/popup/popup.tsx
@@ -0,0 +1,438 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+
+/**
+ * Popup shown to the user when they click
+ * the Taler browser action button.
+ *
+ * @author Florian Dold
+ */
+
+
+"use strict";
+
+import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
+import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
+import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet";
+import {AmountJson} from "../lib/wallet/types";
+
+declare var i18n: any;
+
+function onUpdateNotification(f: () => void) {
+ let port = chrome.runtime.connect({name: "notifications"});
+ port.onMessage.addListener((msg, port) => {
+ f();
+ });
+}
+
+
+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 {
+ this.routeHandlers.push(f);
+ return () => {
+ let i = this.routeHandlers.indexOf(f);
+ this.routeHandlers = this.routeHandlers.splice(i, 1);
+ }
+ }
+
+ static routeHandlers: any[] = [];
+
+ componentWillMount() {
+ console.log("router mounted");
+ window.onhashchange = () => {
+ this.setState({});
+ 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>;
+ }
+}
+
+export function main() {
+ console.log("popup main");
+
+ let el = (
+ <div>
+ <WalletNavBar />
+ <div style="margin:1em">
+ <Router>
+ <WalletBalance route="/balance" default/>
+ <WalletHistory route="/history"/>
+ <WalletDebug route="/debug"/>
+ </Router>
+ </div>
+ </div>
+ );
+
+ preact.render(el, document.getElementById("content")!);
+}
+
+interface TabProps extends preact.ComponentProps {
+ target: string;
+}
+
+function Tab(props: TabProps) {
+ let cssClass = "";
+ if (props.target == Router.getRoute()) {
+ cssClass = "active";
+ }
+ let onClick = (e: Event) => {
+ Router.setRoute(props.target);
+ e.preventDefault();
+ };
+ return (
+ <a onClick={onClick} href={props.target} className={cssClass}>
+ {props.children}
+ </a>
+ );
+}
+
+
+class WalletNavBar extends preact.Component<any,any> {
+ cancelSubscription: any;
+
+ componentWillMount() {
+ this.cancelSubscription = Router.onRoute(() => {
+ this.setState({});
+ });
+ }
+
+ 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>);
+ }
+}
+
+
+function ExtensionLink(props: any) {
+ let onClick = (e: Event) => {
+ chrome.tabs.create({
+ "url": chrome.extension.getURL(props.target)
+ });
+ e.preventDefault();
+ };
+ return (
+ <a onClick={onClick} href={props.target}>
+ {props.children}
+ </a>)
+}
+
+class WalletBalance extends preact.Component<any, any> {
+ myWallet: any;
+ gotError = false;
+
+ componentWillMount() {
+ this.updateBalance();
+
+ onUpdateNotification(() => this.updateBalance());
+ }
+
+ updateBalance() {
+ chrome.runtime.sendMessage({type: "balances"}, (resp) => {
+ if (resp.error) {
+ this.gotError = true;
+ console.error("could not retrieve balances", resp);
+ this.forceUpdate();
+ return;
+ }
+ this.gotError = false;
+ console.log("got wallet", resp);
+ this.myWallet = resp.balances;
+ this.forceUpdate();
+ });
+ }
+
+ renderEmpty() : JSX.Element {
+ let helpLink = (
+ <ExtensionLink target="pages/help/empty-wallet.html">
+ help
+ </ExtensionLink>
+ );
+ return <div>You have no balance to show. Need some {helpLink} getting started?</div>;
+ }
+
+ render(): JSX.Element {
+ let wallet = this.myWallet;
+ if (this.gotError) {
+ return i18n`Error: could not retrieve balance information.`;
+ }
+ if (!wallet) {
+ return this.renderEmpty();
+ }
+ console.log(wallet);
+ let listing = Object.keys(wallet).map((key) => {
+ return <p>{formatAmount(wallet[key])}</p>
+ });
+ if (listing.length > 0) {
+ return <div>{listing}</div>;
+ }
+
+ return this.renderEmpty();
+ }
+}
+
+
+function formatAmount(amount: AmountJson) {
+ let v = amount.value + amount.fraction / 1e6;
+ return `${v.toFixed(2)} ${amount.currency}`;
+}
+
+
+function abbrev(s: string, n: number = 5) {
+ let sAbbrev = s;
+ if (s.length > n) {
+ sAbbrev = s.slice(0, n) + "..";
+ }
+ return (
+ <span className="abbrev" title={s}>
+ {sAbbrev}
+ </span>
+ );
+}
+
+
+function formatHistoryItem(historyItem: HistoryRecord) {
+ const d = historyItem.detail;
+ const t = historyItem.timestamp;
+ console.log("hist item", historyItem);
+ switch (historyItem.type) {
+ case "create-reserve":
+ return (
+ <p>
+ {i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${formatAmount(
+ d.requestedAmount)}.`}
+ </p>
+ );
+ case "confirm-reserve": {
+ // FIXME: eventually remove compat fix
+ let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
+ let amount = formatAmount(d.requestedAmount);
+ let pub = abbrev(d.reservePub);
+ return (
+ <p>
+ {i18n.parts`Started to withdraw ${amount} from ${exchange} (${pub}).`}
+ </p>
+ );
+ }
+ case "offer-contract": {
+ let link = chrome.extension.getURL("view-contract.html");
+ 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>
+ );
+ }
+ case "depleted-reserve": {
+ let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
+ let amount = formatAmount(d.requestedAmount);
+ let pub = abbrev(d.reservePub);
+ return (<p>
+ {i18n.parts`Withdrew ${amount} from ${exchange} (${pub}).`}
+ </p>);
+ }
+ case "pay": {
+ let url = substituteFulfillmentUrl(d.fulfillmentUrl,
+ {H_contract: d.contractHash});
+ let merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
+ let fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>;
+ return (
+ <p>
+ {i18n.parts`Paid ${formatAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`}
+ </p>);
+ }
+ default:
+ return (<p>i18n`Unknown event (${historyItem.type})`</p>);
+ }
+}
+
+
+class WalletHistory extends preact.Component<any, any> {
+ myHistory: any[];
+ gotError = false;
+
+ componentWillMount() {
+ this.update();
+ onUpdateNotification(() => this.update());
+ }
+
+ 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);
+ this.forceUpdate();
+ return;
+ }
+ this.gotError = false;
+ console.log("got history", resp.history);
+ this.myHistory = resp.history;
+ this.forceUpdate();
+ });
+ }
+
+ render(): JSX.Element {
+ console.log("rendering history");
+ let history: HistoryRecord[] = this.myHistory;
+ if (this.gotError) {
+ return i18n`Error: could not retrieve event history`;
+ }
+
+ if (!history) {
+ // We're not ready yet
+ return <span />;
+ }
+
+ let subjectMemo: {[s: string]: boolean} = {};
+ let listing: any[] = [];
+ for (let record of history.reverse()) {
+ if (record.subjectId && subjectMemo[record.subjectId]) {
+ continue;
+ }
+ if (record.level != undefined && record.level < HistoryLevel.User) {
+ continue;
+ }
+ subjectMemo[record.subjectId as string] = true;
+
+ let item = (
+ <div className="historyItem">
+ <div className="historyDate">
+ {(new Date(record.timestamp)).toString()}
+ </div>
+ {formatHistoryItem(record)}
+ </div>
+ );
+
+ listing.push(item);
+ }
+
+ if (listing.length > 0) {
+ return <div className="container">{listing}</div>;
+ }
+ return <p>{i18n`Your wallet has no events recorded.`}</p>
+ }
+
+}
+
+
+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();
+ }
+}
+
+
+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>
+ <br />
+ <button onClick={confirmReset}>
+ reset
+ </button>
+ <button onClick={reload}>
+ reload chrome extension
+ </button>
+ </div>);
+}
+
+
+function openExtensionPage(page: string) {
+ return function() {
+ chrome.tabs.create({
+ "url": chrome.extension.getURL(page)
+ });
+ }
+}
+
+
+function openTab(page: string) {
+ return function() {
+ chrome.tabs.create({
+ "url": page
+ });
+ }
+}