diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/NavigationBar.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/NavigationBar.tsx | 335 |
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> + ); +} |