/* This file is part of GNU Taler (C) 2021-2024 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 { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; import { ErrorType, HttpError, urlPattern, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, FunctionComponent, VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import { Loading } from "./components/exception/loading.js"; import { Menu, NotConnectedAppMenu, NotificationCard, } from "./components/menu/index.js"; import { MerchantBackend } from "./declaration.js"; import { useInstanceBankAccounts } from "./hooks/bank.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; import { usePreference } from "./hooks/preference.js"; import { DEFAULT_ADMIN_USERNAME, useSessionState } from "./hooks/session.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/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 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 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 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 TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js"; import TokenPage from "./paths/instance/token/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 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 { LoginPage } from "./paths/login/index.js"; import NotFoundPage from "./paths/notfound/index.js"; import { Settings } from "./paths/settings/index.js"; import { Notification } from "./utils/types.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 {} export const privatePages = { home: urlPattern(/\/home/, () => "#/home"), go: urlPattern(/\/home/, () => "#/home"), }; export const publicPages = { home: urlPattern(/\/home/, () => "#/home"), go: urlPattern(/\/home/, () => "#/home"), }; export function Routing(_p: Props): VNode { const { i18n } = useTranslationContext(); const { state } = useSessionState(); type GlobalNotifState = | (Notification & { to: string | undefined }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); const [error] = useErrorBoundary(); const instance = useInstanceBankAccounts(); const accounts = !instance.ok ? undefined : instance.data.accounts; const shouldWarnAboutMissingBankAccounts = !state.isAdmin && accounts !== undefined && accounts.length < 1 const shouldLogin = state.status === "loggedOut" || state.status === "expired"; 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 (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) { return ( { route(InstancePaths.bank_list); }} /> ); } if (props) { return ; } return ; }; } if (shouldLogin) { return ( ); } if (shouldWarnAboutMissingBankAccounts) { return ( {}} /> ); } return ( {error && ( { (error instanceof Error ? error.stack : String(error)) as TranslatedString } ), }} /> )} { const movingOutFromNotification = globalNotification && e.url !== globalNotification.to; if (movingOutFromNotification) { setGlobalNotification(undefined); } }} > {/** * Admin pages */} {state.isAdmin && ( { route(AdminPaths.new_instance); }} onUpdate={(id: string): void => { route(`/instance/${id}/update`); }} onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> )} {state.isAdmin && ( route(AdminPaths.list_instances)} onConfirm={() => { route(InstancePaths.order_list); }} /> )} {state.isAdmin && ( 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 { 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 today = format(new Date(), dateFormatForSettings(settings)); const [prefs, updatePref] = usePreference(); const now = AbsoluteTime.now(); const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1; if (hidden || !needsToBeShown) return ; const oneDay = { d_ms: 1000 * 60 * 60 * 24 }; const tomorrow = AbsoluteTime.addDuration(now, oneDay); return (

Some transfer are on hold until a KYC process is completed. Go to the KYC section in the left panel for more information

), }} /> ); }