diff options
Diffstat (limited to 'packages/kyc-ui/src/pages/Start.tsx')
-rw-r--r-- | packages/kyc-ui/src/pages/Start.tsx | 407 |
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; +} |