taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 60fa2f8c6fe2d1902624052746c2a7a83d2f4c65
parent 2bea1081cd7ee9a5d8195069d1857eede7f4bbb8
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 24 Oct 2025 20:39:33 -0300

kyc

Diffstat:
Mpackages/kyc-ui/src/pages/FillForm.tsx | 63+++++++++++++++++++++++++++++----------------------------------
Mpackages/kyc-ui/src/pages/Start.tsx | 70++++++++++++++++++++++------------------------------------------------
Mpackages/kyc-ui/src/pages/TriggerForms.tsx | 5++---
Mpackages/kyc-ui/src/pages/TriggerKyc.tsx | 278+++++++++++++++++++++++++++++++------------------------------------------------
4 files changed, 160 insertions(+), 256 deletions(-)

diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -24,7 +24,7 @@ import { import { AcceptTermOfServiceContext, Attention, - Button, + ButtonBetter, ErrorsSummary, FormMetadata, FormUI, @@ -34,8 +34,8 @@ import { preloadedForms, useAsyncAsHook, useExchangeApiContext, - useLocalNotificationHandler, - useTranslationContext, + useLocalNotificationBetter, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useFormMeta } from "../../../web-util/src/hooks/useForm.js"; @@ -115,7 +115,7 @@ function ShowForm({ onComplete: () => void; }): VNode { const { lib } = useExchangeApiContext(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [preferences] = usePreferences(); const { i18n } = useTranslationContext(); @@ -125,33 +125,29 @@ function ShowForm({ design, } = useFormMeta<FormType>(theForm, formContext, {}); const validatedForm = status.status !== "ok" ? undefined : status.result; + if (validatedForm) { + validatedForm[TalerFormAttributes.FORM_ID] = theForm.id; + validatedForm[TalerFormAttributes.FORM_VERSION] = theForm.version; + validatedForm[TalerFormAttributes.FORM_CONTEXT] = formContext; + } - const submitHandler = - validatedForm === undefined - ? undefined - : withErrorHandler( - async () => { - validatedForm[TalerFormAttributes.FORM_ID] = theForm.id; - validatedForm[TalerFormAttributes.FORM_VERSION] = theForm.version; - validatedForm[TalerFormAttributes.FORM_CONTEXT] = formContext; - return lib.exchange.uploadKycForm(reqId, validatedForm); - }, - (res) => { - onComplete(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.PayloadTooLarge: - return i18n.str`The form is too big for the server, try uploading smaller files.`; - case HttpStatusCode.NotFound: - return i18n.str`The account was not found`; - case HttpStatusCode.Conflict: - return i18n.str`Officer disabled or more recent decision was already submitted.`; - default: - assertUnreachable(fail); - } - }, - ); + const submit = safeFunctionHandler( + lib.exchange.uploadKycForm, + !validatedForm ? undefined : [reqId, validatedForm], + ); + submit.onSuccess = onComplete; + submit.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.PayloadTooLarge: + return i18n.str`The form is too big for the server, try uploading smaller files.`; + case HttpStatusCode.NotFound: + return i18n.str`The account was not found`; + case HttpStatusCode.Conflict: + return i18n.str`Officer disabled or more recent decision was already submitted.`; + default: + assertUnreachable(fail); + } + }; return ( <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> @@ -181,14 +177,13 @@ function ShowForm({ > <i18n.Translate>Cancel</i18n.Translate> </button> - <Button + <ButtonBetter type="submit" - handler={submitHandler} - disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + onClick={submit} > <i18n.Translate>Submit</i18n.Translate> - </Button> + </ButtonBetter> </div> {!status.errors ? undefined : <ErrorsSummary errors={status.errors} />} </div> diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -23,11 +23,12 @@ import { } from "@gnu-taler/taler-util"; import { Attention, + ButtonBetter, ErrorLoading, Loading, LocalNotificationBanner, useExchangeApiContext, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -168,9 +169,26 @@ export function Start({ token }: Props): VNode { function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { const { i18n } = useTranslationContext(); - const [notification, , notify] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { lib } = useExchangeApiContext(); + const start = safeFunctionHandler((id: string) => + lib.exchange.startExternalKycProcess(id), + ); + start.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.NotFound: + return i18n.str`not found`; + case HttpStatusCode.Conflict: + return i18n.str`conflict`; + case HttpStatusCode.PayloadTooLarge: + return i18n.str`payload is too large`; + } + }; + start.onSuccess = (success) => { + setRedirectUrl(success.body.redirect_url); + return i18n.str`Link generated, you can proceed.`; + }; const [redirectUrl, setRedirectUrl] = useState<string>(); const row = ( <Fragment> @@ -218,56 +236,12 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { ) : ( // href={redirectUrl} <p class="text-sm font-semibold leading-6 text-gray-900"> - <button - onClick={async () => { - const res = await lib.exchange.startExternalKycProcess( - req.id!, - ); - if (res.type === "ok") { - setRedirectUrl(res.body.redirect_url); - notify({ - type: "info", - title: i18n.str`Link generated, you can proceed.`, - when: AbsoluteTime.now(), - }); - } else { - switch (res.case) { - case HttpStatusCode.NotFound: { - notify({ - type: "error", - title: i18n.str`could not create redirect url`, - description: i18n.str`not found`, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.Conflict: { - notify({ - type: "error", - title: i18n.str`could not create redirect url`, - description: i18n.str`conflict`, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.PayloadTooLarge: { - notify({ - type: "error", - title: i18n.str`could not create redirect url`, - description: i18n.str`payload too large`, - when: AbsoluteTime.now(), - }); - break; - } - } - } - }} - > + <ButtonBetter onClick={start}> <span class="absolute inset-x-0 -top-px bottom-0"></span> <i18n.Translate context="KYC_REQUIREMENT_LINK_DESCRIPTION"> {req.description} </i18n.Translate> - </button> + </ButtonBetter> </p> )} </div> diff --git a/packages/kyc-ui/src/pages/TriggerForms.tsx b/packages/kyc-ui/src/pages/TriggerForms.tsx @@ -20,7 +20,7 @@ import { preloadedForms, UIHandlerId, useFormMeta, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -35,8 +35,7 @@ type Props = { export function TriggerForms({ formId }: Props): VNode { const { i18n } = useTranslationContext(); - const [notification, withErrorHandler, notify] = - useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const pf = preloadedForms(i18n); diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -14,31 +14,33 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - AbsoluteTime, AccessToken, AmountJson, Amounts, + AmountString, assertUnreachable, createNewWalletKycAccount, eddsaGetPublic, encodeCrock, HttpStatusCode, + opFixedSuccess, + opKnownFailure, signKycAuth, signWalletAccountSetup, - WalletKycRequest, + WalletKycRequest } from "@gnu-taler/taler-util"; import { - Button, + ButtonBetter, FormMetadata, FormUI, LocalNotificationBanner, UIHandlerId, useExchangeApiContext, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; +import { useMemo } from "preact/hooks"; import { useFormMeta } from "../../../web-util/src/hooks/useForm.js"; type FormType = { @@ -51,10 +53,8 @@ type Props = { export function TriggerKyc({ onKycStarted }: Props): VNode { const { i18n } = useTranslationContext(); - const [notification, withErrorHandler, notify] = - useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { config, lib } = useExchangeApiContext(); - const [kycAccount, setKycAccount] = useState<string>(); const theForm: FormMetadata = { id: "asd", @@ -98,101 +98,66 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { return createNewWalletKycAccount(extraEntropy); }, [1]); - useEffect(() => { - if (!kycAccount) return; - const paytoHash = kycAccount; - async function check() { - const { signingKey } = await accountPromise; - const merchantPub = eddsaGetPublic(signingKey); - const accountOwnerSig = encodeCrock(signKycAuth(signingKey)); - const result = await lib.exchange.checkKycStatus({ - accountPub: encodeCrock(merchantPub), - accountSig: accountOwnerSig, - paytoHash, - }); - switch (result.case) { - case "ok": - console.log("empty body"); - break; - case HttpStatusCode.Ok: - case HttpStatusCode.Accepted: - onKycStarted(result.body.access_token); - break; - case HttpStatusCode.Forbidden: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`access denied`, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.NotFound: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`not found`, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.Conflict: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`conflict`, - when: AbsoluteTime.now(), - }); - break; - } + const send = safeFunctionHandler( + async (balance: AmountString) => { + const account = await accountPromise; + const limit: WalletKycRequest = { + balance, + reserve_pub: account.id, + reserve_sig: encodeCrock( + signWalletAccountSetup(account.signingKey, balance), + ), + }; + const resp = await lib.exchange.notifyKycBalanceLimit(limit); + if (resp.type === "ok") { + return opKnownFailure(HttpStatusCode.Ok); } - } - check(); - }, [kycAccount]); - - function triggerAmount(amount: AmountJson) { - return withErrorHandler( - async () => { - const account = await accountPromise; - const balance = Amounts.stringify(amount); - const limit: WalletKycRequest = { - balance, - reserve_pub: account.id, - reserve_sig: encodeCrock( - signWalletAccountSetup(account.signingKey, balance), - ), - }; - return lib.exchange.notifyKycBalanceLimit(limit); - }, - (res) => { - notify({ - type: "info", - title: i18n.str`No kyc required`, - when: AbsoluteTime.now(), + if (resp.case === HttpStatusCode.UnavailableForLegalReasons) { + const paytoHash = resp.body.h_payto; + const { signingKey } = await accountPromise; + const merchantPub = eddsaGetPublic(signingKey); + const accountOwnerSig = encodeCrock(signKycAuth(signingKey)); + const result = await lib.exchange.checkKycStatus({ + accountPub: encodeCrock(merchantPub), + accountSig: accountOwnerSig, + paytoHash, }); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Forbidden: - return i18n.str`Access denied trying to test balance.`; - case HttpStatusCode.UnavailableForLegalReasons: - setKycAccount(fail.body.h_payto); - return i18n.str`Unavailable For Legal Reasons`; - default: - assertUnreachable(fail); + if (result.type === "ok") { + return opKnownFailure(HttpStatusCode.Ok); } - }, - ); - } - - const sendFormValue = + if ( + result.type === "fail" && + (result.case === HttpStatusCode.Accepted || + result.case === HttpStatusCode.Ok) + ) { + return opFixedSuccess(result.body); + } + return result; + } + return resp; + }, theForm === undefined || status.status === "fail" ? undefined - : triggerAmount(status.result.amount); + : [Amounts.stringify(status.result.amount)], + ); - if (kycAccount) { - return <div>loading...</div>; - } + send.onSuccess = (success) => { + onKycStarted(success.body.access_token); + }; + send.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Ok: + return i18n.str`No kyc triggered.`; + case HttpStatusCode.Forbidden: + return i18n.str`Forbidden.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict.`; + default: + assertUnreachable(fail); + } + }; return ( <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> @@ -208,14 +173,13 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { > <i18n.Translate>Cancel</i18n.Translate> </button> - <Button + <ButtonBetter type="submit" - handler={sendFormValue} - // disabled={!submitHandler} + onClick={send} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Submit</i18n.Translate> - </Button> + </ButtonBetter> </div> <div class="grid grid-cols-1 gap-x-8 gap-y-4 "> @@ -227,172 +191,144 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { </i18n.Translate> </p> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000000`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000000`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger TOPS Terms of service</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000010`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000010`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger GLS onboarding</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000020`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000020`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.1</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000030`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000030`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.4</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000040`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000040`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.5</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000050`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000050`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.9</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000060`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000060`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.11</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000070`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000070`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.12</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000080`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000080`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.13</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000090`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000090`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.14</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000100`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000100`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.15</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000110`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000110`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Challenger test</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000120`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000120`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.9 customer</i18n.Translate> - </Button> + </ButtonBetter> </div> <div> - <Button + <ButtonBetter type="submit" - handler={triggerAmount( - Amounts.parseOrThrow(`${config.config.currency}:1000130`), - )} + onClick={send.lambda(() => [`${config.config.currency}:1000130`])} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Trigger VQF 902.9 officer</i18n.Translate> - </Button> + </ButtonBetter> </div> </div> </div>