aboutsummaryrefslogtreecommitdiff
path: root/packages/kyc-ui/src/pages/Start.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/kyc-ui/src/pages/Start.tsx')
-rw-r--r--packages/kyc-ui/src/pages/Start.tsx407
1 files changed, 407 insertions, 0 deletions
diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx
new file mode 100644
index 000000000..3dbecc093
--- /dev/null
+++ b/packages/kyc-ui/src/pages/Start.tsx
@@ -0,0 +1,407 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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/>
+ */
+import {
+ AccessToken,
+ HttpStatusCode,
+ KycRequirementInformation,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ Loading,
+ LocalNotificationBanner,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useKycInfo } from "../hooks/kyc.js";
+import { FillForm } from "./FillForm.js";
+
+type Props = {
+ onLoggedOut: () => void;
+ token: AccessToken;
+};
+
+function ShowReqList({
+ token,
+ onFormSelected,
+}: {
+ token: AccessToken;
+ onFormSelected: (r: KycRequirementInformation) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ // const { lib } = useExchangeApiContext();
+ // const { state, start } = useSessionState();
+ const result = useKycInfo(token);
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={result} />;
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotModified: {
+ return <div> not modified </div>;
+ }
+ case HttpStatusCode.NoContent: {
+ return <div> not requirements </div>;
+ }
+ case HttpStatusCode.Accepted: {
+ return <div> accepted </div>;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
+ }
+
+ const errors = undefinedIfEmpty({
+ // password: !password ? i18n.str`required` : undefined,
+ // url: !url
+ // ? i18n.str`required`
+ // : !safeToURL(url)
+ // ? i18n.str`invalid format`
+ // : undefined,
+ });
+
+ // const onStart =
+ // !!errors
+ // ? undefined
+ // : withErrorHandler(
+ // async () => {
+ // return {
+ // type: "ok",
+ // body: {},
+ // }
+ // // return lib.exchange.uploadKycForm(
+ // // "clientId",
+ // // createRFC8959AccessTokenEncoded(password),
+ // // );
+ // },
+ // (ok) => {
+ // // start({
+ // // nonce: ok.body.nonce,
+ // // clientId,
+ // // redirectURL: url,
+ // // state: encodeCrock(randomBytes(32)),
+ // // });
+
+ // onCreated();
+ // },
+ // // () => {
+ // // // switch (fail.case) {
+ // // // case HttpStatusCode.NotFound:
+ // // // return i18n.str`Client doesn't exist.`;
+ // // // }
+ // // },
+ // );
+
+ // const requirements: typeof result.body.requirements = [{
+ // description: "this is the form description, click to show the form field bla bla bla",
+ // form: "asdasd" as KycBuiltInFromId,
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "this is the description of the link and service provider.",
+ // form: "LINK",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }, {
+ // description: "you can't click this because this is only information, wait until AML officer replies.",
+ // form: "INFO",
+ // description_i18n: {},
+ // id: "ASDASD" as KycRequirementInformationId,
+ // }]
+ const requirements = result.body.requirements;
+ if (!result.body.requirements.length) {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>No requirements for this account</i18n.Translate>
+ </h2>
+ </div>
+ <div class="m-8">
+ <Attention title={i18n.str`Kyc completed`} type="success">
+ <i18n.Translate>You can close this now</i18n.Translate>
+ </Attention>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Complete any of the following requirements
+ </i18n.Translate>
+ </h2>
+ </div>
+
+ <div class="mt-8">
+ <ul
+ role="list"
+ class=" divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
+ >
+ {requirements.map((req, idx) => {
+ return (
+ <li
+ key={idx}
+ class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"
+ >
+ <RequirementRow
+ requirement={req}
+ onFormSelected={() => {
+ onFormSelected(req);
+ }}
+ />
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
+export function Start({ token, onLoggedOut }: Props): VNode {
+ const [req, setReq] = useState<KycRequirementInformation>();
+ // if (!state) {
+ // return <Loading />;
+ // }
+
+ if (!req) {
+ return <ShowReqList token={token} onFormSelected={(r) => setReq(r)} />;
+ }
+ return (
+ <FillForm
+ formId={req.form}
+ requirement={req}
+ token={token}
+ onComplete={() => {
+ setReq(undefined);
+ }}
+ />
+ );
+}
+
+function RequirementRow({
+ requirement: req,
+ onFormSelected,
+}: {
+ requirement: KycRequirementInformation;
+ onFormSelected: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { lib } = useExchangeApiContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const reqId = req.id;
+ const startHandler = !reqId
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.exchange.startExternalKycProcess(reqId);
+ },
+ (res) => {
+ window.open(res.body.redirect_url, "_blank");
+ },
+ );
+
+ switch (req.form) {
+ case "INFO": {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Information</i18n.Translate>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ </Fragment>
+ );
+ }
+ case "LINK": {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <Button type="submit" handler={startHandler}>
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Begin KYC process</i18n.Translate>
+ </Button>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ <div class="flex shrink-0 items-center gap-x-4">
+ <div class="hidden sm:flex sm:flex-col sm:items-end">
+ <p class="text-sm leading-6 text-gray-900">
+ <i18n.Translate>Start</i18n.Translate>
+ </p>
+ </div>
+ <svg
+ class="h-5 w-5 flex-none text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </Fragment>
+ );
+ }
+ default: {
+ return (
+ <Fragment>
+ <div class="flex min-w-0 gap-x-4">
+ <div class="inline-block h-10 w-10 rounded-full">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="size-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
+ />
+ </svg>
+ </div>
+ <div class="min-w-0 flex-auto">
+ <p class="text-sm font-semibold leading-6 text-gray-900">
+ <button onClick={onFormSelected}>
+ <span class="absolute inset-x-0 -top-px bottom-0"></span>
+ <i18n.Translate>Form</i18n.Translate>
+ </button>
+ </p>
+ <p class="mt-1 flex text-xs leading-5 text-gray-500">
+ {req.description}
+ </p>
+ </div>
+ </div>
+ <div class="flex shrink-0 items-center gap-x-4">
+ <div class="hidden sm:flex sm:flex-col sm:items-end">
+ <p class="text-sm leading-6 text-gray-900">Fill form</p>
+ </div>
+ <svg
+ class="h-5 w-5 flex-none text-gray-400"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </div>
+ </Fragment>
+ );
+ }
+ }
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null): void {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
+export function undefinedIfEmpty<T extends object | undefined>(
+ obj: T,
+): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}