taler-typescript-core

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

commit c4b3d0f95285211fad7a0d72c2ef4992c1817f65
parent 01ae524ead2264774d877b135a581681651a74f5
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon,  1 Jun 2026 11:14:20 -0300

several fixes and improvements

better error reporting (user can copy the content)
normalize safeHandler and MFA
simplify button
only one notificaton handler
prevent printing sensitive info

Diffstat:
Mpackages/kyc-ui/src/Routing.tsx | 4----
Mpackages/kyc-ui/src/app.tsx | 89+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/kyc-ui/src/pages/FillForm.tsx | 50+++++++++++++++++++++++++-------------------------
Mpackages/kyc-ui/src/pages/Frame.tsx | 29+++++++----------------------
Mpackages/kyc-ui/src/pages/Start.tsx | 55++++++++++++++++++++++++++++++-------------------------
Mpackages/kyc-ui/src/pages/TriggerForms.tsx | 6+-----
Mpackages/kyc-ui/src/pages/TriggerKyc.tsx | 86+++++++++++++++++++++++++++++++++++++++----------------------------------------
7 files changed, 151 insertions(+), 168 deletions(-)

diff --git a/packages/kyc-ui/src/Routing.tsx b/packages/kyc-ui/src/Routing.tsx @@ -23,7 +23,6 @@ import { import { Fragment, VNode, h } from "preact"; import { assertUnreachable } from "@gnu-taler/taler-util"; -import { useErrorBoundary } from "preact/hooks"; import { useSessionState } from "./hooks/session.js"; import { ChallengeCompleted } from "./pages/ChallengeCompleted.js"; import { Frame } from "./pages/Frame.js"; @@ -72,9 +71,6 @@ function PublicRounting(): VNode { const location = useCurrentLocation(publicPages); const { state, start } = useSessionState(); const { navigateTo } = useNavigationContext(); - useErrorBoundary((e) => { - console.log("error", e); - }); const currentToken = state?.accessToken; switch (location.name) { case undefined: { diff --git a/packages/kyc-ui/src/app.tsx b/packages/kyc-ui/src/app.tsx @@ -26,6 +26,7 @@ import { BrowserHashNavigationProvider, ExchangeApiProvider, Loading, + NotificationProvider, TalerWalletIntegrationBrowserProvider, TranslationProvider, UiForms, @@ -59,51 +60,53 @@ export function App(): VNode { return ( <SettingsProvider value={settings}> <TranslationProvider source={strings}> - <ExchangeApiProvider - baseUrl={new URL("/", baseUrl)} - frameOnError={Frame} - evictors={{ - exchange: evictExchangeSwrCache, - }} - > - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - // normally, do not revalidate - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - revalidateOnMount: undefined, - focusThrottleInterval: undefined, - - // normally, do not refresh - refreshInterval: undefined, - dedupingInterval: 2000, - refreshWhenHidden: false, - refreshWhenOffline: false, - - // ignore errors - shouldRetryOnError: false, - errorRetryCount: 0, - errorRetryInterval: undefined, - - // do not go to loading again if already has data - keepPreviousData: true, + <NotificationProvider> + <ExchangeApiProvider + baseUrl={new URL("/", baseUrl)} + frameOnError={Frame} + evictors={{ + exchange: evictExchangeSwrCache, }} > - <TalerWalletIntegrationBrowserProvider> - <BrowserHashNavigationProvider> - <UiFormsProvider value={forms}> - <NotifierProvider> - <Routing /> - </NotifierProvider> - </UiFormsProvider> - </BrowserHashNavigationProvider> - </TalerWalletIntegrationBrowserProvider> - </SWRConfig> - </ExchangeApiProvider> + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE + ? localStorageProvider + : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, + + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, + + // ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, + + // do not go to loading again if already has data + keepPreviousData: true, + }} + > + <TalerWalletIntegrationBrowserProvider> + <BrowserHashNavigationProvider> + <UiFormsProvider value={forms}> + <NotifierProvider> + <Routing /> + </NotifierProvider> + </UiFormsProvider> + </BrowserHashNavigationProvider> + </TalerWalletIntegrationBrowserProvider> + </SWRConfig> + </ExchangeApiProvider> + </NotificationProvider> </TranslationProvider> </SettingsProvider> ); diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -24,17 +24,16 @@ import { import { AcceptTermOfServiceContext, Attention, - ButtonBetter, + Button, ErrorsSummary, FormMetadata, FormUI, InternationalizationAPI, Loading, - LocalNotificationBanner, preloadedForms, useAsyncAsHook, useExchangeApiContext, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -115,7 +114,7 @@ function ShowForm({ onComplete: () => void; }): VNode { const { lib } = useExchangeApiContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const [preferences] = usePreferences(); const { i18n } = useTranslationContext(); @@ -131,30 +130,31 @@ function ShowForm({ validatedForm[TalerFormAttributes.FORM_CONTEXT] = formContext; } - const submit = safeFunctionHandler( - i18n.str`upload kyc form`, - lib.exchange.uploadKycForm.bind(lib.exchange), - !validatedForm ? undefined : [reqId, validatedForm], + const submit = actionHandler( + (ct, id, f) => lib.exchange.uploadKycForm(id, f), + !validatedForm ? undefined : ([reqId, validatedForm] as const), ); 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.InternalServerError: - return i18n.str`There was a problem processing your request. Please try again later.`; - 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); - } - }; + submit.onFail = showError( + i18n.str`Failed to upload the KYC information.`, + (fail) => { + switch (fail.case) { + case HttpStatusCode.PayloadTooLarge: + return i18n.str`The form is too big for the server, try uploading smaller files.`; + case HttpStatusCode.InternalServerError: + return i18n.str`There was a problem processing your request. Please try again later.`; + 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"> - <LocalNotificationBanner notification={notification} /> <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> <FormUI model={handler} design={design} /> </div> @@ -181,13 +181,13 @@ function ShowForm({ > <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetter + <Button submit 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> - </ButtonBetter> + </Button> </div> {!status.errors ? undefined : <ErrorsSummary errors={status.errors} />} </div> diff --git a/packages/kyc-ui/src/pages/Frame.tsx b/packages/kyc-ui/src/pages/Frame.tsx @@ -14,25 +14,22 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TranslatedString } from "@gnu-taler/taler-util"; import { Footer, Header, RouteDefinition, ToastBanner, - notifyError, - notifyException, + useRenderErrorReport, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useEffect, useErrorBoundary, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { useNotifierContext } from "../context/notifier.js"; import { getAllBooleanPreferences, getLabelForPreferences, usePreferences, } from "../context/preferences.js"; -import { useSettingsContext } from "../context/settings.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -46,27 +43,15 @@ export function Frame({ routeTestForms?: RouteDefinition; children: ComponentChildren; }): VNode { - const settings = useSettingsContext(); const [preferences, updatePreferences] = usePreferences(); const [title, setTitle] = useState<string>(); const notifier = useNotifierContext(); - const [error, resetError] = useErrorBoundary(); const { i18n } = useTranslationContext(); - useEffect(() => { - if (error) { - if (error instanceof Error) { - console.log("Internal error, please report", error); - notifyException(i18n.str`Internal error, please report.`, error); - } else { - console.log("Internal error, please report", error); - notifyError( - i18n.str`Internal error, please report.`, - String(error) as TranslatedString, - ); - } - resetError(); - } - }, [error]); + + useRenderErrorReport({ + hash: __GIT_HASH__, + version: __VERSION__, + }); useEffect(() => { return notifier.subscribe((event) => { diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -22,13 +22,12 @@ import { } from "@gnu-taler/taler-util"; import { Attention, - ButtonBetter, + Button, ErrorLoading, Loading, - LocalNotificationBanner, useExchangeApiContext, - useLocalNotificationBetter, - useTranslationContext, + useNotificationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -53,7 +52,12 @@ function ShowReqList({ return <Loading />; } if (result instanceof TalerError) { - return <ErrorLoading error={result} />; + return ( + <ErrorLoading + error={result} + title={i18n.str`Failed to load KYC information.`} + /> + ); } if (result.type === "fail") { @@ -176,29 +180,31 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { state: LinkGenerationState.WAIT, }); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { lib } = useExchangeApiContext(); - const start = safeFunctionHandler( - i18n.str`start external kyc`, - async (id: string) => { + const start = actionHandler( + async (ct, id: string) => { return lib.exchange.startExternalKycProcess(id); }, [req.id!], ); - start.onFail = (fail) => { - setLoading({ state: LinkGenerationState.ERROR }); - 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`; - default: - assertUnreachable(fail.case); - } - }; + start.onFail = showError( + i18n.str`Failed to start the KYC process.`, + (fail) => { + setLoading({ state: LinkGenerationState.ERROR }); + 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`; + default: + assertUnreachable(fail.case); + } + }, + ); start.onSuccess = (success) => { setLoading({ state: LinkGenerationState.DONE, @@ -215,7 +221,6 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { const row = ( <Fragment> <div class="flex min-w-0 gap-x-4"> - <LocalNotificationBanner notification={notification} /> <div class="inline-block h-10 w-10 rounded-full"> {!redirectUrl ? ( <svg @@ -257,12 +262,12 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { </p> ) : ( <p class="text-sm font-semibold leading-6 text-gray-900"> - <ButtonBetter onClick={start} > + <Button onClick={start}> <span class="absolute inset-x-0 -top-px bottom-0"></span> <i18n.Translate context="KYC_REQUIREMENT_LINK_DESCRIPTION"> {req.description} </i18n.Translate> - </ButtonBetter> + </Button> </p> )} </div> diff --git a/packages/kyc-ui/src/pages/TriggerForms.tsx b/packages/kyc-ui/src/pages/TriggerForms.tsx @@ -16,12 +16,10 @@ import { FormMetadata, FormUI, - LocalNotificationBanner, preloadedForms, UIHandlerId, useFormMeta, - useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -35,7 +33,6 @@ type Props = { export function TriggerForms({ formId }: Props): VNode { const { i18n } = useTranslationContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const pf = preloadedForms(i18n); @@ -80,7 +77,6 @@ export function TriggerForms({ formId }: Props): VNode { : pf.find((f) => f.id === status.result.form); return ( <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> - <LocalNotificationBanner notification={notification} /> <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> <FormUI model={handler} design={design} /> </div> diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -30,13 +30,12 @@ import { WalletKycRequest, } from "@gnu-taler/taler-util"; import { - ButtonBetter, + Button, FormMetadata, FormUI, - LocalNotificationBanner, UIHandlerId, useExchangeApiContext, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -54,7 +53,7 @@ type Props = { export function TriggerKyc({ onKycStarted }: Props): VNode { const { i18n } = useTranslationContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { config, lib } = useExchangeApiContext(); const theForm: FormMetadata = { @@ -99,15 +98,15 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { return createNewWalletKycAccount(extraEntropy); }, [1]); - const send = safeFunctionHandler( - i18n.str`trigger kyc process`, - async (balance: AmountString) => { + // i18n.str`trigger kyc process`, + const send = actionHandler( + async (ct, balance: AmountString) => { const account = await accountPromise; const limit: WalletKycRequest = { balance, reserve_pub: account.id, reserve_sig: encodeCrock( - signWalletAccountSetup(account.signingKey, balance), + signWalletAccountSetup(account.__signingKey, balance), ), }; const resp = await lib.exchange.notifyKycBalanceLimit(limit); @@ -116,9 +115,9 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { } 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 { __signingKey } = await accountPromise; + const merchantPub = eddsaGetPublic(__signingKey); + const accountOwnerSig = encodeCrock(signKycAuth(__signingKey)); const statusRes = await lib.exchange.checkKycStatus({ accountPub: encodeCrock(merchantPub), accountSig: accountOwnerSig, @@ -140,7 +139,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { send.onSuccess = (success) => { onKycStarted(success.access_token); }; - send.onFail = (fail) => { + send.onFail = showError(i18n.str`Failed to trigger a KYC event.`, (fail) => { switch (fail.case) { case HttpStatusCode.NoContent: return i18n.str`No kyc configured.`; @@ -153,11 +152,10 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> - <LocalNotificationBanner notification={notification} /> <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> <FormUI model={handler} design={design} /> </div> @@ -170,12 +168,12 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { > <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetter + <Button 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> - </ButtonBetter> + </Button> </div> <div class="grid grid-cols-1 gap-x-8 gap-y-4 "> @@ -187,130 +185,130 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { </i18n.Translate> </p> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> <div> - <ButtonBetter + <Button onClick={send.withArgs(`${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> - </ButtonBetter> + </Button> </div> </div> </div>