merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 3b7d2dd5275c36460560721f7c4c9a7c793c6510
parent 40bcbdfa0543f3cb3aa4eb84df098b9b48af94c7
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 16 Dec 2021 16:20:04 -0300

adding kyc frontend

Diffstat:
Mpackages/merchant-backend/src/hooks/product.ts | 39---------------------------------------
Mpackages/merchant-backoffice/src/ApplicationReadyRoutes.tsx | 26++++++--------------------
Mpackages/merchant-backoffice/src/InstanceRoutes.tsx | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/merchant-backoffice/src/components/menu/SideBar.tsx | 13+++++++++++++
Mpackages/merchant-backoffice/src/hooks/backend.ts | 3+++
Mpackages/merchant-backoffice/src/hooks/instance.ts | 35+++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 353 insertions(+), 67 deletions(-)

diff --git a/packages/merchant-backend/src/hooks/product.ts b/packages/merchant-backend/src/hooks/product.ts @@ -79,46 +79,7 @@ export function useProductAPI(): ProductAPI { data, }); - /** - * There is some inconsistency in how the cache is evicted. - * I'm keeping this for later inspection - */ - - // -- Clear all cache - // -- This seems to work always but is bad - - // const keys = [...cache.keys()] - // console.log(keys) - // cache.clear() - // await Promise.all(keys.map(k => trigger(k))) - - // -- From the keys to the cache trigger - // -- An intermediate step - - // const keys = [ - // [`/private/products`, token, url], - // [`/private/products/${productId}`, token, url], - // ] - // cache.clear() - // const f: string[][] = keys.map(k => cache.serializeKey(k)) - // console.log(f) - // const m = flat(f) - // console.log(m) - // await Promise.all(m.map(k => trigger(k, true))) - - // await Promise.all(keys.map(k => mutate(k))) - - // -- This is how is supposed to be use - - // await mutate([`/private/products`, token, url]) - // await mutate([`/private/products/${productId}`, token, url]) - - // await mutateAll(/@"\/private\/products"@/); await mutateAll(/@"\/private\/products\/.*"@/); - // return true - // return r - - // -- FIXME: why this un-break the tests? return Promise.resolve(); }; diff --git a/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx @@ -98,7 +98,6 @@ export function ApplicationReadyRoutes(): VNode { <Route default component={DefaultMainRoute} - clearTokenAndGoToRoot={clearTokenAndGoToRoot} admin={admin} instanceNameByBackendURL={instanceNameByBackendURL} /> @@ -106,29 +105,16 @@ export function ApplicationReadyRoutes(): VNode { ); } -function DefaultMainRoute({ - clearTokenAndGoToRoot, - instance, - admin, - instanceNameByBackendURL, -}: any) { +function DefaultMainRoute({ instance, admin, instanceNameByBackendURL }: any) { const [instanceName, setInstanceName] = useState( instanceNameByBackendURL || instance || "default" ); return ( - <Fragment> - <Menu - instance={instanceName} - admin={admin} - onLogout={clearTokenAndGoToRoot} - setInstanceName={setInstanceName} - /> - <InstanceRoutes - admin={admin} - id={instanceName} - setInstanceName={setInstanceName} - /> - </Fragment> + <InstanceRoutes + admin={admin} + id={instanceName} + setInstanceName={setInstanceName} + /> ); } diff --git a/packages/merchant-backoffice/src/InstanceRoutes.tsx b/packages/merchant-backoffice/src/InstanceRoutes.tsx @@ -23,12 +23,16 @@ 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 { NotificationCard } from "./components/menu"; +import { Menu, NotificationCard } from "./components/menu"; import { useBackendContext } from "./context/backend"; import { InstanceContextProvider } from "./context/instance"; -import { useBackendDefaultToken, useBackendInstanceToken } from "./hooks"; +import { + useBackendDefaultToken, + useBackendInstanceToken, + useLocalStorage, +} from "./hooks"; import { HttpError } from "./hooks/backend"; -import { useTranslator } from "./i18n"; +import { Translate, useTranslator } from "./i18n"; import InstanceCreatePage from "./paths/admin/create"; import InstanceListPage from "./paths/admin/list"; import OrderCreatePage from "./paths/instance/orders/create"; @@ -42,6 +46,7 @@ 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, @@ -49,6 +54,8 @@ import InstanceUpdatePage, { 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 = '/', @@ -67,6 +74,8 @@ export enum InstancePaths { reserves_details = "/reserves/:rid/details", reserves_new = "/reserves/new", + kyc = "/kyc", + transfers_list = "/transfers", transfers_new = "/transfer/new", } @@ -89,15 +98,19 @@ export interface Props { export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode { const [_, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); - const { updateLoginStatus: changeBackend, addTokenCleaner } = - useBackendContext(); + const { + updateLoginStatus: changeBackend, + addTokenCleaner, + clearAllTokens, + } = useBackendContext(); const cleaner = useCallback(() => { updateToken(undefined); }, [id]); const i18n = useTranslator(); - const [globalNotification, setGlobalNotification] = useState< - (Notification & { to: string }) | undefined - >(undefined); + + type GlobalNotifState = (Notification & { to: string }) | undefined; + const [globalNotification, setGlobalNotification] = + useState<GlobalNotifState>(undefined); useEffect(() => { addTokenCleaner(cleaner); @@ -178,8 +191,20 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode { }; } + const clearTokenAndGoToRoot = () => { + clearAllTokens(); + route("/"); + }; + return ( <InstanceContextProvider value={value}> + <Menu + instance={id} + admin={admin} + onLogout={clearTokenAndGoToRoot} + setInstanceName={setInstanceName} + /> + <KycBanner /> <NotificationCard notification={globalNotification} /> <Router @@ -395,6 +420,8 @@ export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode { route(InstancePaths.reserves_list); }} /> + + <Route path={InstancePaths.kyc} component={ListKYCPage} /> {/** * Example pages */} @@ -469,3 +496,33 @@ function AdminInstanceUpdatePage({ </InstanceContextProvider> ); } + +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 <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)}> + <Translate>Hide for today</Translate> + </button> + </div> + </div> + ), + }} + /> + ); +} diff --git a/packages/merchant-backoffice/src/components/menu/SideBar.tsx b/packages/merchant-backoffice/src/components/menu/SideBar.tsx @@ -24,6 +24,7 @@ import { useCallback } from "preact/hooks"; import { useBackendContext } from "../../context/backend"; import { useConfigContext } from "../../context/config"; import { useInstanceContext } from "../../context/instance"; +import { useInstanceKYCDetails } from "../../hooks/instance"; import { Translate } from "../../i18n"; import { LangSelector } from "./LangSelector"; @@ -45,6 +46,8 @@ export function Sidebar({ const config = useConfigContext(); const backend = useBackendContext(); + const kycStatus = useInstanceKYCDetails(); + const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; // const withInstanceIdIfNeeded = useCallback(function (path: string) { // if (mimic) { // return path + '?instance=' + instance @@ -130,6 +133,16 @@ export function Sidebar({ <span class="menu-item-label">Reserves</span> </a> </li> + {needKYC && ( + <li> + <a href={"/kyc"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-account-check" /> + </span> + <span class="menu-item-label">KYC Status</span> + </a> + </li> + )} </ul> <p class="menu-label"> <Translate>Connection</Translate> diff --git a/packages/merchant-backoffice/src/hooks/backend.ts b/packages/merchant-backoffice/src/hooks/backend.ts @@ -66,6 +66,7 @@ export interface RequestInfo { hasToken: boolean; params: unknown; data: unknown; + status: number; } interface HttpResponseLoading<T> { @@ -163,6 +164,7 @@ function buildRequestOk<T>( data: res.config.data, url, hasToken, + status: res.status, }, }; } @@ -187,6 +189,7 @@ function buildRequestFailed( params: ex.request?.params, url, hasToken, + status: status || 0, }; if (status && status >= 400 && status < 500) { diff --git a/packages/merchant-backoffice/src/hooks/instance.ts b/packages/merchant-backoffice/src/hooks/instance.ts @@ -214,6 +214,41 @@ export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.Que return { loading: true }; } +type KYCStatus = + | { type: "ok" } + | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects }; + +export function useInstanceKYCDetails(): HttpResponse<KYCStatus> { + const { url: baseUrl, token: baseToken } = useBackendContext(); + const { token: instanceToken, id, admin } = useInstanceContext(); + + const { url, token } = !admin + ? { url: baseUrl, token: baseToken } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; + + const { data, error } = useSWR< + HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>, + HttpError + >([`/private/kyc`, token, url], fetcher, { + refreshInterval: 5000, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + }); + + if (data) { + if (data.info?.status === 202) + return { ok: true, data: { type: "redirect", status: data.data } }; + return { ok: true, data: { type: "ok" } }; + } + if (error) return error; + return { loading: true }; +} + export function useManagedInstanceDetails( instanceId: string ): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { diff --git a/packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx @@ -0,0 +1,180 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; + +export interface Props { + status: MerchantBackend.Instances.AccountKycRedirects; +} + +export function ListPage({ status }: Props): VNode { + const i18n = useTranslator(); + + return ( + <section class="section is-main-section"> + <p>asdasdasd</p> + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + <Translate>Pending KYC verification</Translate> + </p> + + <div class="card-header-icon" aria-label="more options" /> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {status.pending_kycs.length > 0 ? ( + <PendingTable entries={status.pending_kycs} /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + + {status.timeout_kycs.length > 0 ? ( + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + <Translate>Timed out</Translate> + </p> + + <div class="card-header-icon" aria-label="more options" /> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {status.timeout_kycs.length > 0 ? ( + <TimedOutTable entries={status.timeout_kycs} /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + ) : undefined} + </section> + ); +} +interface PendingTableProps { + entries: MerchantBackend.Instances.MerchantAccountKycRedirect[]; +} + +interface TimedOutTableProps { + entries: MerchantBackend.Instances.ExchangeKycTimeout[]; +} + +function PendingTable({ entries }: PendingTableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <Translate>Exchange</Translate> + </th> + <th> + <Translate>Target account</Translate> + </th> + <th> + <Translate>KYC URL</Translate> + </th> + </tr> + </thead> + <tbody> + {entries.map((e, i) => { + return ( + <tr key={i}> + <td>{e.exchange_url}</td> + <td>{e.payto_uri}</td> + <td> + <a href={e.kyc_url} target="_black" rel="noreferrer"> + {e.kyc_url} + </a> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} + +function TimedOutTable({ entries }: TimedOutTableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <Translate>Exchange</Translate> + </th> + <th> + <Translate>Code</Translate> + </th> + <th> + <Translate>Http Status</Translate> + </th> + </tr> + </thead> + <tbody> + {entries.map((e, i) => { + return ( + <tr key={i}> + <td>{e.exchange_url}</td> + <td>{e.exchange_code}</td> + <td>{e.exchange_http_status}</td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); +} + +function EmptyTable(): VNode { + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-emoticon-happy mdi-48px" /> + </span> + </p> + <p> + <Translate>No pending kyc verification!</Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx @@ -0,0 +1,51 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading"; +import { HttpError } from "../../../../hooks/backend"; +import { useInstanceKYCDetails } from "../../../../hooks/instance"; +import { ListPage } from "./ListPage"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; +} + +export default function ListKYC({ + onUnauthorized, + onLoadError, + onNotFound, +}: Props): VNode { + const result = useInstanceKYCDetails(); + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); + + const status = result.data.type === "ok" ? undefined : result.data.status; + + if (!status) { + return <div>no kyc required</div>; + } + return <ListPage status={status} />; +}