taler-typescript-core

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

commit 2bea1081cd7ee9a5d8195069d1857eede7f4bbb8
parent 617dd2bc3efbe851bd7c009ce9b8488236fa910b
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 24 Oct 2025 19:25:48 -0300

challenger

Diffstat:
Mpackages/challenger-ui/src/pages/AnswerChallenge.tsx | 155++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/challenger-ui/src/pages/AskChallenge.tsx | 89++++++++++++++++++++++++++++++++++---------------------------------------------
Mpackages/challenger-ui/src/pages/Setup.tsx | 68+++++++++++++++++++++++++++++++-------------------------------------
3 files changed, 144 insertions(+), 168 deletions(-)

diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -23,13 +23,13 @@ import { } from "@gnu-taler/taler-util"; import { Attention, - Button, + ButtonBetter, LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, Time, useChallengerApiContext, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -96,7 +96,7 @@ export function AnswerChallenge({ const { config, lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); const { sent, failed, completed } = useSessionState(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [pin, setPin] = useState<string | undefined>(); const errors = undefinedIfEmpty({ pin: !pin ? i18n.str`Can't be empty` : undefined, @@ -117,7 +117,7 @@ export function AnswerChallenge({ ? AbsoluteTime.never() : AbsoluteTime.fromProtocolTimestamp(deadlineTS); }, [deadlineTS?.t_s]); - + useReloadOnDeadline(deadline); const lastAddr = !lastStatus?.last_address @@ -130,79 +130,76 @@ export function AnswerChallenge({ const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1; const contact = lastStatus?.last_address; - const onSendAgain = + const sendAgain = safeFunctionHandler( + lib.challenger.challenge, contact === undefined || - lastStatus == undefined || - lastStatus.pin_transmissions_left === 0 || - !AbsoluteTime.isExpired(deadline) + lastStatus === undefined || + lastStatus.pin_transmissions_left === 0 || + !AbsoluteTime.isExpired(deadline) ? undefined - : withErrorHandler( - async () => { - return await lib.challenger.challenge(session.nonce, contact); - }, - (ok) => { - if (ok.body.type === "completed") { - completed(ok.body); - } else { - sent(ok.body); - } - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The request was not accepted, try reloading the app.`; - case HttpStatusCode.NotFound: - return i18n.str`Challenge not found.`; - case HttpStatusCode.NotAcceptable: - return i18n.str`Server templates are missing due to misconfiguration.`; - case HttpStatusCode.TooManyRequests: - return i18n.str`There have been too many attempts to request challenge transmissions.`; - case HttpStatusCode.InternalServerError: - return i18n.str`Server is unable to respond due to internal problems.`; - } - }, - ); + : [session.nonce, contact], + ); + sendAgain.onSuccess = (sucess) => { + if (sucess.body.type === "completed") { + completed(sucess.body); + } else { + sent(sucess.body); + } + }; + sendAgain.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The request was not accepted, try reloading the app.`; + case HttpStatusCode.NotFound: + return i18n.str`Challenge not found.`; + case HttpStatusCode.NotAcceptable: + return i18n.str`Server templates are missing due to misconfiguration.`; + case HttpStatusCode.TooManyRequests: + return i18n.str`There have been too many attempts to request challenge transmissions.`; + case HttpStatusCode.InternalServerError: + return i18n.str`Server is unable to respond due to internal problems.`; + } + }; - const onCheck = + const check = safeFunctionHandler( + lib.challenger.solve, errors !== undefined || - lastStatus == undefined || - lastStatus.auth_attempts_left === 0 + lastStatus == undefined || + lastStatus.auth_attempts_left === 0 || + !pin ? undefined - : withErrorHandler( - async () => { - return lib.challenger.solve(session.nonce, { pin: pin! }); - }, - (ok) => { - if (ok.body.type === "completed") { - completed(ok.body); - } else { - failed(ok.body); - } - onComplete(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The request was not accepted, try reloading the app.`; - case HttpStatusCode.Forbidden: { - revalidateChallengeSession(); - return i18n.str`Invalid pin.`; - } - case HttpStatusCode.NotFound: - return i18n.str`Challenge not found.`; - case HttpStatusCode.NotAcceptable: - return i18n.str`Server templates are missing due to misconfiguration.`; - case HttpStatusCode.TooManyRequests: { - revalidateChallengeSession(); - return i18n.str`There have been too many attempts to request challenge transmissions.`; - } - case HttpStatusCode.InternalServerError: - return i18n.str`Server is unable to respond due to internal problems.`; - default: - assertUnreachable(fail); - } - }, - ); + : [session.nonce, { pin }], + ); + check.onSuccess = (success) => { + if (success.body.type === "completed") { + completed(success.body); + } else { + failed(success.body); + } + onComplete(); + }; + check.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The request was not accepted, try reloading the app.`; + case HttpStatusCode.Forbidden: { + revalidateChallengeSession(); + return i18n.str`Invalid pin.`; + } + case HttpStatusCode.NotFound: + return i18n.str`Challenge not found.`; + case HttpStatusCode.NotAcceptable: + return i18n.str`Server templates are missing due to misconfiguration.`; + case HttpStatusCode.TooManyRequests: { + revalidateChallengeSession(); + return i18n.str`There have been too many attempts to request challenge transmissions.`; + } + case HttpStatusCode.InternalServerError: + return i18n.str`Server is unable to respond due to internal problems.`; + default: + assertUnreachable(fail); + } + }; const cantTryAnymore = lastStatus?.auth_attempts_left === 0; function LastContactSent(): VNode { @@ -259,14 +256,13 @@ export function AnswerChallenge({ )} </div> <div> - <Button + <ButtonBetter type="submit" - disabled={!onSendAgain} class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center 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" - handler={onSendAgain} + onClick={sendAgain} > <i18n.Translate>Send new code</i18n.Translate> - </Button> + </ButtonBetter> {lastStatus === undefined ? undefined : ( <p class="mt-2 text-sm leading-6 text-gray-400"> {lastStatus.pin_transmissions_left < 1 ? ( @@ -381,14 +377,13 @@ export function AnswerChallenge({ </div> <div class="mt-10"> - <Button + <ButtonBetter type="submit" class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center 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" - disabled={!onCheck} - handler={onCheck} + onClick={check} > <i18n.Translate>Check</i18n.Translate> - </Button> + </ButtonBetter> </div> </form> diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -25,7 +25,7 @@ import { } from "@gnu-taler/taler-util"; import { Attention, - Button, + ButtonBetter, countryNameList, ErrorLoading, FormDesign, @@ -34,16 +34,14 @@ import { RouteDefinition, useChallengerApiContext, useForm, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useChallengeSession } from "../hooks/challenge.js"; import { SessionId, useSessionState } from "../hooks/session.js"; -import { - getAddressDescriptionFromAddrType -} from "./AnswerChallenge.js"; +import { getAddressDescriptionFromAddrType } from "./AnswerChallenge.js"; type Props = { onSendSuccesful: () => void; @@ -62,7 +60,7 @@ export function AskChallenge({ const { lib, config } = useChallengerApiContext(); const { i18n } = useTranslationContext(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); // const [address, setEmail] = useState<string | undefined>(); const [addrIndex, setAddrIndex] = useState<number | undefined>(); @@ -161,43 +159,34 @@ export function AskChallenge({ ? undefined : (form.status.result as Record<string, string>); - const onSend = - form.status.errors || !contact - ? undefined - : withErrorHandler( - async () => { - const info = lastStatus.fix_address - ? lastStatus.last_address! - : contact; + const info = lastStatus.fix_address ? lastStatus.last_address! : contact; - return lib.challenger.challenge(session.nonce, info); - }, - (ok) => { - if (ok.body.type === "completed") { - completed(ok.body); - } else { - // if (remember) { - // saveAddress(config.address_type, contact); - // } - sent(ok.body); - } - onSendSuccesful(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The request was not accepted, try reloading the app.`; - case HttpStatusCode.NotFound: - return i18n.str`Challenge not found.`; - case HttpStatusCode.NotAcceptable: - return i18n.str`Server templates are missing due to misconfiguration.`; - case HttpStatusCode.TooManyRequests: - return i18n.str`There have been too many attempts to request challenge transmissions.`; - case HttpStatusCode.InternalServerError: - return i18n.str`Server is unable to respond due to internal problems.`; - } - }, - ); + const send = safeFunctionHandler( + lib.challenger.challenge, + form.status.errors || !info ? undefined : [session.nonce, info], + ); + send.onSuccess = (ok) => { + if (ok.body.type === "completed") { + completed(ok.body); + } else { + sent(ok.body); + } + onSendSuccesful(); + }; + send.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The request was not accepted, try reloading the app.`; + case HttpStatusCode.NotFound: + return i18n.str`Challenge not found.`; + case HttpStatusCode.NotAcceptable: + return i18n.str`Server templates are missing due to misconfiguration.`; + case HttpStatusCode.TooManyRequests: + return i18n.str`There have been too many attempts to request challenge transmissions.`; + case HttpStatusCode.InternalServerError: + return i18n.str`Server is unable to respond due to internal problems.`; + } + }; return ( <Fragment> @@ -383,7 +372,7 @@ export function AskChallenge({ <FormUI design={design} model={form.model} - onSubmit={onSend?.onClick} + onSubmit={send.call} /> </div> @@ -439,11 +428,10 @@ export function AskChallenge({ <div class="mx-auto mt-4 max-w-xl "> {!prevAddr ? ( <div class="mt-10"> - <Button + <ButtonBetter type="submit" - disabled={!onSend} class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center 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" - handler={onSend} + onClick={send} > {(function (): TranslatedString { switch (config.address_type) { @@ -456,15 +444,14 @@ export function AskChallenge({ return i18n.str`Send SMS`; } })()} - </Button> + </ButtonBetter> </div> ) : ( <div class="mt-10"> - <Button + <ButtonBetter type="submit" - disabled={!onSend} class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center 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" - handler={onSend} + onClick={send} > {(function (): TranslatedString { switch (config.address_type) { @@ -483,7 +470,7 @@ export function AskChallenge({ : i18n.str`Change phone`; } })()} - </Button> + </ButtonBetter> </div> )} </div> diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx @@ -15,17 +15,17 @@ */ import { HttpStatusCode, - createClientSecretAccessToken, createRFC8959AccessTokenEncoded, encodeCrock, randomBytes, } from "@gnu-taler/taler-util"; import { Button, + ButtonBetter, LocalNotificationBanner, ShowInputErrorLabel, useChallengerApiContext, - useLocalNotificationHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -33,6 +33,7 @@ import { useState } from "preact/hooks"; import { safeToURL } from "../Routing.js"; import { useSessionState } from "../hooks/session.js"; import { doAutoFocus, undefinedIfEmpty } from "./AnswerChallenge.js"; +import { AccessToken } from "@gnu-taler/taler-util"; type Props = { clientId: string; @@ -49,51 +50,45 @@ export function Setup({ onCreated, }: Props): VNode { const { i18n } = useTranslationContext(); - const [notification, withErrorHandler] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { lib } = useChallengerApiContext(); const { start } = useSessionState(); const [password, setPassword] = useState<string | undefined>(secret); const [url, setUrl] = useState<string | undefined>(redirectURL?.href); const errors = undefinedIfEmpty({ - password: !password ? i18n.str`required` : undefined, + password: !password ? i18n.str`Required` : undefined, url: !url - ? i18n.str`required` + ? i18n.str`Required` : !safeToURL(url) - ? i18n.str`invalid format` + ? i18n.str`Invalid format` : undefined, }); - const onStart = + const doStart = safeFunctionHandler( + (token: AccessToken, url: string) => lib.challenger.setup(clientId, token), !!errors || password === undefined || url === undefined ? undefined - : withErrorHandler( - async () => { - return lib.challenger.setup( - clientId, - createRFC8959AccessTokenEncoded(password), - ); - }, - (ok) => { - start(); - - const redirect = new URL(window.location.href) - redirect.searchParams.set("client_id",clientId) - redirect.searchParams.set("redirect_uri",url) - redirect.searchParams.set("state",encodeCrock(randomBytes(32))) - redirect.searchParams.set("nonce",ok.body.nonce) - redirect.hash = "" - window.location.href = redirect.href + : [createRFC8959AccessTokenEncoded(password), url], + ); + doStart.onSuccess = (ok, token, redirect_uri) => { + start(); + const redirect = new URL(window.location.href); + redirect.searchParams.set("client_id", clientId); + redirect.searchParams.set("redirect_uri", redirect_uri); + redirect.searchParams.set("state", encodeCrock(randomBytes(32))); + redirect.searchParams.set("nonce", ok.body.nonce); + redirect.hash = ""; + window.location.href = redirect.href; + onCreated(); + }; - onCreated(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.NotFound: - return i18n.str`Client doesn't exist.`; - } - }, - ); + doStart.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.NotFound: + return i18n.str`Client doesn't exist.`; + } + }; return ( <Fragment> @@ -173,14 +168,13 @@ export function Setup({ </div> </form> <div class="mt-10"> - <Button + <ButtonBetter type="submit" - disabled={!onStart} class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center 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" - handler={onStart} + onClick={doStart} > <i18n.Translate>Start</i18n.Translate> - </Button> + </ButtonBetter> </div> </div> </Fragment>