/* This file is part of GNU Taler (C) 2021-2023 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 */ /** * * @author Sebastian Javier Marchano (sebasjm) */ import { useTranslationContext, HttpError, ErrorType, GlobalNotificationsBanner, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, FunctionComponent, h, VNode } from "preact"; import { Route, route, Router } from "preact-router"; import { useCallback, useEffect, useErrorBoundary, useMemo, useState } from "preact/hooks"; import { Loading } from "./components/exception/loading.js"; import { Menu, NotificationCard } from "./components/menu/index.js"; import { useBackendContext } from "./context/backend.js"; import { InstanceContextProvider } from "./context/instance.js"; import { useBackendDefaultToken, useBackendInstanceToken, useSimpleLocalStorage, } from "./hooks/index.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; import TokenPage from "./paths/instance/token/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; import OrderListPage from "./paths/instance/orders/list/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; import BankAccountListPage from "./paths/instance/accounts/list/index.js"; import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js"; import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; import WebhookListPage from "./paths/instance/webhooks/list/index.js"; import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { AdminUpdate as InstanceAdminUpdatePage, Props as InstanceUpdatePageProps, } from "./paths/instance/update/index.js"; import { LoginPage } from "./paths/login/index.js"; import NotFoundPage from "./paths/notfound/index.js"; import { Notification } from "./utils/types.js"; import { LoginToken, MerchantBackend } from "./declaration.js"; import { Settings } from "./paths/settings/index.js"; import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; import { TranslatedString } from "@gnu-taler/taler-util"; import { useInstanceBankAccounts } from "./hooks/bank.js"; export enum InstancePaths { error = "/error", settings = "/settings", token = "/token", bank_list = "/bank", bank_update = "/bank/:bid/update", bank_new = "/bank/new", inventory_list = "/inventory", inventory_update = "/inventory/:pid/update", inventory_new = "/inventory/new", order_list = "/orders", order_new = "/order/new", order_details = "/order/:oid/details", reserves_list = "/reserves", reserves_details = "/reserves/:rid/details", reserves_new = "/reserves/new", kyc = "/kyc", transfers_list = "/transfers", transfers_new = "/transfer/new", templates_list = "/templates", templates_update = "/templates/:tid/update", templates_new = "/templates/new", templates_use = "/templates/:tid/use", templates_qr = "/templates/:tid/qr", webhooks_list = "/webhooks", webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", otp_devices_list = "/otp-devices", otp_devices_update = "/otp-devices/:vid/update", otp_devices_new = "/otp-devices/new", interface = "/interface", } // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => { }; export enum AdminPaths { list_instances = "/instances", new_instance = "/instance/new", update_instance = "/instance/:id/update", } export interface Props { id: string; admin?: boolean; path: string; onUnauthorized: () => void; onLoginPass: () => void; setInstanceName: (s: string) => void; } export function InstanceRoutes({ id, admin, path, // onUnauthorized, onLoginPass, setInstanceName, }: Props): VNode { const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); const { i18n } = useTranslationContext(); type GlobalNotifState = (Notification & { to: string | undefined }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); const changeToken = (token?: LoginToken) => { if (admin) { updateToken(token); } else { updateDefaultToken(token); } onLoginPass() }; const [error] = useErrorBoundary(); const value = useMemo( () => ({ id, token, admin, changeToken }), [id, token, admin], ); const instance = useInstanceBankAccounts(); const accounts = !instance.ok ? undefined : instance.data.accounts; function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { return function ServerErrorRedirectToImpl( error: HttpError, ) { if (error.type === ErrorType.TIMEOUT) { setGlobalNotification({ message: i18n.str`The request to the backend take too long and was cancelled`, description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`, type: "ERROR", to, }); } else { setGlobalNotification({ message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, details: error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER ? error.payload.detail : undefined, type: "ERROR", to, }); } return ; }; } // const LoginPageAccessDeniend = onUnauthorized const LoginPageAccessDenied = () => { return } function IfAdminCreateDefaultOr(Next: FunctionComponent) { return function IfAdminCreateDefaultOrImpl(props?: T) { if (admin && id === "default") { return ( { route(InstancePaths.bank_list); }} /> ); } if (props) { return ; } return ; }; } const clearTokenAndGoToRoot = () => { route("/"); // clear all tokens updateToken(undefined) updateDefaultToken(undefined) }; if (accounts !== undefined && !admin && accounts.length < 1) { return { route(InstancePaths.interface) }} path={path} onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} isPasswordOk={defaultToken !== undefined} /> { }} /> } return ( { route(InstancePaths.interface) }} path={path} onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} isPasswordOk={defaultToken !== undefined} /> {error && {(error instanceof Error ? error.stack : String(error)) as TranslatedString} }} /> } { const movingOutFromNotification = globalNotification && e.url !== globalNotification.to; if (movingOutFromNotification) { setGlobalNotification(undefined); } }} > {/** * Admin pages */} {admin && ( { route(AdminPaths.new_instance); }} onUpdate={(id: string): void => { route(`/instance/${id}/update`); }} setInstanceName={setInstanceName} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> )} {admin && ( route(AdminPaths.list_instances)} onConfirm={() => { route(InstancePaths.order_list); }} /> )} {admin && ( route(AdminPaths.list_instances)} onConfirm={() => { route(AdminPaths.list_instances); }} onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)} onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)} onNotFound={NotFoundPage} /> )} {/** * Update instance page */} { route(`/`); }} onConfirm={() => { route(`/`); }} onUpdateError={noop} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> {/** * Update instance page */} { route(`/`); }} onCancel={() => { route(InstancePaths.order_list) }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> {/** * Inventory pages */} { route(InstancePaths.inventory_new); }} onSelect={(id: string) => { route(InstancePaths.inventory_update.replace(":pid", id)); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.inventory_list); }} onBack={() => { route(InstancePaths.inventory_list); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.inventory_list); }} onBack={() => { route(InstancePaths.inventory_list); }} /> {/** * Bank pages */} { route(InstancePaths.bank_new); }} onSelect={(id: string) => { route(InstancePaths.bank_update.replace(":bid", id)); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.bank_list); }} onBack={() => { route(InstancePaths.bank_list); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.bank_list); }} onBack={() => { route(InstancePaths.bank_list); }} /> {/** * Order pages */} { route(InstancePaths.order_new); }} onSelect={(id: string) => { route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.order_list); }} /> { route(InstancePaths.order_details.replace(":oid", orderId)); }} onBack={() => { route(InstancePaths.order_list); }} /> {/** * Transfer pages */} { route(InstancePaths.transfers_new); }} /> { route(InstancePaths.transfers_list); }} onBack={() => { route(InstancePaths.transfers_list); }} /> {/** * Webhooks pages */} { route(InstancePaths.webhooks_new); }} onSelect={(id: string) => { route(InstancePaths.webhooks_update.replace(":tid", id)); }} /> { route(InstancePaths.webhooks_list); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.webhooks_list); }} /> { route(InstancePaths.webhooks_list); }} onBack={() => { route(InstancePaths.webhooks_list); }} /> {/** * Validator pages */} { route(InstancePaths.otp_devices_new); }} onSelect={(id: string) => { route(InstancePaths.otp_devices_update.replace(":vid", id)); }} /> { route(InstancePaths.otp_devices_list); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.otp_devices_list); }} /> { route(InstancePaths.otp_devices_list); }} onBack={() => { route(InstancePaths.otp_devices_list); }} /> {/** * Templates pages */} { route(InstancePaths.templates_new); }} onNewOrder={(id: string) => { route(InstancePaths.templates_use.replace(":tid", id)); }} onQR={(id: string) => { route(InstancePaths.templates_qr.replace(":tid", id)); }} onSelect={(id: string) => { route(InstancePaths.templates_update.replace(":tid", id)); }} /> { route(InstancePaths.templates_list); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.templates_list); }} /> { route(InstancePaths.templates_list); }} onBack={() => { route(InstancePaths.templates_list); }} /> { route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.templates_list); }} /> { route(InstancePaths.templates_list); }} /> {/** * Example pages */} ); } export function Redirect({ to }: { to: string }): null { useEffect(() => { route(to, true); }); return null; } function AdminInstanceUpdatePage({ id, ...rest }: { id: string } & InstanceUpdatePageProps): VNode { const [token, changeToken] = useBackendInstanceToken(id); const updateLoginStatus = (token?: LoginToken): void => { changeToken(token); }; const value = useMemo( () => ({ id, token, admin: true, changeToken }), [id, token], ); const { i18n } = useTranslationContext(); return ( ) => { const notif = error.type === ErrorType.TIMEOUT ? { message: i18n.str`The request to the backend take too long and was cancelled`, description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, type: "ERROR" as const, } : { message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, details: error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER ? error.payload.detail : undefined, type: "ERROR" as const, }; return ( ); }} onUnauthorized={() => { return ( ); }} /> ); } function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); const [settings] = useSettings(); const today = format(new Date(), dateFormatForSettings(settings)); const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide"); const hasBeenHidden = today === lastHide; const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; if (hasBeenHidden || !needsToBeShown) return ; return ( Some transfer are on hold until a KYC process is completed. Go to the KYC section in the left panel for more information setLastHide(today)}> Hide for today ), }} /> ); }
Some transfer are on hold until a KYC process is completed. Go to the KYC section in the left panel for more information