/* This file is part of GNU Taler (C) 2022 Taler Systems S.A. 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. 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 GNU Taler; see the file COPYING. If not, see */ /** * Popup shown to the user when they click * the Taler browser action button. * * @author sebasjm */ /** * Imports. */ 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 { encodeCrockForURI, 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 = { pattern: string; (params: DynamicPart): string; }; function replaceAll( pattern: string, vars: Record, values: Record, ): string { let result = pattern; for (const v in vars) { result = result.replace(vars[v], !values[v] ? "" : values[v]); } return result; } // eslint-disable-next-line @typescript-eslint/ban-types function pageDefinition(pattern: string): PageLocation { 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, ); const f = (values: T): string => replaceAll(pattern, vars, (values ?? {}) as Record); f.pattern = pattern; return f; } /** * taler action and scope info may contain * character not suitable for URL */ type CrockEncodedString = string; export const Pages = { welcome: "/welcome", balance: "/balance", balanceHistory: pageDefinition<{ scope?: CrockEncodedString }>( "/balance/history/:scope?", ), searchHistory: pageDefinition<{ scope?: CrockEncodedString }>( "/search/history/:scope?", ), balanceTransaction: pageDefinition<{ tid: string }>( "/balance/transaction/:tid", ), bankManange: pageDefinition<{ scope: CrockEncodedString }>( "/bank/manage/:scope", ), balanceDeposit: pageDefinition<{ scope: CrockEncodedString; }>("/balance/deposit/:scope"), sendCash: pageDefinition<{ scope: CrockEncodedString; amount?: string }>( "/destination/send/:scope/:amount?", ), // if no scope is specified, then exchange selection page will be shown receiveCash: pageDefinition<{ scope?: CrockEncodedString; amount?: string }>( "/destination/get/:scope?/:amount?", ), receiveCashForPurchase: pageDefinition<{ id?: string }>( "/add-for-payment/purchase/:id", ), receiveCashForInvoice: pageDefinition<{ id?: string }>( "/add-for-payment/purchase/:id", ), 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: CrockEncodedString }>("/taler-uri/:uri"), // FIXME: mem leak problems defaultCtaSimple: pageDefinition<{ uri: CrockEncodedString }>( "/taler-uri-simple/:uri", ), cta: pageDefinition<{ action: CrockEncodedString }>("/cta/:action"), ctaPay: "/cta/pay", ctaPayTemplate: "/cta/pay/template", ctaRecovery: "/cta/recovery", ctaRefund: "/cta/refund", ctaWithdraw: "/cta/withdraw", ctaDeposit: pageDefinition<{ scope: CrockEncodedString; account: CrockEncodedString; }>("/cta/deposit/:scope/:account"), ctaExperiment: "/cta/experiment", ctaAddExchange: "/cta/add/exchange", ctaInvoiceCreate: pageDefinition<{ scope: CrockEncodedString; }>("/cta/invoice/create/:scope/:amount?"), ctaTransferCreate: pageDefinition<{ scope: CrockEncodedString; }>("/cta/transfer/create/:scope/:amount?"), ctaInvoicePay: "/cta/invoice/pay", ctaTransferPickup: "/cta/transfer/pickup", ctaWithdrawManual: pageDefinition<{ scope: CrockEncodedString; amount?: string; }>("/cta/manual-withdraw/:scope/:amount?"), paytoQrs: pageDefinition<{ payto: CrockEncodedString }>("/payto/qrs/:payto?"), paytoBanks: pageDefinition<{ payto: CrockEncodedString }>( "/payto/banks/:payto?", ), }; 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=${encodeCrockForURI(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 ( Balance Backup
{attentionCount > 0 ? ( ) : ( )}
); } export type WalletNavBarOptions = "balance" | "backup" | "dev"; export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); const hook = useAsyncAsHook(async () => { return await api.wallet.call( WalletApiOperation.GetUserAttentionUnreadCount, {}, ); }); const attentionCount = (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0; return ( Balance Backup {attentionCount > 0 ? ( Notifications ) : ( )} Dev tools ); }