gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]