commit 8803e1ca647bf6f05388c58df2906e5768bb58fb
parent c4b3d0f95285211fad7a0d72c2ef4992c1817f65
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 1 Jun 2026 11:14:57 -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:
7 files changed, 151 insertions(+), 164 deletions(-)
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-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 { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
import { SessionId } from "./hooks/session.js";
import { AnswerChallenge } from "./pages/AnswerChallenge.js";
@@ -93,9 +92,6 @@ function PublicRounting(): VNode {
const loc = useCurrentLocation(publicPages);
const { i18n } = useTranslationContext();
const { navigateTo } = useNavigationContext();
- useErrorBoundary((e) => {
- console.log("error", e);
- });
const location: typeof loc =
loc.name === undefined
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
@@ -26,6 +26,7 @@ import {
BrowserHashNavigationProvider,
ChallengerApiProvider,
Loading,
+ NotificationProvider,
TalerWalletIntegrationBrowserProvider,
TranslationProvider,
} from "@gnu-taler/web-util/browser";
@@ -70,47 +71,49 @@ export function App(): VNode {
return (
<SettingsProvider value={settings}>
<TranslationProvider source={strings}>
- <ChallengerApiProvider
- baseUrl={new URL("/", baseUrl)}
- frameOnError={Frame}
- evictors={{
- challenger: evictBankSwrCache,
- }}
- >
- <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>
+ <ChallengerApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={Frame}
+ evictors={{
+ challenger: evictBankSwrCache,
}}
>
- <TalerWalletIntegrationBrowserProvider>
- <BrowserHashNavigationProvider>
- <Routing />
- </BrowserHashNavigationProvider>
- </TalerWalletIntegrationBrowserProvider>
- </SWRConfig>
- </ChallengerApiProvider>
+ <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>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </ChallengerApiProvider>
+ </NotificationProvider>
</TranslationProvider>
</SettingsProvider>
);
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -20,6 +20,7 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
+ ErrorLoading,
Loading,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -50,7 +51,7 @@ export function CheckChallengeIsUpToDate({
return <Loading />;
}
if (result instanceof TalerError) {
- return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
+ return <ErrorLoading title={i18n.str`Failed to load the session.`} error={result} />;
}
if (result.type === "fail") {
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -15,32 +15,31 @@
*/
import {
AbsoluteTime,
+ ChallengerApi,
EmptyObject,
HttpStatusCode,
TalerError,
- ChallengerApi,
+ TalerFormAttributes,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
Attention,
- ButtonBetter,
- LocalNotificationBanner,
+ Button,
RouteDefinition,
ShowInputErrorLabel,
Time,
useChallengerApiContext,
- useLocalNotificationBetter,
+ useNotificationContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
+import { useMemo } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import {
revalidateChallengeSession,
useChallengeSession,
} from "../hooks/challenge.js";
import { SessionId, useSessionState } from "../hooks/session.js";
-import { TalerFormAttributes } from "@gnu-taler/taler-util";
-import { useMemo } from "preact/compat";
type Props = {
focus?: boolean;
@@ -96,7 +95,8 @@ export function AnswerChallenge({
const { config, lib } = useChallengerApiContext();
const { i18n } = useTranslationContext();
const { sent, failed, completed } = useSessionState();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
+
const [pin, setPin] = useState<string | undefined>();
const errors = undefinedIfEmpty({
pin: !pin ? i18n.str`Can't be empty` : undefined,
@@ -130,15 +130,15 @@ export function AnswerChallenge({
const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1;
const contact = lastStatus?.last_address;
- const sendAgain = safeFunctionHandler(
- i18n.str`create challenge`,
- lib.challenger.challenge.bind(lib.challenger),
+ // i18n.str`create challenge`,
+ const sendAgain = actionHandler(
+ (ct, n, b) => lib.challenger.challenge(n, b),
contact === undefined ||
lastStatus === undefined ||
lastStatus.pin_transmissions_left === 0 ||
!AbsoluteTime.isExpired(deadline)
? undefined
- : [session.nonce, contact],
+ : ([session.nonce, contact] as const),
);
sendAgain.onSuccess = (success) => {
if (success.type === "completed") {
@@ -147,32 +147,35 @@ export function AnswerChallenge({
sent(success);
}
};
- 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.`;
- default:
- assertUnreachable(fail);
- }
- };
+ sendAgain.onFail = showError(
+ i18n.str`Failed to create a new challenge.`,
+ (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.`;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
- const check = safeFunctionHandler(
- i18n.str`solve challenge`,
- lib.challenger.solve.bind(lib.challenger),
+ // i18n.str`solve challenge`,
+ const check = actionHandler(
+ (ct, n, b) => lib.challenger.solve(n, b),
errors !== undefined ||
lastStatus == undefined ||
lastStatus.auth_attempts_left === 0 ||
!pin
? undefined
- : [session.nonce, { pin }],
+ : ([session.nonce, { pin }] as const),
);
check.onSuccess = (success) => {
if (success.type === "completed") {
@@ -182,28 +185,31 @@ export function AnswerChallenge({
}
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.`;
+ check.onFail = showError(
+ i18n.str`Failed to solve the challenge.`,
+ (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);
}
- 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 {
@@ -260,13 +266,13 @@ export function AnswerChallenge({
)}
</div>
<div>
- <ButtonBetter
+ <Button
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"
onClick={sendAgain}
>
<i18n.Translate>Send new code</i18n.Translate>
- </ButtonBetter>
+ </Button>
{lastStatus === undefined ? undefined : (
<p class="mt-2 text-sm leading-6 text-gray-400">
{lastStatus.pin_transmissions_left < 1 ? (
@@ -293,7 +299,6 @@ export function AnswerChallenge({
if (cantTryAnymore) {
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">
@@ -311,8 +316,6 @@ export function AnswerChallenge({
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">
@@ -381,13 +384,13 @@ export function AnswerChallenge({
</div>
<div class="mt-10">
- <ButtonBetter
+ <Button
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"
onClick={check}
>
<i18n.Translate>Check</i18n.Translate>
- </ButtonBetter>
+ </Button>
</div>
</form>
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -25,17 +25,16 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- ButtonBetter,
+ Button,
countryNameList,
ErrorLoading,
FormDesign,
FormUI,
- LocalNotificationBanner,
RouteDefinition,
useChallengerApiContext,
useForm,
- useLocalNotificationBetter,
- useTranslationContext,
+ useNotificationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -60,7 +59,7 @@ export function AskChallenge({
const { lib, config } = useChallengerApiContext();
const { i18n } = useTranslationContext();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+ const { actionHandler, showError } = useNotificationContext();
// const [address, setEmail] = useState<string | undefined>();
const [addrIndex, setAddrIndex] = useState<number | undefined>();
@@ -74,7 +73,12 @@ export function AskChallenge({
);
}
if (result instanceof TalerError) {
- return <ErrorLoading error={result} />;
+ return (
+ <ErrorLoading
+ title={i18n.str`Failed to load the session.`}
+ error={result}
+ />
+ );
}
if (result.type === "fail") {
switch (result.case) {
@@ -161,10 +165,10 @@ export function AskChallenge({
const info = lastStatus.fix_address ? lastStatus.last_address! : contact;
- const send = safeFunctionHandler(
- i18n.str`create challenge`,
- lib.challenger.challenge.bind(lib.challenger),
- form.status.errors || !info ? undefined : [session.nonce, info],
+ // i18n.str`create challenge`,
+ const send = actionHandler(
+ (ct, n, i) => lib.challenger.challenge(n, i),
+ form.status.errors || !info ? undefined : ([session.nonce, info] as const),
);
send.onSuccess = (ok) => {
if (ok.type === "completed") {
@@ -174,7 +178,7 @@ export function AskChallenge({
}
onSendSuccesful();
};
- send.onFail = (fail) => {
+ send.onFail = showError(i18n.str`Failed to create a challenge.`, (fail) => {
switch (fail.case) {
case HttpStatusCode.BadRequest:
return i18n.str`The request was not accepted, try reloading the app.`;
@@ -189,12 +193,10 @@ export function AskChallenge({
default:
assertUnreachable(fail);
}
- };
+ });
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">
@@ -427,7 +429,7 @@ export function AskChallenge({
<div class="mx-auto mt-4 max-w-xl ">
{!prevAddr ? (
<div class="mt-10">
- <ButtonBetter
+ <Button
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"
onClick={send}
@@ -443,11 +445,11 @@ export function AskChallenge({
return i18n.str`Send SMS`;
}
})()}
- </ButtonBetter>
+ </Button>
</div>
) : (
<div class="mt-10">
- <ButtonBetter
+ <Button
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"
onClick={send}
@@ -469,7 +471,7 @@ export function AskChallenge({
: i18n.str`Change phone`;
}
})()}
- </ButtonBetter>
+ </Button>
</div>
)}
</div>
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
@@ -14,48 +14,32 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TranslatedString } from "@gnu-taler/taler-util";
import {
Footer,
Header,
ToastBanner,
- notifyError,
- notifyException,
+ useRenderErrorReport,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
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;
export function Frame({ children }: { children: ComponentChildren }): VNode {
- const settings = useSettingsContext();
const [preferences, updatePreferences] = usePreferences();
- 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__,
+ });
return (
<div
diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx
@@ -22,12 +22,11 @@ import {
randomBytes,
} from "@gnu-taler/taler-util";
import {
- ButtonBetter,
- LocalNotificationBanner,
+ Button,
ShowInputErrorLabel,
useChallengerApiContext,
- useLocalNotificationBetter,
- useTranslationContext
+ useNotificationContext,
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -42,6 +41,7 @@ type Props = {
onCreated: () => void;
focus?: boolean;
};
+
export function Setup({
clientId,
secret,
@@ -50,8 +50,8 @@ export function Setup({
onCreated,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const [notification, safeFunctionHandler] = useLocalNotificationBetter();
const { lib } = useChallengerApiContext();
+ const { actionHandler, showError } = useNotificationContext();
const { start } = useSessionState();
const [password, setPassword] = useState<string | undefined>(secret);
const [url, setUrl] = useState<string | undefined>(redirectURL?.href);
@@ -65,13 +65,13 @@ export function Setup({
: undefined,
});
- const doStart = safeFunctionHandler(
- i18n.str`setup challenge`,
- (token: AccessToken, url) => lib.challenger.setup(clientId, token),
+ const doStart = actionHandler(
+ (ct, token: AccessToken, url) => lib.challenger.setup(clientId, token),
!!errors || password === undefined || url === undefined
? undefined
: [createRFC8959AccessTokenEncoded(password), url],
);
+
doStart.onSuccess = (ok, token, redirect_uri) => {
start();
const redirect = new URL(window.location.href);
@@ -84,19 +84,17 @@ export function Setup({
onCreated();
};
- doStart.onFail = (fail) => {
+ doStart.onFail = showError(i18n.str`Failed to setup a new challenge.`, (fail) => {
switch (fail.case) {
case HttpStatusCode.NotFound:
return i18n.str`The server doesn't know about this client. Either the URL or the secret is wrong.`;
default:
assertUnreachable(fail.case);
}
- };
+ });
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">
@@ -169,16 +167,16 @@ export function Setup({
/>
</div>
</div>
+ <div class="mt-10">
+ <Button
+ 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"
+ onClick={doStart}
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </Button>
+ </div>
</form>
- <div class="mt-10">
- <ButtonBetter
- 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"
- onClick={doStart}
- >
- <i18n.Translate>Start</i18n.Translate>
- </ButtonBetter>
- </div>
</div>
</Fragment>
);