From ffd2a62c3f7df94365980302fef3bc3376b48182 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 3 Aug 2020 13:00:48 +0530 Subject: modularize repo, use pnpm, improve typechecking --- .../taler-wallet-webextension/src/pages/popup.tsx | 502 +++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/pages/popup.tsx (limited to 'packages/taler-wallet-webextension/src/pages/popup.tsx') diff --git a/packages/taler-wallet-webextension/src/pages/popup.tsx b/packages/taler-wallet-webextension/src/pages/popup.tsx new file mode 100644 index 000000000..72c9f4bcb --- /dev/null +++ b/packages/taler-wallet-webextension/src/pages/popup.tsx @@ -0,0 +1,502 @@ +/* + 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 * as i18n from "../i18n"; + +import { + AmountJson, + Amounts, + time, + taleruri, + walletTypes, +} from "taler-wallet-core"; + + +import { abbrev, renderAmount, PageLink } from "../renderHtml"; +import * as wxApi from "../wxApi"; + +import React, { Fragment, useState, useEffect } from "react"; + +import moment from "moment"; +import { PermissionsCheckbox } from "./welcome"; + +// FIXME: move to newer react functions +/* eslint-disable react/no-deprecated */ + +class Router extends React.Component { + 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 { + Router.routeHandlers.push(f); + return () => { + const i = Router.routeHandlers.indexOf(f); + this.routeHandlers = this.routeHandlers.splice(i, 1); + }; + } + + private static routeHandlers: any[] = []; + + componentWillMount(): void { + console.log("router mounted"); + window.onhashchange = () => { + this.setState({}); + for (const f of Router.routeHandlers) { + f(); + } + }; + } + + render(): JSX.Element { + const route = window.location.hash.substring(1); + console.log("rendering route", route); + let defaultChild: React.ReactChild | null = null; + let foundChild: React.ReactChild | null = null; + React.Children.forEach(this.props.children, (child) => { + const childProps: any = (child as any).props; + if (!childProps) { + return; + } + if (childProps.default) { + defaultChild = child as React.ReactChild; + } + if (childProps.route === route) { + foundChild = child as React.ReactChild; + } + }); + const c: React.ReactChild | null = foundChild || defaultChild; + if (!c) { + throw Error("unknown route"); + } + Router.setRoute((c as any).props.route); + return
{c}
; + } +} + +interface TabProps { + target: string; + children?: React.ReactNode; +} + +function Tab(props: TabProps): JSX.Element { + let cssClass = ""; + if (props.target === Router.getRoute()) { + cssClass = "active"; + } + const onClick = (e: React.MouseEvent): void => { + Router.setRoute(props.target); + e.preventDefault(); + }; + return ( + + {props.children} + + ); +} + +class WalletNavBar extends React.Component { + private cancelSubscription: any; + + componentWillMount(): void { + this.cancelSubscription = Router.onRoute(() => { + this.setState({}); + }); + } + + componentWillUnmount(): void { + if (this.cancelSubscription) { + this.cancelSubscription(); + } + } + + render(): JSX.Element { + console.log("rendering nav bar"); + 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 / Amounts.fractionalBase; + return ( + + {v}{" "} + {amount.currency} + + ); +} + +function EmptyBalanceView(): JSX.Element { + return ( + + You have no balance to show. Need some{" "} + help getting started? + + ); +} + +class WalletBalanceView extends React.Component { + private balance?: walletTypes.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: walletTypes.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: walletTypes.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}
+ ) : ( + + ); + } +} + +function Icon({ l }: { l: string }): JSX.Element { + return
{l}
; +} + +function formatAndCapitalize(text: string): string { + text = text.replace("-", " "); + text = text.replace(/^./, text[0].toUpperCase()); + return text; +} + +const HistoryComponent = (props: any): JSX.Element => { + return TBD; +}; + +class WalletSettings extends React.Component { + render(): JSX.Element { + return ( +
+

Permissions

+ +
+ ); + } +} + +function reload(): void { + try { + chrome.runtime.reload(); + window.close(); + } catch (e) { + // Functionality missing in firefox, ignore! + } +} + +function confirmReset(): void { + if ( + confirm( + "Do you want to IRREVOCABLY DESTROY everything inside your" + + " wallet and LOSE ALL YOUR COINS?", + ) + ) { + wxApi.resetDb(); + window.close(); + } +} + +function WalletDebug(props: any): JSX.Element { + return ( +
+

Debug tools:

+ + + + +
+ + +
+ ); +} + +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; +} + +function actionForTalerUri(talerUri: string): string | undefined { + const uriType = taleruri.classifyTalerUri(talerUri); + switch (uriType) { + case taleruri.TalerUriType.TalerWithdraw: + return makeExtensionUrlWithParams("withdraw.html", { + talerWithdrawUri: talerUri, + }); + case taleruri.TalerUriType.TalerPay: + return makeExtensionUrlWithParams("pay.html", { + talerPayUri: talerUri, + }); + case taleruri.TalerUriType.TalerTip: + return makeExtensionUrlWithParams("tip.html", { + talerTipUri: talerUri, + }); + case taleruri.TalerUriType.TalerRefund: + return makeExtensionUrlWithParams("refund.html", { + talerRefundUri: talerUri, + }); + case taleruri.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; +} + +async function findTalerUriInActiveTab(): Promise { + return new Promise((resolve, reject) => { + chrome.tabs.executeScript( + { + code: ` + (() => { + let x = document.querySelector("a[href^='taler://'"); + 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]); + }, + ); + }); +} + +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.

+

+ +

+

+ +

+
+ ); + } + return ( +
+ +
+ + + + + +
+
+ ); +} + +export function createPopup(): JSX.Element { + return ; +} -- cgit v1.2.3