commit e157ed6e2b8c6fcbe1679dd5d173f606c41d31c7 parent 9db74cdff9f76331db3bf38e047a9ac790606945 Author: Sebastian <sebasjm@taler-systems.com> Date: Sun, 7 Jun 2026 12:17:12 -0300 fix #11487 Diffstat:
43 files changed, 412 insertions(+), 307 deletions(-)
diff --git a/packages/taler-exchange-kyc-webui/dev.mjs b/packages/taler-exchange-kyc-webui/dev.mjs @@ -28,7 +28,7 @@ const build = initializeDev({ assets: [{ base: "src", files: [ - "src/index.html","src/forms.json", + "src/index.html","src/forms.json", "src/settings.json", ] }], }, @@ -44,6 +44,4 @@ serve({ port: 8080, source: "./src", onSourceUpdate: build, - appSamplePath: "/kyc-spa/ZBNB5AS4F4MARC983KZ64EMHHNWGF9GDD4J0CA4EPCVERCEK64S0", - appPath: "/kyc-spa/:token", }); diff --git a/packages/taler-exchange-kyc-webui/src/Routing.tsx b/packages/taler-exchange-kyc-webui/src/Routing.tsx @@ -22,7 +22,7 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { assertUnreachable } from "@gnu-taler/taler-util"; +import { AccessToken, assertUnreachable } from "@gnu-taler/taler-util"; import { useSessionState } from "./hooks/session.js"; import { ChallengeCompleted } from "./pages/ChallengeCompleted.js"; import { Frame } from "./pages/Frame.js"; @@ -51,6 +51,10 @@ export function Routing(): VNode { const publicPages = { completed: urlPattern(/\/completed/, () => `#/completed`), start: urlPattern(/\/start/, () => `#/start`), + token: urlPattern<{ t: string }>( + /\/token\/(?<t>[a-zA-Z0-9-_]+)/, + ({ t }) => `#/token/${t}`, + ), triggerKyc: urlPattern(/\/test\/trigger-kyc/, () => `#/test/trigger-kyc`), triggerForm: urlPattern( /\/test\/show-forms\/(?<fid>[a-zA-Z0-9-_]+)/, @@ -72,6 +76,7 @@ function PublicRounting(): VNode { const { state, start } = useSessionState(); const { navigateTo } = useNavigationContext(); const currentToken = state?.accessToken; + console.log("sasas", location, state); switch (location.name) { case undefined: { navigateTo(publicPages.start.url({})); @@ -84,6 +89,13 @@ function PublicRounting(): VNode { return <Start token={currentToken} />; } + case "token": { + return ( + <Fragment> + <Start token={location.values.t as AccessToken} /> + </Fragment> + ); + } case "completed": { return <ChallengeCompleted />; } diff --git a/packages/taler-exchange-kyc-webui/src/hooks/kyc.ts b/packages/taler-exchange-kyc-webui/src/hooks/kyc.ts @@ -49,7 +49,6 @@ export function useKycInfo(token?: AccessToken) { }, [token], ); - const result = useLongPolling( prev, (result) => { diff --git a/packages/taler-exchange-kyc-webui/src/hooks/session.ts b/packages/taler-exchange-kyc-webui/src/hooks/session.ts @@ -21,6 +21,7 @@ import { codecForAccessToken, codecForList, codecForString, + codecOptional, codecOptionalDefault, } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; @@ -29,7 +30,7 @@ import { codecForNumber } from "@gnu-taler/taler-util"; import { useEffect } from "preact/compat"; export type SessionState = { - accessToken: AccessToken; + accessToken?: AccessToken; testAccounts: PrivPub[]; time: number; }; @@ -41,7 +42,7 @@ type PrivPub = { export const codecForSessionState = (): Codec<SessionState> => buildCodecForObject<SessionState>() - .property("accessToken", codecForAccessToken()) + .property("accessToken", codecOptional(codecForAccessToken())) .property("time", codecOptionalDefault(codecForNumber(), 0)) .property( "testAccounts", diff --git a/packages/taler-exchange-kyc-webui/src/pages/Start.tsx b/packages/taler-exchange-kyc-webui/src/pages/Start.tsx @@ -27,7 +27,7 @@ import { Loading, useExchangeApiContext, useNotificationContext, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -47,7 +47,6 @@ function ShowReqList({ }): VNode { const { i18n } = useTranslationContext(); const result = useKycInfo(token); - if (!result) { return <Loading />; } @@ -182,7 +181,6 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { const { actionHandler, showError } = useNotificationContext(); const { lib } = useExchangeApiContext(); - const start = actionHandler( async (ct, id: string) => { return lib.exchange.startExternalKycProcess(id); @@ -210,7 +208,7 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { state: LinkGenerationState.DONE, url: success.redirect_url, }); - return i18n.str`Link generated, you can proceed.`; + return undefined; }; useEffect(() => { start.call(); @@ -301,12 +299,12 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { ); if (redirectUrl) { return ( - <li key={req.id} class="hover:bg-gray-50 sm:px-6 px-4 py-5"> + <li key={req.id} class="hover:bg-gray-50"> <a href={redirectUrl} target="_blank" rel="noreferrer" - class="relative flex justify-between gap-x-6 " + class="relative flex justify-between gap-x-6 px-4 py-5" > {row} </a> diff --git a/packages/taler-merchant-webui/src/Application.tsx b/packages/taler-merchant-webui/src/Application.tsx @@ -21,6 +21,7 @@ import { CacheEvictor, + MerchantVersionResponse, TalerMerchantInstanceCacheEviction, TalerMerchantManagementCacheEviction, assertUnreachable, @@ -28,6 +29,7 @@ import { } from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, + ConfigResult, MerchantApiProvider, NotificationCardBulma, NotificationProvider, @@ -220,19 +222,16 @@ function localStorageProvider(): Map<unknown, unknown> { return map; } -function OnConfigError({ children }: { children: ComponentChildren }): VNode { +function OnConfigError({ + status, + children, +}: { + status: ConfigResult<MerchantVersionResponse>; + children: ComponentChildren; +}): VNode { const { i18n } = useTranslationContext(); - return ( - <div> - <NotificationCardBulma - notification={{ - message: i18n.str`Contacting the server failed`, - type: "ERROR", - description: <Fragment>{children}</Fragment>, - }} - /> - </div> - ); + if (status === undefined) return <Loading />; + return <Fragment>{children}</Fragment>; } const swrCacheEvictor = new (class implements CacheEvictor< diff --git a/packages/taler-merchant-webui/src/Routing.tsx b/packages/taler-merchant-webui/src/Routing.tsx @@ -246,6 +246,7 @@ export function Routing(_p: Props): VNode { <Route path={InstancePaths.resetAccount} component={ResetAccount} + focus onCancel={() => route(InstancePaths.order_list)} onReseted={() => route(InstancePaths.order_list)} /> diff --git a/packages/taler-merchant-webui/src/components/SolveMFA.tsx b/packages/taler-merchant-webui/src/components/SolveMFA.tsx @@ -31,7 +31,7 @@ import { } from "../hooks/preference.js"; import { FormErrors, FormProvider } from "./form/FormProvider.js"; import { InputCode } from "./form/InputCode.js"; -import { ConfirmModal, SimpleModal } from "./modal/index.js"; +import { BlockingModal, ConfirmModal, SimpleModal } from "./modal/index.js"; import { Loading, Spinner } from "./exception/loading.js"; const TALER_SCREEN_ID = 5; @@ -280,11 +280,17 @@ function SolveChallenge({ return ( <ConfirmModal - label={i18n.str`Verify`} - description={i18n.str`Validation code sent.`} - active + title={i18n.str`Validation code sent.`} onCancel={onCancel} - confirm={!tries.numTries ? undefined : verify} // first try is going to be automatically + preventBackgroundClose + confirm={ + !tries.numTries + ? undefined + : { + handler: verify, + label: i18n.str`Verify`, + } + } // first try is going to be automatically > <FormProvider<Form> name="settings" @@ -375,16 +381,11 @@ export function SolveChallengeDialog({}: {}): VNode { if (!mfa.pending) return <Fragment />; if (mfa.pending.loadingFirstChallenge) { return ( - <ConfirmModal - active - noCancelButton - description={i18n.str`Loading first challenge...`} - label={"asd" as TranslatedString} - > + <BlockingModal title={i18n.str`Loading first challenge...`}> <div class="columns is-centered is-vcentered"> <Spinner /> </div> - </ConfirmModal> + </BlockingModal> ); } return <SolveMFAChallenges state={mfa.pending} focus onCancel={mfa.cancel} />; @@ -490,16 +491,11 @@ function SolveMFAChallenges({ if (loadingNext) { return ( - <ConfirmModal - active - noCancelButton - description={i18n.str`Loading next challenge...`} - label={"asd" as TranslatedString} - > + <BlockingModal title={i18n.str`Loading next challenge...`}> <div class="columns is-centered is-vcentered"> <Spinner /> </div> - </ConfirmModal> + </BlockingModal> ); } if (active) { @@ -529,9 +525,9 @@ function SolveMFAChallenges({ return pending; }); if (nextPending) { - setLoadingNext(true) + setLoadingNext(true); await sendMessage.withArgs(nextPending).call(); - setLoadingNext(false) + setLoadingNext(false); } else { setActive(undefined); } @@ -547,11 +543,21 @@ function SolveMFAChallenges({ return ( <ConfirmModal - label={!enough ? i18n.str`Continue` : i18n.str`Complete`} - description={i18n.str`Multi-factor authentication required.`} - active + title={i18n.str`Multi-factor authentication required.`} + focus + preventBackgroundClose onCancel={onCancel} - confirm={!enough ? doSend : retry.withArgs(solved)} + confirm={ + !enough + ? { + handler: doSend, + label: i18n.str`Continue`, + } + : { + handler: retry.withArgs(solved), + label: i18n.str`Complete`, + } + } > <div> {requirement.combi_and ? ( diff --git a/packages/taler-merchant-webui/src/components/form/InputCurrency.tsx b/packages/taler-merchant-webui/src/components/form/InputCurrency.tsx @@ -51,7 +51,6 @@ export function InputCurrency<T>({ const { value } = useField<T>(name); const parsedValue = !value ? undefined : Amounts.parse(value); - console.log("intpu currency", currency); // if the field already has a value, use the // currency of that value // fallback to the selected currency on the session diff --git a/packages/taler-merchant-webui/src/components/form/InputPassword.tsx b/packages/taler-merchant-webui/src/components/form/InputPassword.tsx @@ -39,6 +39,7 @@ export function InputPassword<T>({ help, tooltip, expand, + focus, autoComplete, children, side, @@ -49,6 +50,7 @@ export function InputPassword<T>({ name={name} readonly={readonly} side={side} + focus={focus} label={label} placeholder={placeholder} autoComplete={autoComplete} diff --git a/packages/taler-merchant-webui/src/components/form/InputWithAddon.tsx b/packages/taler-merchant-webui/src/components/form/InputWithAddon.tsx @@ -39,7 +39,6 @@ export interface Props<T> extends InputProps<T> { inputExtra?: any; children?: ComponentChildren; side?: ComponentChildren; - focus?: boolean; autoComplete?: SupportedAutocomplete; } @@ -101,9 +100,7 @@ export function InputWithAddon<T>({ </div> )} <p - class={`control${expand ? " is-expanded" : ""}${ - required ? " has-icons-right" : "" - }`} + class={`control${expand ? " is-expanded" : ""}`} > <fieldset> <InternalTextInputSwitch diff --git a/packages/taler-merchant-webui/src/components/form/useField.tsx b/packages/taler-merchant-webui/src/components/form/useField.tsx @@ -87,6 +87,7 @@ export interface InputProps<T> { name: T; label: ComponentChildren; placeholder?: string; + focus?: boolean; tooltip?: TranslatedString; readonly?: boolean; help?: ComponentChildren; diff --git a/packages/taler-merchant-webui/src/components/modal/index.tsx b/packages/taler-merchant-webui/src/components/modal/index.tsx @@ -43,45 +43,56 @@ import { doAutoFocus } from "../form/Input.js"; const TALER_SCREEN_ID = 18; -interface Props { - active?: boolean; - description?: TranslatedString; +interface ConfirmProps { + title: TranslatedString; onCancel?: () => void; - confirm?: SafeHandler<any, any>; - label: TranslatedString; + confirm?: { + label: TranslatedString; + handler: SafeHandler<any, any>; + }; + children?: ComponentChildren; + danger?: boolean; + focus?: boolean; + preventBackgroundClose?: boolean; +} +interface BlockingProps { + title?: TranslatedString; + confirm?: { + label: TranslatedString; + handler: SafeHandler<any, any>; + }; children?: ComponentChildren; danger?: boolean; focus?: boolean; - /** - * sometimes we want to prevent the user to close the dialog by error when clicking outside the box - * - * This could have been implemented as a separated component also - */ - noCancelButton?: boolean; } +/** + * Modal used for in-context confirmation. + * + * @param param0 + * @returns + */ export function ConfirmModal({ - active, - description, + title, onCancel, confirm, children, danger, focus, - label, - noCancelButton, -}: Props): VNode { + preventBackgroundClose, +}: ConfirmProps): VNode { const { i18n } = useTranslationContext(); return ( - <div class={active ? "modal is-active" : "modal"}> - <div class="modal-background " onClick={onCancel} /> + <div class="modal is-active"> + <div + class="modal-background " + onClick={preventBackgroundClose ? undefined : onCancel} + /> <div class="modal-card" style={{ maxWidth: 700 }}> <header class="modal-card-head"> - {!description ? null : ( - <p class="modal-card-title"> - <b>{description}</b> - </p> - )} + <p class="modal-card-title"> + <b>{title}</b> + </p> <button class="delete " type="button" @@ -97,7 +108,7 @@ export function ConfirmModal({ > {confirm ? ( <Fragment> - {onCancel && !noCancelButton ? ( + {onCancel ? ( <button class="button " type="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> @@ -105,13 +116,14 @@ export function ConfirmModal({ <Button submit + focus class={danger ? "button is-danger " : "button is-info "} - onClick={confirm} + onClick={confirm.handler} > - <i18n.Translate>{label}</i18n.Translate> + <i18n.Translate>{confirm.label}</i18n.Translate> </Button> </Fragment> - ) : noCancelButton ? undefined : ( + ) : !onCancel ? undefined : ( <button type="button" class="button " @@ -124,18 +136,51 @@ export function ConfirmModal({ </div> </footer> </div> - {noCancelButton ? undefined : ( - <button - type="button" - class="modal-close is-large " - aria-label="close" - onClick={onCancel} - /> - )} </div> ); } +export function BlockingModal({ + title, + confirm, + children, + danger, + focus, +}: BlockingProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="modal is-active"> + <div class="modal-background " /> + <div class="modal-card" style={{ maxWidth: 700 }}> + <header class="modal-card-head"> + {!title ? null : ( + <p class="modal-card-title"> + <b>{title}</b> + </p> + )} + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div + class="buttons" + style={{ width: "100%", justifyContent: "space-between" }} + > + {confirm ? ( + <Button + submit + focus={focus} + class={danger ? "button is-danger " : "button is-info "} + onClick={confirm.handler} + > + <i18n.Translate>{confirm.label}</i18n.Translate> + </Button> + ) : undefined} + </div> + </footer> + </div> + </div> + ); +} export function SimpleModal({ onCancel, children, @@ -198,11 +243,12 @@ export function CompareAccountsModal({ const { i18n } = useTranslationContext(); return ( <ConfirmModal - label={i18n.str`Correct the form`} - description={i18n.str`Comparing account details`} - active + title={i18n.str`Comparing account details`} onCancel={onCancel} - confirm={confirm} + confirm={{ + handler: confirm, + label: i18n.str`Correct the form`, + }} > <p> <i18n.Translate> @@ -366,11 +412,8 @@ export function ValidBankAccount({ const [{ showDebugInfo }] = useCommonPreferences(); return ( <ConfirmModal - label={i18n.str`OK`} - description={i18n.str`Validate bank account: ${from}`} - active + title={i18n.str`Validate bank account: ${from}`} onCancel={onCancel} - // onConfirm={onConfirm} > {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto, payto }, undefined, 2)}</pre> diff --git a/packages/taler-merchant-webui/src/hooks/transfer.ts b/packages/taler-merchant-webui/src/hooks/transfer.ts @@ -46,7 +46,9 @@ export interface InstanceConfirmedTransferFilter { export function revalidateInstanceIncomingTransfers() { return mutate( (key) => - Array.isArray(key) && key[key.length - 1] === "listIncomingWireTransfers", + Array.isArray(key) && + (key[key.length - 1] === "listIncomingWireTransfers" || + key[key.length - 1] === "getIncomingWireTransfersDetails"), undefined, { revalidate: true }, ); diff --git a/packages/taler-merchant-webui/src/paths/admin/create/CreatePage.tsx b/packages/taler-merchant-webui/src/paths/admin/create/CreatePage.tsx @@ -228,6 +228,12 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { return i18n.str`Conflict.`; case HttpStatusCode.NotFound: return i18n.str`Not found.`; + case HttpStatusCode.BadRequest: + return i18n.str`bad request`; + case HttpStatusCode.Forbidden: + return i18n.str`forbidden`; + case HttpStatusCode.PayloadTooLarge: + return i18n.str`payload too large` default: assertUnreachable(fail); } diff --git a/packages/taler-merchant-webui/src/paths/admin/list/View.tsx b/packages/taler-merchant-webui/src/paths/admin/list/View.tsx @@ -212,12 +212,13 @@ export function DeleteModal({ const { i18n } = useTranslationContext(); return ( <ConfirmModal - label={i18n.str`Delete instance`} - description={i18n.str`Delete the instance "${element.name}"`} + title={i18n.str`Delete the instance "${element.name}"`} danger - active onCancel={onCancel} - confirm={confirm} + confirm={{ + handler: confirm, + label: i18n.str`Delete instance`, + }} > <p> <i18n.Translate> @@ -249,12 +250,13 @@ function PurgeModal({ element, onCancel, confirm }: DeleteModalProps): VNode { const { i18n } = useTranslationContext(); return ( <ConfirmModal - label={i18n.str`Purge the instance`} - description={i18n.str`Purge the instance "${element.name}"`} + title={i18n.str`Purge the instance "${element.name}"`} danger - active onCancel={onCancel} - confirm={confirm} + confirm={{ + handler: confirm, + label: i18n.str`Purge the instance`, + }} > <p> <i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/instance/accessTokens/create-pos/CreatePage.tsx b/packages/taler-merchant-webui/src/paths/instance/accessTokens/create-pos/CreatePage.tsx @@ -114,6 +114,12 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { return i18n.str`Check the password.`; case HttpStatusCode.NotFound: return i18n.str`Instance not found.`; + case HttpStatusCode.BadRequest: + return i18n.str`bad request`; + case HttpStatusCode.Forbidden: + return i18n.str`forbidden`; + case HttpStatusCode.PayloadTooLarge: + return i18n.str`payload too large` default: assertUnreachable(fail); } diff --git a/packages/taler-merchant-webui/src/paths/instance/accessTokens/create-pos/index.tsx b/packages/taler-merchant-webui/src/paths/instance/accessTokens/create-pos/index.tsx @@ -53,10 +53,8 @@ export default function PosTokenCreatePage({ <Fragment> {!ok ? undefined : ( <ConfirmModal - active onCancel={onConfirm} - description={i18n.str`Access token created`} - label={i18n.str`Confirm`} + title={i18n.str`Access token created`} > <div class=""> <table> diff --git a/packages/taler-merchant-webui/src/paths/instance/accessTokens/create/CreatePage.tsx b/packages/taler-merchant-webui/src/paths/instance/accessTokens/create/CreatePage.tsx @@ -156,6 +156,12 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { return i18n.str`Check the password.`; case HttpStatusCode.NotFound: return i18n.str`Instance not found.`; + case HttpStatusCode.BadRequest: + return i18n.str`bad request`; + case HttpStatusCode.Forbidden: + return i18n.str`forbidden`; + case HttpStatusCode.PayloadTooLarge: + return i18n.str`payload too large`; default: assertUnreachable(fail); } diff --git a/packages/taler-merchant-webui/src/paths/instance/accessTokens/create/index.tsx b/packages/taler-merchant-webui/src/paths/instance/accessTokens/create/index.tsx @@ -46,10 +46,8 @@ export default function AccessTokenCreatePage({ <Fragment> {!ok ? undefined : ( <ConfirmModal - active onCancel={onConfirm} - description={i18n.str`Access token created`} - label={i18n.str`Confirm`} + title={i18n.str`Access token created`} > <div class=""> <table> diff --git a/packages/taler-merchant-webui/src/paths/instance/accessTokens/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/accessTokens/list/index.tsx @@ -124,12 +124,13 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { /> {deleting && ( <ConfirmModal - label={i18n.str`Delete access token`} - description={deleting.description as TranslatedString} + title={deleting.description as TranslatedString} danger - active onCancel={() => setDeleting(null)} - confirm={deleteToken} + confirm={{ + handler: deleteToken, + label: i18n.str`Delete access token`, + }} > <p class="warning"> <i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/instance/accounts/list/Table.tsx b/packages/taler-merchant-webui/src/paths/instance/accounts/list/Table.tsx @@ -85,16 +85,14 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { <Fragment> {deleting && ( <ConfirmModal - label={i18n.str`Delete account`} - description={i18n.str`Delete the account "${ + title={i18n.str`Delete the account "${ Result.orElse(Paytos.fromString(deleting.payto_uri), { displayName: i18n.str`Invalid payto: "${deleting.payto_uri}"`, }).displayName }"`} danger - active onCancel={() => setDeleting(null)} - confirm={remove} + confirm={{ handler: remove, label: i18n.str`Delete account` }} > <p> <i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/instance/exchanges/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/exchanges/list/index.tsx @@ -107,9 +107,7 @@ function ShowExchangeStatus({ if (isWorking) { return ( <ConfirmModal - label={i18n.str`Ok`} - description={e.exchange_url as TranslatedString} - active + title={e.exchange_url as TranslatedString} onCancel={onCancel} > <p style={{ paddingTop: 0 }}> @@ -139,9 +137,7 @@ function ShowExchangeStatus({ } return ( <ConfirmModal - label={i18n.str`Error`} - description={e.exchange_url as TranslatedString} - active + title={e.exchange_url as TranslatedString} onCancel={onCancel} > <p style={{ paddingTop: 0 }}> diff --git a/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/kyc/list/index.tsx @@ -131,9 +131,7 @@ function ShowInstructionForKycRedirect({ if (uri.tag === "error") { return ( <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`The account for wire transfers is invalid.`} - active + title={i18n.str`The account for wire transfers is invalid.`} onCancel={onCancel} > {!showDebugInfo ? undefined : ( @@ -170,9 +168,7 @@ function ShowInstructionForKycRedirect({ case TalerMerchantApi.MerchantAccountKycStatus.NO_EXCHANGE_KEY: { return ( <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`No contact with the payment service yet.`} - active + title={i18n.str`No contact with the payment service yet.`} onCancel={onCancel} > {!showDebugInfo ? undefined : ( @@ -190,9 +186,7 @@ function ShowInstructionForKycRedirect({ case TalerMerchantApi.MerchantAccountKycStatus.KYC_WIRE_IMPOSSIBLE: { return ( <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`No transfers can be made from this account`} - active + title={i18n.str`No transfers can be made from this account`} onCancel={onCancel} > {!showDebugInfo ? undefined : ( @@ -212,9 +206,7 @@ function ShowInstructionForKycRedirect({ case TalerMerchantApi.MerchantAccountKycStatus.KYC_REQUIRED: { return ( <ConfirmModal - label={i18n.str`Test`} - description={i18n.str`Know Your Customer process is required.`} - active + title={i18n.str`Know Your Customer process is required.`} onCancel={onCancel} > {!showDebugInfo ? undefined : ( @@ -249,12 +241,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.AWAITING_AML_REVIEW: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Awaiting AML review`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Awaiting AML review`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -269,12 +256,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.READY: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Ready`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Ready`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -289,12 +271,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.LOGIC_BUG: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Logic bug`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Logic bug`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -309,12 +286,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_INTERNAL_ERROR: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Internal error`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Internal error`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -329,12 +301,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.MERCHANT_INTERNAL_ERROR: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Internal error`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Internal error`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -349,12 +316,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Logic bug`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Logic bug`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -369,12 +331,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_UNREACHABLE: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Logic bug`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Logic bug`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -389,12 +346,7 @@ function ShowInstructionForKycRedirect({ } case TalerMerchantApi.MerchantAccountKycStatus.UNSUPPORTED_ACCOUNT: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Unsupported account`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Unsupported account`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre>{JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)}</pre> )} @@ -409,12 +361,7 @@ function ShowInstructionForKycRedirect({ case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_STATUS_INVALID: { return ( - <ConfirmModal - label={i18n.str`Ok`} - description={i18n.str`Logic bug`} - active - onCancel={onCancel} - > + <ConfirmModal title={i18n.str`Logic bug`} onCancel={onCancel}> {!showDebugInfo ? undefined : ( <pre> {JSON.stringify({ strPayto: e.payto_uri }, undefined, 2)} diff --git a/packages/taler-merchant-webui/src/paths/instance/orders/details/DetailPage.tsx b/packages/taler-merchant-webui/src/paths/instance/orders/details/DetailPage.tsx @@ -666,7 +666,6 @@ function PaidPage({ }); }); } - const refundTaken = Amounts.stringify(totalRefundedTaken); const maxTotalFee = Amounts.add(totalRefundedTaken, maxDepositFee).amount; const shouldBeWiredMinimum = Amounts.sub(amount, maxTotalFee).amount; // we have a minimum but not a price value @@ -713,21 +712,19 @@ function PaidPage({ return ( <div> <section class="section"> - <ConfirmModal - label={i18n.str`OK`} - description={i18n.str`Related wire transfers`} - active={verifyingWiretransfer.length > 0} - onCancel={() => { - setVerifyingWiretransfer([]); - }} - // onConfirm={onConfirm} - > - <CardTableIncoming - transfers={verifyingWiretransfer} - onSelected={onWireTransferSelection} - /> - </ConfirmModal> - {/* )} */} + {verifyingWiretransfer.length > 0 ? ( + <ConfirmModal + title={i18n.str`Related wire transfers`} + onCancel={() => { + setVerifyingWiretransfer([]); + }} + > + <CardTableIncoming + transfers={verifyingWiretransfer} + onSelected={onWireTransferSelection} + /> + </ConfirmModal> + ) : undefined} <div class="columns"> <div class="column" /> <div class="column is-10"> @@ -1116,7 +1113,7 @@ export function DetailPage({ TalerMerchantApi.CheckPaymentPaidResponse | undefined >(undefined); const { i18n } = useTranslationContext(); - const DetailByStatus = function () { + function DetailByStatus() { switch (selected.order_status) { case "claimed": return <ClaimedPage id={id} order={selected} />; @@ -1145,7 +1142,7 @@ export function DetailPage({ return ( <Fragment> - {DetailByStatus()} + <DetailByStatus /> {showRefund && ( <RefundModal order={showRefund} diff --git a/packages/taler-merchant-webui/src/paths/instance/orders/list/Table.tsx b/packages/taler-merchant-webui/src/paths/instance/orders/list/Table.tsx @@ -414,11 +414,11 @@ export function RefundModal({ (k) => (errors as Record<string, unknown>)[k] !== undefined, ); - const req: TalerMerchantApi.RefundRequest | undefined = !form.refund + const req: TalerMerchantApi.RefundRequest | undefined = hasErrors ? undefined : { refund: Amounts.stringify( - Amounts.add(Amounts.parse(form.refund)!, totalRefunded).amount, + Amounts.add(Amounts.parse(form.refund!)!, totalRefunded).amount, ), reason: form.description === undefined @@ -459,12 +459,13 @@ export function RefundModal({ //FIXME: parameters in the translation return ( <ConfirmModal - description={i18n.str`refund`} - label={i18n.str`Confirm`} + title={i18n.str`Refund`} danger - active onCancel={onCancel} - confirm={refund} + confirm={{ + handler: refund, + label: i18n.str`Confirm` + }} > {refunds.length > 0 && ( <div class="columns"> diff --git a/packages/taler-merchant-webui/src/paths/instance/password/DetailPage.tsx b/packages/taler-merchant-webui/src/paths/instance/password/DetailPage.tsx @@ -79,8 +79,6 @@ export function DetailPage({ const hasErrors = errors !== undefined; - const text = i18n.str`You are updating the password for the instance with ID "${instanceId}"`; - return ( <div> <section class="section"> @@ -89,7 +87,12 @@ export function DetailPage({ <div class="level"> <div class="level-left"> <div class="level-item"> - <span class="is-size-4">{text}</span> + <span class="is-size-4"> + <i18n.Translate> + You are updating the password for the instance with ID "$ + {instanceId}" + </i18n.Translate> + </span> </div> </div> </div> @@ -107,13 +110,11 @@ export function DetailPage({ name="current" label={i18n.str`Current password`} tooltip={i18n.str`In order to verify that you have access.`} - expand autoComplete="current-password" /> )} <InputPassword<State> name="next" - expand label={i18n.str`New password`} tooltip={i18n.str`Next password to be used`} autoComplete="new-password" diff --git a/packages/taler-merchant-webui/src/paths/instance/password/index.tsx b/packages/taler-merchant-webui/src/paths/instance/password/index.tsx @@ -47,6 +47,7 @@ export interface Props { onCancel: () => void; } +const emptyList: string[] = [] export default function PasswordPage({ onCancel, onChange }: Props): VNode { const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); @@ -107,7 +108,7 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { ); return updated; }, - !session.token ? undefined : [session.token, "", "", []], + !session.token ? undefined : [session.token, "", "", emptyList], ); changePassword.onSuccess = (suc) => { onChange(); diff --git a/packages/taler-merchant-webui/src/paths/instance/products/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/products/list/index.tsx @@ -124,19 +124,18 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { {deleting && ( <ConfirmModal - label={ - forceDeletion - ? i18n.str`Cancel orders and delete` - : i18n.str`Delete product` - } - description={i18n.str`Delete the product "${deleting.description}"`} + title={i18n.str`Delete the product "${deleting.description}"`} danger - active onCancel={() => { setDeleting(null); setForceDeletion(false); }} - confirm={remove} + confirm={{ + handler: remove, + label: forceDeletion + ? i18n.str`Cancel orders and delete` + : i18n.str`Delete product`, + }} > {forceDeletion ? ( <p> diff --git a/packages/taler-merchant-webui/src/paths/instance/templates/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/templates/list/index.tsx @@ -136,12 +136,13 @@ export default function ListTemplates({ {deleting && ( <ConfirmModal - label={i18n.str`Delete template`} - description={i18n.str`Delete the template "${deleting.template_description}"`} + title={i18n.str`Delete the template "${deleting.template_description}"`} danger - active onCancel={() => setDeleting(null)} - confirm={remove} + confirm={{ + handler: remove, + label: i18n.str`Delete template`, + }} > <p> <i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/tokenfamilies/list/index.tsx @@ -109,12 +109,13 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { {deleting && ( <ConfirmModal - label={i18n.str`Delete token family`} - description={i18n.str`Delete the token family "${deleting.name}"`} + title={i18n.str`Delete the token family "${deleting.name}"`} danger - active onCancel={() => setDeleting(null)} - confirm={remove} + confirm={{ + handler: remove, + label: i18n.str`Delete token family`, + }} > <p> <i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/instance/transfers/list/DetailsPage.tsx b/packages/taler-merchant-webui/src/paths/instance/transfers/list/DetailsPage.tsx @@ -225,9 +225,6 @@ function DetailsPageInternal({ </div> <div class="buttons is-right mt-5"> - <button class="button" type="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> <Button class="button is-success" submit onClick={confirm}> <i18n.Translate>I have received the wire transfer</i18n.Translate> diff --git a/packages/taler-merchant-webui/src/paths/login/index.tsx b/packages/taler-merchant-webui/src/paths/login/index.tsx @@ -31,6 +31,7 @@ import { } from "@gnu-taler/taler-util"; import { Button, + Loading, undefinedIfEmpty, useNotificationContext, useTranslationContext, @@ -110,7 +111,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { const api = getInstanceForUsername(usr); const resp = await api.createAccessToken( usr, - pwd.__value, + pwd.__pwd, FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), { challengeIds }, ); @@ -139,7 +140,13 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { case HttpStatusCode.NotFound: // setOpFailed("bad-username"); // return undefined; - return i18n.str`The account doesn't exist.`; + return i18n.str`User not found.`; + case HttpStatusCode.BadRequest: + return i18n.str`bad request.`; + case HttpStatusCode.PayloadTooLarge: + return i18n.str`payload too large.`; + case HttpStatusCode.Forbidden: + return i18n.str`forbidden.`; default: assertUnreachable(fail); } @@ -176,6 +183,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { <InputWithAddon<Form> name="username" label={i18n.str`Username`} + focus={focus} tooltip={i18n.str`Instance name.`} autoComplete="username" /> diff --git a/packages/taler-merchant-webui/src/paths/resetAccount/index.tsx b/packages/taler-merchant-webui/src/paths/resetAccount/index.tsx @@ -15,6 +15,7 @@ */ import { + asPassword, assertUnreachable, HttpStatusCode, MerchantAuthMethod, @@ -36,6 +37,8 @@ import { InputPassword } from "../../components/form/InputPassword.js"; import { useSessionContext } from "../../context/session.js"; import { undefinedIfEmpty } from "../../utils/table.js"; import { useMerchantChallengeHandlerContext } from "../../context/challenge.js"; +import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js"; +import { useMemo } from "preact/hooks"; const TALER_SCREEN_ID = 82; @@ -47,15 +50,24 @@ interface Props { onCancel: () => void; id: string; onReseted: () => void; + focus?: boolean; } +const emptyList: string[] = [] + export function ResetAccount({ onCancel, onReseted, id: instanceId, + focus, }: Props): VNode { const { i18n } = useTranslationContext(); - const { state: session, lib, logIn } = useSessionContext(); + const { + state: session, + lib, + getInstanceForUsername, + logIn, + } = useSessionContext(); const [value, setValue] = useState<Partial<Form>>({ // password: "asd", @@ -83,18 +95,34 @@ export function ResetAccount({ const { actionHandler, showError } = useNotificationContext(); const mfa = useMerchantChallengeHandlerContext(); + const pwd = useMemo(() => asPassword(value.password!), [value.password]); + /** + * Login after reset cant be implemented. + * + * See https://bugs.gnunet.org/view.php?id=10401#c27223 + */ const reset = actionHandler( /*reset password for self provision*/ - async (ct, password: string, challengeIds: string[]) => { - const forgot = await lib - .subInstanceApi(instanceId) - .instance.forgotPasswordSelfProvision( - { method: MerchantAuthMethod.TOKEN, password }, - { challengeIds }, - ); - return forgot; + async (ct, usr, pwd, challengeIds: string[]) => { + const api = getInstanceForUsername(usr); + const forgot = await api.forgotPasswordSelfProvision( + { method: MerchantAuthMethod.TOKEN, password: pwd.__pwd }, + { challengeIds }, + ); + // if (forgot.type !== "ok") { + return forgot; + // } + // const resp = await api.createAccessToken( + // usr, + // pwd.__pwd, + // FOREVER_REFRESHABLE_TOKEN(i18n.str`Reset password`), + // { challengeIds }, + // ); + // return resp; }, - hasErrors ? undefined : [value.password!, []], + hasErrors + ? undefined + : ([instanceId, pwd, emptyList] as const), ); reset.onSuccess = (suc) => { // logIn(instanceId, suc.access_token); @@ -108,13 +136,21 @@ export function ResetAccount({ mfa.onNewChallenge( i18n.str`Forgot password`, fail.body, - reset.lambda((prev, [ids]) => (!prev ? undefined : [prev[0], ids])), + reset.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), ); return undefined; case HttpStatusCode.Forbidden: return i18n.str`Forbidden.`; case HttpStatusCode.NotFound: - return i18n.str`The instance "${instanceId}" was not found.`; + return i18n.str`User not found.`; + // case HttpStatusCode.BadRequest: + // return i18n.str`bad request.`; + // case HttpStatusCode.PayloadTooLarge: + // return i18n.str`payload too large.`; + // case HttpStatusCode.Unauthorized: + // return i18n.str`Wrong passwowrd.`; default: assertUnreachable(fail); } @@ -147,13 +183,12 @@ export function ResetAccount({ <InputPassword<Form> label={i18n.str`New password`} name="password" - expand + focus autoComplete="new-password" /> <InputPassword<Form> label={i18n.str`Repeat password`} name="repeat" - expand autoComplete="new-password" /> </section> diff --git a/packages/taler-merchant-webui/src/scss/_loading.scss b/packages/taler-merchant-webui/src/scss/_loading.scss @@ -29,7 +29,7 @@ margin: 8px; border: 8px solid black; border-radius: 50%; - animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + animation: spin 1s cubic-bezier(0.5, 0, 0.5, 1) infinite; border-color: black transparent transparent transparent; } .lds-ring div:nth-child(1) { @@ -41,7 +41,7 @@ .lds-ring div:nth-child(3) { animation-delay: -0.15s; } -@keyframes lds-ring { +@keyframes spin { 0% { transform: rotate(0deg); } diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts @@ -186,6 +186,10 @@ export interface DetailsMap { txState: TransactionState; debugStateNum?: number; }; + [TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION]: { + client: string; + server: string; + } } type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty; diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -289,15 +289,7 @@ export class TalerMerchantInstanceHttpClient { params: { challengeIds?: string[]; } = {}, - ): Promise< - | OperationFail<HttpStatusCode.NotFound> - | OperationOk<TalerMerchantApi.LoginTokenSuccessResponse> - | OperationAlternative< - HttpStatusCode.Accepted, - TalerMerchantApi.ChallengeResponse - > - | OperationFail<HttpStatusCode.Unauthorized> - > { + ) { const url = new URL(`private/token`, this.baseUrl); const headers = authHeaders({ type: "basic", @@ -326,6 +318,12 @@ export class TalerMerchantInstanceHttpClient { codecForChallengeResponse(), ); } + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.PayloadTooLarge: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts @@ -45,12 +45,12 @@ export async function unlockOfficerAccount( password: Password, ): Promise<OfficerSession> { const rawKey = decodeCrock(account); - const rawPassword = stringToBytes(password.__value); + const rawPassword = stringToBytes(password.__pwd); const __signingKey = (await decryptWithDerivedKey( rawKey, rawPassword, - password.__value, + password.__pwd, ).catch((e) => { throw new UnwrapKeyError(e instanceof Error ? e.message : String(e)); })) as EddsaPrivP; @@ -62,10 +62,14 @@ export async function unlockOfficerAccount( return { id: accountId, __signingKey }; } declare const __password: unique symbol; -export function asPassword(pwd: string): Password { - return { __value: pwd } as Password; +export function asPassword(__pwd: string): Password { + return { __pwd } as Password; } -export type Password = { __value: string; [__password]: true }; +/** + * Opaque password type + * Useful to prevent accidentally printing when serializing into JSON + */ +export type Password = { __pwd: string; [__password]: true }; /** * Create new account (secured private key) @@ -81,7 +85,7 @@ export async function createNewOfficerAccount( ): Promise<OfficerSession & { safe: LockedAccount }> { const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); - const key = stringToBytes(password.__value); + const key = stringToBytes(password.__pwd); const localRnd = getRandomBytes(24); const mergedRnd: EncryptionNonceP = extraNonce @@ -92,7 +96,7 @@ export async function createNewOfficerAccount( mergedRnd, key, eddsaPriv, - password.__value, + password.__pwd, ); const __signingKey = eddsaPriv as EddsaPrivP; @@ -124,9 +128,9 @@ export async function createNewWalletKycAccount( const protectedPrivKey = password ? await encryptWithDerivedKey( mergedRnd, - stringToBytes(password.__value), + stringToBytes(password.__pwd), eddsaPriv, - password.__value, + password.__pwd, ) : undefined; diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -521,11 +521,19 @@ declare const opaque_OfficerSigningKey: unique symbol; export interface OfficerSession { id: OfficerId; + /** + * Double __ to prevent accidentally + * printing when serializing into JSON + */ __signingKey: EddsaPrivP; } export interface ReserveAccount { id: EddsaPublicKeyString; + /** + * Double __ to prevent accidentally + * printing when serializing into JSON + */ __signingKey: EddsaPrivP; } diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx @@ -95,7 +95,6 @@ export function Button({ disabled={running || !onClick || !onClick.args || disabled} ref={focus ? doAutoFocus : undefined} type={submit ? "submit" : "button"} - data-pepe="si" data-failed={failed ? "true" : undefined} onClick={(e) => { e.preventDefault(); @@ -145,12 +144,7 @@ function Failed(): VNode { // const tailwind_textNeutralTertiary: CSSProperties = {}; const tailwind_animateSpin: CSSProperties = { - animation: "spin 1s linear infinite", -}; -// const tailwind_fillBrand: CSSProperties = {}; -const tailwind_size8: CSSProperties = { - width: 8, - height: 8, + animation: "spin 1s cubic-bezier(0.5, 0, 0.5, 1) infinite", }; const tailwind_size24: CSSProperties = { width: 24, @@ -187,7 +181,6 @@ function Wait(): VNode { stroke-width="1.5" stroke="currentColor" style={{ ...singleLineHeight, ...tailwind_animateSpin }} - // class="text-neutral-tertiary animate-spin fill-brand " > <path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /> </svg> diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts @@ -17,9 +17,12 @@ import { CacheEvictor, LibtoolVersion, + makeErrorDetail, + makeTalerErrorDetail, ObservabilityEvent, ObservableHttpClientLibrary, TalerError, + TalerErrorCode, TalerExchangeApi, TalerExchangeCacheEviction, TalerExchangeHttpClient, @@ -153,21 +156,36 @@ export const ExchangeApiProvider = ({ if (checked === undefined) { return h(frameOnError, { - children: h("div", {}, i18n.str`Checking compatibility of this webapp with backend service...`), + children: h( + "div", + {}, + i18n.str`Checking compatibility of this webapp with backend service...`, + ), }); } if (checked.type === "error") { return h(frameOnError, { - children: h(ErrorLoading, { title: i18n.str`There was an error trying to contact the backend service.`, error: checked.error }), + children: h(ErrorLoading, { + title: i18n.str`There was an error trying to contact the backend service.`, + error: checked.error, + }), }); } if (checked.type === "incompatible") { + const title = i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.config.version}"`; + const error = TalerError.fromDetail( + TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + { + client: checked.supported, + server: checked.result.config.version, + }, + title, + ); return h(frameOnError, { - children: h( - "div", - {}, - i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.config.version}"`, - ), + children: h(ErrorLoading, { + title, + error, + }), }); } diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts @@ -20,6 +20,7 @@ import { ObservabilityEvent, ObservableHttpClientLibrary, TalerError, + TalerErrorCode, TalerMerchantApi, TalerMerchantInstanceCacheEviction, TalerMerchantManagementCacheEviction, @@ -34,7 +35,10 @@ import { } from "preact"; import { useContext, useEffect, useState } from "preact/hooks"; import { ErrorLoading } from "../components/ErrorLoadingMerchant.js"; -import { BrowserFetchHttpLib, useTranslationContext } from "../index.browser.js"; +import { + BrowserFetchHttpLib, + useTranslationContext, +} from "../index.browser.js"; import { APIClient, ActiviyTracker, @@ -74,7 +78,7 @@ type Evictors = { >; }; -type ConfigResult<T> = +export type ConfigResult<T> = | undefined | { type: "ok"; config: T; hints: VersionHint[] } | ConfigResultFail<T>; @@ -94,7 +98,10 @@ export const MerchantApiProvider = ({ baseUrl: URL; evictors?: Evictors; children: ComponentChildren; - frameOnError: FunctionComponent<{ children: ComponentChildren }>; + frameOnError: FunctionComponent<{ + status: ConfigResult<TalerMerchantApi.MerchantVersionResponse>; + children: ComponentChildren; + }>; }): VNode => { const [checked, setChecked] = useState<ConfigResult<TalerMerchantApi.MerchantVersionResponse>>(); @@ -141,21 +148,39 @@ export const MerchantApiProvider = ({ if (checked === undefined) { return h(frameOnError, { - children: h("div", {}, i18n.str`Checking compatibility of this webapp with backend service...`), + status: checked, + children: h( + "div", + {}, + i18n.str`Checking compatibility of this webapp with backend service...`, + ), }); } if (checked.type === "error") { return h(frameOnError, { - children: h(ErrorLoading, { title: i18n.str`There was an error trying to contact the backend service.`, error: checked.error }), + status: checked, + children: h(ErrorLoading, { + title: i18n.str`There was an error trying to contact the backend service.`, + error: checked.error, + }), }); } if (checked.type === "incompatible") { + const title = i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`; + const error = TalerError.fromDetail( + TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + { + client: checked.supported, + server: checked.result.version, + }, + title, + ); return h(frameOnError, { - children: h( - "div", - {}, - i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`, - ), + status: checked, + children: h(ErrorLoading, { + title, + error, + }), }); } diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts @@ -72,8 +72,6 @@ export async function serve(opts: { examplesLocationJs?: string; examplesLocationCss?: string; onSourceUpdate?: () => Promise<void>; - appPath?: string; - appSamplePath?: string; }): Promise<void> { if (process.env.SERVER_DIR) { return watch(opts); @@ -82,7 +80,7 @@ export async function serve(opts: { WS: "/ws", EXAMPLE: "/examples", ROOT: "/", - APP: opts.appPath ?? "/app", + APP: "/app", }; const app = createApp(); @@ -132,7 +130,7 @@ export async function serve(opts: { it will connect to this server using websocket and reload automatically when the code changes <h1>Endpoints</h1> <dl> - <dt><a href=".${opts.appSamplePath ?? "/app"}">app</a></dt> + <dt><a href="./app">app</a></dt> <dd>Where you can find the application. Reloads on update.</dd> <dt><a href="./examples">ui examples</a></dt>