summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/NavigationBar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/NavigationBar.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx335
1 files changed, 273 insertions, 62 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 9edd8ca67..fe348f7fb 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -1,93 +1,304 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU 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
+ GNU 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/>
+ GNU 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
+ * @author sebasjm
*/
/**
* Imports.
*/
-import { i18n } from "@gnu-taler/taler-util";
-import { ComponentChildren, JSX, h } from "preact";
-import Match from "preact-router/match";
-import { useDevContext } from "./context/devContext";
-import { PopupNavigation } from './components/styled'
-
-export enum Pages {
- welcome = '/welcome',
- balance = '/balance',
- manual_withdraw = '/manual-withdraw',
- settings = '/settings',
- dev = '/dev',
- cta = '/cta',
- backup = '/backup',
- history = '/history',
- transaction = '/transaction/:tid',
- provider_detail = '/provider/:pid',
- provider_add = '/provider/add',
-
- reset_required = '/reset-required',
- payback = '/payback',
- return_coins = '/return-coins',
-
- pay = '/pay',
- refund = '/refund',
- tips = '/tip',
- withdraw = '/withdraw',
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Fragment, h, VNode } from "preact";
+import { EnabledBySettings } from "./components/EnabledBySettings.js";
+import {
+ NavigationHeader,
+ NavigationHeaderHolder,
+ SvgIcon,
+} from "./components/styled/index.js";
+import { useBackendContext } from "./context/backend.js";
+import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
+import searchIcon from "./svg/search_24px.inline.svg";
+import qrIcon from "./svg/qr_code_24px.inline.svg";
+import settingsIcon from "./svg/settings_black_24dp.inline.svg";
+import warningIcon from "./svg/warning_24px.inline.svg";
+import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+/**
+ * List of pages used by the wallet
+ *
+ * @author sebasjm
+ */
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+type PageLocation<DynamicPart extends object> = {
+ pattern: string;
+ (params: DynamicPart): string;
+};
+
+function replaceAll(
+ pattern: string,
+ vars: Record<string, string>,
+ values: Record<string, string>,
+): string {
+ let result = pattern;
+ for (const v in vars) {
+ result = result.replace(
+ vars[v],
+ !values[v] ? "" : encodeURIComponent(values[v]),
+ );
+ }
+ return result;
}
-interface TabProps {
- target: string;
- current?: string;
- children?: ComponentChildren;
+// eslint-disable-next-line @typescript-eslint/ban-types
+function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
+ const patternParams = pattern.match(/(:[\w?]*)/g);
+ if (!patternParams)
+ throw Error(
+ `page definition pattern ${pattern} doesn't have any parameter`,
+ );
+
+ const vars = patternParams.reduce(
+ (prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
+
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ },
+ {} as Record<string, string>,
+ );
+
+ const f = (values: T): string =>
+ replaceAll(pattern, vars, (values ?? {}) as Record<string, string>);
+ f.pattern = pattern;
+ return f;
}
-function Tab(props: TabProps): JSX.Element {
- let cssClass = "";
- if (props.current?.startsWith(props.target)) {
- cssClass = "active";
+export const Pages = {
+ welcome: "/welcome",
+ balance: "/balance",
+ balanceHistory: pageDefinition<{ currency?: string }>(
+ "/balance/history/:currency?",
+ ),
+ searchHistory: pageDefinition<{ currency?: string }>(
+ "/search/history/:currency?",
+ ),
+ balanceDeposit: pageDefinition<{ amount: string }>(
+ "/balance/deposit/:amount",
+ ),
+ balanceTransaction: pageDefinition<{ tid: string }>(
+ "/balance/transaction/:tid",
+ ),
+ sendCash: pageDefinition<{ amount?: string }>("/destination/send/:amount"),
+ receiveCash: pageDefinition<{ amount?: string }>("/destination/get/:amount?"),
+ dev: "/dev",
+
+ exchanges: "/exchanges",
+ backup: "/backup",
+ backupProviderDetail: pageDefinition<{ pid: string }>(
+ "/backup/provider/:pid",
+ ),
+ backupProviderAdd: "/backup/provider/add",
+
+ qr: "/qr",
+ notifications: "/notifications",
+ settings: "/settings",
+ settingsExchangeAdd: pageDefinition<{ currency?: string }>(
+ "/settings/exchange/add/:currency?",
+ ),
+
+ defaultCta: pageDefinition<{ uri: string }>("/taler-uri/:uri"),
+ cta: pageDefinition<{ action: string }>("/cta/:action"),
+ ctaPay: "/cta/pay",
+ ctaPayTemplate: "/cta/pay/template",
+ ctaRecovery: "/cta/recovery",
+ ctaRefund: "/cta/refund",
+ ctaWithdraw: "/cta/withdraw",
+ ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
+ ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
+ "/cta/invoice/create/:amount?",
+ ),
+ ctaTransferCreate: pageDefinition<{ amount?: string }>(
+ "/cta/transfer/create/:amount?",
+ ),
+ ctaInvoicePay: "/cta/invoice/pay",
+ ctaTransferPickup: "/cta/transfer/pickup",
+ ctaWithdrawManual: pageDefinition<{ amount?: string }>(
+ "/cta/manual-withdraw/:amount?",
+ ),
+};
+
+const talerUriActionToPageName: {
+ [t in TalerUriAction]: keyof typeof Pages | undefined;
+} = {
+ [TalerUriAction.Withdraw]: "ctaWithdraw",
+ [TalerUriAction.Pay]: "ctaPay",
+ [TalerUriAction.Refund]: "ctaRefund",
+ [TalerUriAction.PayPull]: "ctaInvoicePay",
+ [TalerUriAction.PayPush]: "ctaTransferPickup",
+ [TalerUriAction.Restore]: "ctaRecovery",
+ [TalerUriAction.PayTemplate]: "ctaPayTemplate",
+ [TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
+};
+
+export function getPathnameForTalerURI(talerUri: string): string | undefined {
+ const uri = parseTalerUri(talerUri);
+ if (!uri) {
+ return undefined;
}
+ const pageName = talerUriActionToPageName[uri.type];
+ if (!pageName) {
+ return undefined;
+ }
+ const pageString: string =
+ typeof Pages[pageName] === "function"
+ ? (Pages[pageName] as any)()
+ : Pages[pageName];
+ return `${pageString}?talerUri=${encodeURIComponent(talerUri)}`;
+}
+
+export type PopupNavBarOptions = "balance" | "backup" | "dev";
+export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionUnreadCount,
+ {},
+ );
+ });
+ const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+
+ const { i18n } = useTranslationContext();
return (
- <a href={props.target} class={cssClass}>
- {props.children}
- </a>
+ <NavigationHeader>
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <i18n.Translate>Balance</i18n.Translate>
+ </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+ <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
+ {attentionCount > 0 ? (
+ <a href={Pages.notifications}>
+ <SvgIcon
+ title={i18n.str`Notifications`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="yellow"
+ />
+ </a>
+ ) : (
+ <Fragment />
+ )}
+ <a href={Pages.qr}>
+ <SvgIcon
+ title={i18n.str`QR Reader and Taler URI`}
+ dangerouslySetInnerHTML={{ __html: qrIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.settings}>
+ <SvgIcon
+ title={i18n.str`Settings`}
+ dangerouslySetInnerHTML={{ __html: settingsIcon }}
+ color="white"
+ />
+ </a>
+ </div>
+ </NavigationHeader>
);
}
+export type WalletNavBarOptions = "balance" | "backup" | "dev";
+export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
+ const { i18n } = useTranslationContext();
-export function NavBar({ devMode, path }: { path: string, devMode: boolean }) {
- return <PopupNavigation devMode={devMode}>
- <div>
- <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
- <Tab target="/history" current={path}>{i18n.str`History`}</Tab>
- <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab>
- <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab>
- {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>}
- </div>
- </PopupNavigation>
-}
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionUnreadCount,
+ {},
+ );
+ });
+ const attentionCount =
+ (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0;
-export function WalletNavBar() {
- const { devMode } = useDevContext()
- return <Match>{({ path }: any) => {
- console.log("path", path)
- return <NavBar devMode={devMode} path={path} />
- }}</Match>
-}
+ return (
+ <NavigationHeaderHolder>
+ <NavigationHeader>
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <i18n.Translate>Balance</i18n.Translate>
+ </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+
+ {attentionCount > 0 ? (
+ <a href={Pages.notifications}>
+ <i18n.Translate>Notifications</i18n.Translate>
+ </a>
+ ) : (
+ <Fragment />
+ )}
+ <EnabledBySettings name="advancedMode">
+ <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
+ <i18n.Translate>Dev tools</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+
+ <div
+ style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
+ >
+ <a href={Pages.searchHistory({})}>
+ <SvgIcon
+ title={i18n.str`Search transactions`}
+ dangerouslySetInnerHTML={{ __html: searchIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.qr}>
+ <SvgIcon
+ title={i18n.str`QR Reader and Taler URI`}
+ dangerouslySetInnerHTML={{ __html: qrIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.settings}>
+ <SvgIcon
+ title={i18n.str`Settings`}
+ dangerouslySetInnerHTML={{ __html: settingsIcon }}
+ color="white"
+ />
+ </a>
+ </div>
+ </NavigationHeader>
+ </NavigationHeaderHolder>
+ );
+}