commit 2bea1081cd7ee9a5d8195069d1857eede7f4bbb8
parent 617dd2bc3efbe851bd7c009ce9b8488236fa910b
Author: Sebastian <sebasjm@gmail.com>
Date: Fri, 24 Oct 2025 19:25:48 -0300
challenger
Diffstat:
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>