[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 03/03: adding kyc frontend
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 03/03: adding kyc frontend |
Date: |
Thu, 16 Dec 2021 20:20:47 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
commit 3b7d2dd5275c36460560721f7c4c9a7c793c6510
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Dec 16 16:20:04 2021 -0300
adding kyc frontend
---
packages/merchant-backend/src/hooks/product.ts | 39 -----
.../src/ApplicationReadyRoutes.tsx | 26 +--
.../merchant-backoffice/src/InstanceRoutes.tsx | 73 ++++++++-
.../src/components/menu/SideBar.tsx | 13 ++
packages/merchant-backoffice/src/hooks/backend.ts | 3 +
packages/merchant-backoffice/src/hooks/instance.ts | 35 ++++
.../src/paths/instance/kyc/list/ListPage.tsx | 180 +++++++++++++++++++++
.../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
index 04cbea8..4fc8bcc 100644
--- 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
index 12234be..ebc3d1d 100644
--- 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
index 20a7601..06f1db1 100644
--- 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
index 79ab044..231ac7d 100644
--- 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
index 1b27cfe..789cfc8 100644
--- 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
index d3a55da..9153e19 100644
--- 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
new file mode 100644
index 0000000..13f9b03
--- /dev/null
+++ 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
new file mode 100644
index 0000000..5dff019
--- /dev/null
+++ 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} />;
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.