/* 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, TalerError, TranslatedString, } from "@gnu-taler/taler-util"; import { urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, 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 { useSessionContext } from "./context/session.js"; import { useInstanceBankAccounts } from "./hooks/bank.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; import { usePreference } from "./hooks/preference.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"), }; const history = createHashHistory(); export function Routing(_p: Props): VNode { // const { i18n } = useTranslationContext(); const { state } = useSessionContext(); type GlobalNotifState = | (Notification & { to: string | undefined }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); const [error] = useErrorBoundary(); const [preference] = usePreference(); const now = AbsoluteTime.now(); const instance = useInstanceBankAccounts(); const accounts = !instance || instance instanceof TalerError || instance.type === "fail" ? undefined : instance.body; const shouldWarnAboutMissingBankAccounts = !state.isAdmin && accounts !== undefined && accounts.accounts.length < 1 && (AbsoluteTime.isNever(preference.hideMissingAccountUntil) || AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 1); const shouldLogin = state.status === "loggedOut"; // 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.hint // : 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 ( { route(InstancePaths.bank_list); }} /> ); } 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`); }} /> )} {state.isAdmin && ( route(AdminPaths.list_instances)} onConfirm={() => { route(AdminPaths.list_instances); }} /> )} {state.isAdmin && ( route(AdminPaths.list_instances)} onConfirm={() => { route(AdminPaths.list_instances); }} /> )} {/** * Update instance page */} { route(`/`); }} onConfirm={() => { route(`/`); }} /> {/** * Update instance page */} { route(`/`); }} onCancel={() => { route(InstancePaths.order_list); }} /> {/** * Inventory pages */} { route(InstancePaths.inventory_new); }} onSelect={(id: string) => { route(InstancePaths.inventory_update.replace(":pid", id)); }} /> { route(InstancePaths.inventory_list); }} onBack={() => { route(InstancePaths.inventory_list); }} /> { 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)); }} /> { route(InstancePaths.bank_list); }} onBack={() => { route(InstancePaths.bank_list); }} /> { 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)); }} /> { 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); }} 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); }} 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); }} onBack={() => { route(InstancePaths.templates_list); }} /> { route(InstancePaths.templates_list); }} onBack={() => { route(InstancePaths.templates_list); }} /> { route(InstancePaths.order_details.replace(":oid", id)); }} 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.hint // : undefined, // type: "ERROR" as const, // }; // return ( // // // // // ); // }} // onUnauthorized={() => { // return ( // // // // // ); // }} /> ); } function BankAccountBanner(): VNode { const { i18n } = useTranslationContext(); const [, updatePref] = usePreference(); const now = AbsoluteTime.now(); const oneDay = { d_ms: 1000 * 60 * 60 * 24 }; const tomorrow = AbsoluteTime.addDuration(now, oneDay); return (

Without this the merchant backend will refuse to create new orders.

), }} /> ); } 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 !== undefined && !(kycStatus instanceof TalerError) && kycStatus.type === "ok" && !!kycStatus.body; 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

), }} /> ); }