From 3e060b80428943c6562250a6ff77eff10a0259b7 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 24 Oct 2022 10:46:14 +0200 Subject: repo: integrate packages from former merchant-backoffice.git --- .../merchant-backoffice-ui/src/InstanceRoutes.tsx | 528 +++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 packages/merchant-backoffice-ui/src/InstanceRoutes.tsx (limited to 'packages/merchant-backoffice-ui/src/InstanceRoutes.tsx') diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx new file mode 100644 index 000000000..06f1db17c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -0,0 +1,528 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { 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"; +import { Menu, NotificationCard } from "./components/menu"; +import { useBackendContext } from "./context/backend"; +import { InstanceContextProvider } from "./context/instance"; +import { + useBackendDefaultToken, + useBackendInstanceToken, + useLocalStorage, +} from "./hooks"; +import { HttpError } from "./hooks/backend"; +import { Translate, useTranslator } from "./i18n"; +import InstanceCreatePage from "./paths/admin/create"; +import InstanceListPage from "./paths/admin/list"; +import OrderCreatePage from "./paths/instance/orders/create"; +import OrderDetailsPage from "./paths/instance/orders/details"; +import OrderListPage from "./paths/instance/orders/list"; +import ProductCreatePage from "./paths/instance/products/create"; +import ProductListPage from "./paths/instance/products/list"; +import ProductUpdatePage from "./paths/instance/products/update"; +import TransferListPage from "./paths/instance/transfers/list"; +import TransferCreatePage from "./paths/instance/transfers/create"; +import ReservesCreatePage from "./paths/instance/reserves/create"; +import ReservesDetailsPage from "./paths/instance/reserves/details"; +import ReservesListPage from "./paths/instance/reserves/list"; +import ListKYCPage from "./paths/instance/kyc/list"; +import InstanceUpdatePage, { + Props as InstanceUpdatePageProps, + AdminUpdate as InstanceAdminUpdatePage, +} from "./paths/instance/update"; +import LoginPage from "./paths/login"; +import NotFoundPage from "./paths/notfound"; +import { Notification } from "./utils/types"; +import { useInstanceKYCDetails } from "./hooks/instance"; +import { format } from "date-fns"; + +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", +} + +// 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; + setInstanceName: (s: string) => void; +} + +export function InstanceRoutes({ id, admin, 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 = useTranslator(); + + 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) { + setGlobalNotification({ + message: i18n`The backend reported a problem: HTTP status #${error.status}`, + description: i18n`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); + }} + /> + + {/** + * 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) { + const [token, changeToken] = useBackendInstanceToken(id); + const { updateLoginStatus: changeBackend } = useBackendContext(); + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url); + if (token) changeToken(token); + }; + const value = useMemo( + () => ({ id, token, admin: true, changeToken }), + [id, token] + ); + const i18n = useTranslator(); + + return ( + + { + return ( + + + + + ); + }} + onUnauthorized={() => { + return ( + + + + + ); + }} + /> + + ); +} + +function KycBanner(): VNode { + const kycStatus = useInstanceKYCDetails(); + 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 +

+
+ +
+ + ), + }} + /> + ); +} -- cgit v1.2.3