/* 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, } from "@gnu-taler/web-util/lib/index.browser"; import { format } from "date-fns"; import { Fragment, FunctionComponent, h, VNode } from "preact"; import { Route, route, Router } from "preact-router"; import { useCallback, useEffect, 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, useLocalStorage, } 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 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 ReservesCreatePage from "./paths/instance/reserves/create/index.js"; import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateUsePage from "./paths/instance/templates/use/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 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 { MerchantBackend } from "./declaration.js"; export enum InstancePaths { // details = '/', error = "/error", update = "/update", product_list = "/products", product_update = "/product/:pid/update", product_new = "/product/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", webhooks_list = "/webhooks", webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", } // 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; setInstanceName: (s: string) => void; } export function InstanceRoutes({ id, admin, path, setInstanceName, }: Props): VNode { const [_, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); const { updateLoginStatus: changeBackend, addTokenCleaner, clearAllTokens, } = useBackendContext(); const cleaner = useCallback(() => { updateToken(undefined); }, [id]); const { i18n } = useTranslationContext(); type GlobalNotifState = (Notification & { to: string }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); useEffect(() => { addTokenCleaner(cleaner); }, [addTokenCleaner, cleaner]); const changeToken = (token?: string) => { if (admin) { updateToken(token); } else { updateDefaultToken(token); } }; const updateLoginStatus = (url: string, token?: string) => { changeBackend(url); if (!token) return; changeToken(token); }; const value = useMemo( () => ({ id, token, admin, changeToken }), [id, token, admin], ); 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.clientError || error.serverError ? error.error?.detail : undefined, type: "ERROR", to, }); } return ; }; } const LoginPageAccessDenied = () => ( ); function IfAdminCreateDefaultOr(Next: FunctionComponent) { return function IfAdminCreateDefaultOrImpl(props?: T) { if (admin && id === "default") { return ( { route(AdminPaths.list_instances); }} /> ); } if (props) { return ; } return ; }; } const clearTokenAndGoToRoot = () => { clearAllTokens(); route("/"); }; return ( { 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(AdminPaths.list_instances); }} /> )} {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)} /> {/** * Product pages */} { route(InstancePaths.product_new); }} onSelect={(id: string) => { route(InstancePaths.product_update.replace(":pid", id)); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.product_list); }} onBack={() => { route(InstancePaths.product_list); }} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.product_list); }} onBack={() => { route(InstancePaths.product_list); }} /> {/** * Order pages */} { route(InstancePaths.order_new); }} onSelect={(id: string) => { route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.update)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { route(InstancePaths.order_list); }} /> { route(InstancePaths.order_list); }} 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); }} /> {/** * Templates pages */} { route(InstancePaths.templates_new); }} onNewOrder={(id: string) => { route(InstancePaths.templates_use.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); }} /> {/** * reserves pages */} { route(InstancePaths.reserves_details.replace(":rid", id)); }} onCreate={() => { route(InstancePaths.reserves_new); }} /> { route(InstancePaths.reserves_list); }} /> { route(InstancePaths.reserves_list); }} onBack={() => { route(InstancePaths.reserves_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: changeBackend } = useBackendContext(); const updateLoginStatus = (url: string, token?: string): void => { changeBackend(url); if (token) 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.clientError || error.serverError ? error.error?.detail : undefined, type: "ERROR" as const, }; return ( ); }} onUnauthorized={() => { return ( ); }} /> ); } function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); const today = format(new Date(), "yyyy-MM-dd"); const [lastHide, setLastHide] = useLocalStorage("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

), }} /> ); }