diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/InstanceRoutes.tsx')
-rw-r--r-- | packages/auditor-backoffice-ui/src/InstanceRoutes.tsx | 800 |
1 files changed, 800 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx new file mode 100644 index 000000000..14ccf773a --- /dev/null +++ b/packages/auditor-backoffice-ui/src/InstanceRoutes.tsx @@ -0,0 +1,800 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel + */ + +import { + useTranslationContext, + HttpError, + ErrorType, +} 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, 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 DepositConfirmationCreatePage from "./paths/instance/deposit_confirmations/create/index.js"; +import DepositConfirmationListPage from "./paths/instance/deposit_confirmations/list/index.js"; +import DepositConfirmationUpdatePage from "./paths/instance/deposit_confirmations/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 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 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 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"; + +export enum InstancePaths { + error = "/error", + settings = "/settings", + token = "/token", + + inventory_list = "/inventory", + inventory_update = "/inventory/:pid/update", + inventory_new = "/inventory/new", + + deposit_confirmation_list = "/deposit-confirmation", + deposit_confirmation_update = "/deposit-confirmation/:pid/update", + deposit_confirmation_new = "/deposit-confirmation/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; + const [globalNotification, setGlobalNotification] = + useState<GlobalNotifState>(undefined); + + const changeToken = (token?: LoginToken) => { + if (admin) { + updateToken(token); + } else { + updateDefaultToken(token); + } + onLoginPass() + }; + // const updateLoginStatus = (url: string, token?: string) => { + // changeToken(token); + // }; + + const value = useMemo( + () => ({ id, token, admin, changeToken }), + [id, token, admin], + ); + + function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { + return function ServerErrorRedirectToImpl( + error: HttpError<MerchantBackend.ErrorDetail>, + ) { + 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 <Redirect to={to} />; + }; + } + + // const LoginPageAccessDeniend = onUnauthorized + const LoginPageAccessDenied = () => { + return <Fragment> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`Session expired or password changed.`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={changeToken} /> + </Fragment> + + } + + function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) { + return function IfAdminCreateDefaultOrImpl(props?: T) { + if (admin && id === "default") { + return ( + <Fragment> + <NotificationCard + notification={{ + message: i18n.str`No 'default' instance configured yet.`, + description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, + type: "INFO", + }} + /> + <InstanceCreatePage + forceId="default" + onConfirm={() => { + route(InstancePaths.order_list); + }} + /> + </Fragment> + ); + } + if (props) { + return <Next {...props} />; + } + return <Next />; + }; + } + + const clearTokenAndGoToRoot = () => { + route("/"); + // clear all tokens + updateToken(undefined) + updateDefaultToken(undefined) + }; + + return ( + <InstanceContextProvider value={value}> + <Menu + instance={id} + admin={admin} + onShowSettings={() => { + route(InstancePaths.interface) + }} + path={path} + onLogout={clearTokenAndGoToRoot} + setInstanceName={setInstanceName} + isPasswordOk={defaultToken !== undefined} + /> + <KycBanner /> + <NotificationCard notification={globalNotification} /> + + <Router + onChange={(e) => { + const movingOutFromNotification = + globalNotification && e.url !== globalNotification.to; + if (movingOutFromNotification) { + setGlobalNotification(undefined); + } + }} + > + <Route path="/" component={Redirect} to={InstancePaths.order_list} /> + {/** + * Admin pages + */} + {admin && ( + <Route + path={AdminPaths.list_instances} + component={InstanceListPage} + onCreate={() => { + route(AdminPaths.new_instance); + }} + onUpdate={(id: string): void => { + route(`/instance/${id}/update`); + }} + setInstanceName={setInstanceName} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + )} + {admin && ( + <Route + path={AdminPaths.new_instance} + component={InstanceCreatePage} + onBack={() => route(AdminPaths.list_instances)} + onConfirm={() => { + route(InstancePaths.order_list); + }} + /> + )} + {admin && ( + <Route + path={AdminPaths.update_instance} + component={AdminInstanceUpdatePage} + onBack={() => 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 + path={InstancePaths.settings} + component={InstanceUpdatePage} + onBack={() => { + route(`/`); + }} + onConfirm={() => { + route(`/`); + }} + onUpdateError={noop} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + {/** + * Update instance page + */} + <Route + path={InstancePaths.token} + component={TokenPage} + onChange={() => { + route(`/`); + }} + onCancel={() => { + route(InstancePaths.order_list) + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> + {/** + * Inventory pages + */} + <Route + path={InstancePaths.inventory_list} + component={ProductListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.inventory_new); + }} + onSelect={(id: string) => { + route(InstancePaths.inventory_update.replace(":pid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.inventory_update} + component={ProductUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} + onConfirm={() => { + route(InstancePaths.inventory_list); + }} + onBack={() => { + route(InstancePaths.inventory_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.inventory_new} + component={ProductCreatePage} + onConfirm={() => { + route(InstancePaths.inventory_list); + }} + onBack={() => { + route(InstancePaths.inventory_list); + }} + /> + {/** + * Deposit confirmation pages + */} + <Route + path={InstancePaths.deposit_confirmation_list} + component={DepositConfirmationListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.deposit_confirmation_new); + }} + onSelect={(id: string) => { + route(InstancePaths.deposit_confirmation_update.replace(":pid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.deposit_confirmation_update} + component={DepositConfirmationUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.deposit_confirmation_list)} + onConfirm={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onBack={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.deposit_confirmation_new} + component={DepositConfirmationCreatePage} + onConfirm={() => { + route(InstancePaths.deposit_confirmation_list); + }} + onBack={() => { + route(InstancePaths.deposit_confirmation_list); + }} + /> + {/** + * Bank pages + */} + <Route + path={InstancePaths.bank_list} + component={BankAccountListPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.bank_new); + }} + onSelect={(id: string) => { + route(InstancePaths.bank_update.replace(":bid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_update} + component={BankAccountUpdatePage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.bank_new} + component={BankAccountCreatePage} + onConfirm={() => { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + /> + {/** + * Order pages + */} + <Route + path={InstancePaths.order_list} + component={OrderListPage} + onCreate={() => { + route(InstancePaths.order_new); + }} + onSelect={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + <Route + path={InstancePaths.order_details} + component={OrderDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + <Route + path={InstancePaths.order_new} + component={OrderCreatePage} + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); + }} + onBack={() => { + route(InstancePaths.order_list); + }} + /> + {/** + * Transfer pages + */} + <Route + path={InstancePaths.transfers_list} + component={TransferListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.transfers_new); + }} + /> + <Route + path={InstancePaths.transfers_new} + component={TransferCreatePage} + onConfirm={() => { + route(InstancePaths.transfers_list); + }} + onBack={() => { + route(InstancePaths.transfers_list); + }} + /> + {/** + * Webhooks pages + */} + <Route + path={InstancePaths.webhooks_list} + component={WebhookListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.webhooks_new); + }} + onSelect={(id: string) => { + route(InstancePaths.webhooks_update.replace(":tid", id)); + }} + /> + <Route + path={InstancePaths.webhooks_update} + component={WebhookUpdatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + <Route + path={InstancePaths.webhooks_new} + component={WebhookCreatePage} + onConfirm={() => { + route(InstancePaths.webhooks_list); + }} + onBack={() => { + route(InstancePaths.webhooks_list); + }} + /> + {/** + * Validator pages + */} + <Route + path={InstancePaths.otp_devices_list} + component={ValidatorListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + route(InstancePaths.otp_devices_new); + }} + onSelect={(id: string) => { + route(InstancePaths.otp_devices_update.replace(":vid", id)); + }} + /> + <Route + path={InstancePaths.otp_devices_update} + component={ValidatorUpdatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + <Route + path={InstancePaths.otp_devices_new} + component={ValidatorCreatePage} + onConfirm={() => { + route(InstancePaths.otp_devices_list); + }} + onBack={() => { + route(InstancePaths.otp_devices_list); + }} + /> + {/** + * Templates pages + */} + <Route + path={InstancePaths.templates_list} + component={TemplateListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onCreate={() => { + 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 + path={InstancePaths.templates_update} + component={TemplateUpdatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_new} + component={TemplateCreatePage} + onConfirm={() => { + route(InstancePaths.templates_list); + }} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_use} + component={TemplateUsePage} + onOrderCreated={(id: string) => { + route(InstancePaths.order_details.replace(":oid", id)); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + <Route + path={InstancePaths.templates_qr} + component={TemplateQrPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.templates_list); + }} + /> + + {/** + * reserves pages + */} + <Route + path={InstancePaths.reserves_list} + component={ReservesListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} + onSelect={(id: string) => { + route(InstancePaths.reserves_details.replace(":rid", id)); + }} + onCreate={() => { + route(InstancePaths.reserves_new); + }} + /> + <Route + path={InstancePaths.reserves_details} + component={ReservesDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route + path={InstancePaths.reserves_new} + component={ReservesCreatePage} + onConfirm={() => { + route(InstancePaths.reserves_list); + }} + onBack={() => { + route(InstancePaths.reserves_list); + }} + /> + <Route path={InstancePaths.kyc} component={ListKYCPage} /> + <Route path={InstancePaths.interface} component={Settings} /> + {/** + * Example pages + */} + <Route path="/loading" component={Loading} /> + <Route default component={NotFoundPage} /> + </Router> + </InstanceContextProvider> + ); +} + +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 ( + <InstanceContextProvider value={value}> + <InstanceAdminUpdatePage + {...rest} + instanceId={id} + onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { + 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 ( + <Fragment> + <NotificationCard notification={notif} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + ); + }} + onUnauthorized={() => { + return ( + <Fragment> + <NotificationCard + notification={{ + message: i18n.str`Access denied`, + description: i18n.str`The access token provided is invalid`, + type: "ERROR", + }} + /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + ); + }} + /> + </InstanceContextProvider> + ); +} + +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 <Fragment />; + return ( + <NotificationCard + notification={{ + type: "WARN", + message: "KYC verification needed", + description: ( + <div> + <p> + Some transfer are on hold until a KYC process is completed. Go to + the KYC section in the left panel for more information + </p> + <div class="buttons is-right"> + <button class="button" onClick={() => setLastHide(today)}> + <i18n.Translate>Hide for today</i18n.Translate> + </button> + </div> + </div> + ), + }} + /> + ); +} |