diff options
Diffstat (limited to 'packages')
49 files changed, 1569 insertions, 660 deletions
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json index 565d1c68a..c3549ef52 100644 --- a/packages/aml-backoffice-ui/package.json +++ b/packages/aml-backoffice-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/aml-backoffice-ui", - "version": "0.12.1", + "version": "0.12.2", "author": "sebasjm", "license": "AGPL-3.0-OR-LATER", "description": "Back-office SPA for GNU Taler Exchange.", diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json index 01ff9fc69..47d1505d1 100644 --- a/packages/anastasis-cli/package.json +++ b/packages/anastasis-cli/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/anastasis-cli", - "version": "0.12.1", + "version": "0.12.2", "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json index 3e0f49ccb..c89b8eecc 100644 --- a/packages/anastasis-core/package.json +++ b/packages/anastasis-core/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/anastasis-core", - "version": "0.12.1", + "version": "0.12.2", "description": "", "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index ee7b3bf4e..17e8e74fc 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/anastasis-webui", - "version": "0.12.1", + "version": "0.12.2", "license": "MIT", "type": "module", "scripts": { diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json index c64110357..bbebabf39 100644 --- a/packages/auditor-backoffice-ui/package.json +++ b/packages/auditor-backoffice-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/auditor-backoffice-ui", - "version": "0.12.1", + "version": "0.12.2", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json index 21abbc3e9..65281bf2b 100644 --- a/packages/bank-ui/package.json +++ b/packages/bank-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/bank-ui", - "version": "0.12.1", + "version": "0.12.2", "license": "AGPL-3.0-OR-LATER", "type": "module", "scripts": { diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts index fadbbc8c1..a03234634 100644 --- a/packages/bank-ui/src/hooks/preferences.ts +++ b/packages/bank-ui/src/hooks/preferences.ts @@ -43,7 +43,7 @@ export const codecForPreferences = (): Codec<Preferences> => .property("showDebugInfo", codecForBoolean()) .property("fastWithdrawalForm", codecForBoolean()) .property("showCopyAccount", codecForBoolean()) - .build("Settings"); + .build("Preferences"); const defaultPreferences: Preferences = { showWithdrawalSuccess: true, diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx index 62b5c6f90..e969caaa7 100644 --- a/packages/bank-ui/src/pages/BankFrame.tsx +++ b/packages/bank-ui/src/pages/BankFrame.tsx @@ -162,7 +162,6 @@ export function BankFrame({ <div class="fixed z-20 top-14 w-full"> <div class="mx-auto w-4/5"> <ToastBanner /> - {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */} </div> </div> diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json index 370051498..d4d047326 100644 --- a/packages/challenger-ui/package.json +++ b/packages/challenger-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/challenger-ui", - "version": "0.12.1", + "version": "0.12.2", "author": "sebasjm", "license": "AGPL-3.0-OR-LATER", "description": "UI for GNU Challenger.", diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx index 7f9f52a19..f7488cb8d 100644 --- a/packages/challenger-ui/src/Routing.tsx +++ b/packages/challenger-ui/src/Routing.tsx @@ -23,6 +23,7 @@ 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, useSessionState } from "./hooks/session.js"; import { AnswerChallenge } from "./pages/AnswerChallenge.js"; @@ -32,7 +33,6 @@ import { Frame } from "./pages/Frame.js"; import { MissingParams } from "./pages/MissingParams.js"; import { NonceNotFound } from "./pages/NonceNotFound.js"; import { Setup } from "./pages/Setup.js"; -import { useErrorBoundary } from "preact/hooks"; export function Routing(): VNode { // check session and defined if this is diff --git a/packages/challenger-ui/src/context/preferences.ts b/packages/challenger-ui/src/context/preferences.ts new file mode 100644 index 000000000..3188bd71c --- /dev/null +++ b/packages/challenger-ui/src/context/preferences.ts @@ -0,0 +1,87 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + Codec, + TranslatedString, + buildCodecForObject, + codecForBoolean, +} from "@gnu-taler/taler-util"; +import { + buildStorageKey, + useLocalStorage, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; + +interface Preferences { + showChallangeSetup: boolean; + showDebugInfo: boolean; +} + +export const codecForPreferences = (): Codec<Preferences> => + buildCodecForObject<Preferences>() + .property("showChallangeSetup", codecForBoolean()) + .property("showDebugInfo", codecForBoolean()) + .build("Preferences"); + +const defaultPreferences: Preferences = { + showChallangeSetup: false, + showDebugInfo: false, +}; + +const PREFERENCES_KEY = buildStorageKey( + "challenger-preferences", + codecForPreferences(), +); +/** + * User preferences. + * + * @returns tuple of [state, update()] + */ +export function usePreferences(): [ + Readonly<Preferences>, + <T extends keyof Preferences>(key: T, value: Preferences[T]) => void, +] { + const { value, update } = useLocalStorage( + PREFERENCES_KEY, + defaultPreferences, + ); + + function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + return [value, updateField]; +} + +export function getAllBooleanPreferences(): Array<keyof Preferences> { + return [ + "showChallangeSetup", + "showDebugInfo", + ]; +} + +export function getLabelForPreferences( + k: keyof Preferences, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): TranslatedString { + switch (k) { + case "showChallangeSetup": + return i18n.str`Show challenger setup screen`; + case "showDebugInfo": + return i18n.str`Show debug info`; + } +} diff --git a/packages/challenger-ui/src/declaration.d.ts b/packages/challenger-ui/src/declaration.d.ts new file mode 100644 index 000000000..581cbcd07 --- /dev/null +++ b/packages/challenger-ui/src/declaration.d.ts @@ -0,0 +1,35 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +declare module "*.css" { + const mapping: Record<string, string>; + export default mapping; +} +declare module "*.svg" { + const content: string; + export default content; +} +declare module "*.jpeg" { + const content: string; + export default content; +} +declare module "*.png" { + const content: string; + export default content; +} + +declare const __VERSION__: string; +declare const __GIT_HASH__: string; diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts index eeb330493..4dc7e0dc1 100644 --- a/packages/challenger-ui/src/hooks/session.ts +++ b/packages/challenger-ui/src/hooks/session.ts @@ -15,9 +15,11 @@ */ import { + AbsoluteTime, ChallengerApi, Codec, buildCodecForObject, + codecForAbsoluteTime, codecForBoolean, codecForChallengeStatus, codecForNumber, @@ -39,8 +41,10 @@ export type SessionId = { }; export type LastChallengeResponse = { - attemptsLeft: number; - nextSend: string; + sendCodeLeft: number; + changeTargetLeft: number; + checkPinLeft: number; + nextSend: AbsoluteTime; transmitted: boolean; }; @@ -51,8 +55,10 @@ export type SessionState = SessionId & { }; export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> => buildCodecForObject<LastChallengeResponse>() - .property("attemptsLeft", codecForNumber()) - .property("nextSend", codecForString()) + .property("sendCodeLeft", codecForNumber()) + .property("changeTargetLeft", codecForNumber()) + .property("checkPinLeft", codecForNumber()) + .property("nextSend", codecForAbsoluteTime) .property("transmitted", codecForBoolean()) .build("LastChallengeResponse"); @@ -125,7 +131,8 @@ export function useSessionState(): SessionStateHandler { const ls = state.lastStatus; if ( ls.changes_left !== st.changes_left || - ls.fix_address !== st.fix_address || ls.last_address !== st.last_address + ls.fix_address !== st.fix_address || + ls.last_address !== st.last_address ) { update({ ...state, diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx index 5fe2d9743..2740e1bdb 100644 --- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -14,8 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { + AbsoluteTime, ChallengerApi, HttpStatusCode, + TalerProtocolTimestamp, assertUnreachable, } from "@gnu-taler/taler-util"; import { @@ -52,8 +54,7 @@ export function AnswerChallenge({ const { state, accepted, completed } = useSessionState(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [pin, setPin] = useState<string | undefined>(); - const [lastTryError, setLastTryError] = - useState<ChallengerApi.InvalidPinResponse>(); + const errors = undefinedIfEmpty({ pin: !pin ? i18n.str`Can't be empty` : undefined, }); @@ -67,7 +68,9 @@ export function AnswerChallenge({ : state.lastStatus.last_address["email"]; const onSendAgain = - !state || lastEmail === undefined + lastEmail === undefined || + state?.lastStatus == undefined || + state?.lastStatus.changes_left === 0 ? undefined : withErrorHandler( async () => { @@ -79,8 +82,12 @@ export function AnswerChallenge({ completed(new URL(ok.body.redirect_url)); } else { accepted({ - attemptsLeft: ok.body.attempts_left, - nextSend: ok.body.next_tx_time, + changeTargetLeft: ok.body.attempts_left, + checkPinLeft: state.lastStatus?.auth_attempts_left ?? 0, + sendCodeLeft: state.lastStatus?.pin_transmissions_left ?? 0, + nextSend: AbsoluteTime.fromProtocolTimestamp( + ok.body.retransmission_time, + ), transmitted: ok.body.transmitted, }); } @@ -89,23 +96,23 @@ export function AnswerChallenge({ (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str``; - case HttpStatusCode.Forbidden: - return i18n.str``; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; } }, ); const onCheck = - errors !== undefined || (lastTryError && lastTryError.exhausted) + errors !== undefined || + state?.lastStatus == undefined || + state?.lastStatus.auth_attempts_left === 0 ? undefined : withErrorHandler( async () => { @@ -115,25 +122,34 @@ export function AnswerChallenge({ if (ok.body.type === "completed") { completed(new URL(ok.body.redirect_url)); } else { - setLastTryError(ok.body); + accepted({ + changeTargetLeft: ok.body.addresses_left, + checkPinLeft: ok.body.auth_attempts_left, + sendCodeLeft: ok.body.pin_transmissions_left, + nextSend: AbsoluteTime.fromProtocolTimestamp( + state?.lastStatus?.retransmission_time ?? + TalerProtocolTimestamp.now(), + ), + transmitted: state?.lastTry?.transmitted ?? false, + }); } onComplete(); }, (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str`Invalid request`; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.Forbidden: { - return i18n.str`Too many attemps where made`; + return i18n.str`Invalid pin.`; } case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; default: assertUnreachable(fail); } @@ -174,11 +190,11 @@ export function AnswerChallenge({ </Attention> )} </p> - {!lastTryError ? undefined : ( + {!state.lastStatus ? undefined : ( <p class="mt-2 text-lg leading-8 text-gray-600"> <i18n.Translate> You can try another PIN but just{" "} - {lastTryError.auth_attempts_left} times more. + {state.lastStatus.auth_attempts_left} times more. </i18n.Translate> </p> )} @@ -222,8 +238,21 @@ export function AnswerChallenge({ <p class="mt-3 text-sm leading-6 text-gray-400"> <i18n.Translate> - You have {state.lastTry.attemptsLeft} attempts left. + We send the code {state.lastTry.checkPinLeft} more times. </i18n.Translate> + {state.lastTry.checkPinLeft < 1 ? ( + <i18n.Translate> + You can't check the PIN anymore. + </i18n.Translate> + ) : state.lastTry.checkPinLeft === 1 ? ( + <i18n.Translate> + You can check the PIN one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can check the PIN {state.lastTry.checkPinLeft} more times. + </i18n.Translate> + )} </p> </div> @@ -240,12 +269,31 @@ export function AnswerChallenge({ <div class="mt-10 flex justify-between"> <div> <a + data-disabled={!state.lastStatus || state.lastStatus.changes_left < 1} href={routeAsk.url({ nonce })} - class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" > <i18n.Translate>Change email</i18n.Translate> </a> - </div> + {state.lastStatus === undefined ? undefined : + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can't change the email anymore. + </i18n.Translate> + ) : state.lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the email one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the email {state.lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} + </p> + } + </div> <div> <Button type="submit" @@ -255,6 +303,22 @@ export function AnswerChallenge({ > <i18n.Translate>Send code again</i18n.Translate> </Button> + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastTry.sendCodeLeft < 1 ? ( + <i18n.Translate> + We can't send you the code anymore. + </i18n.Translate> + ) : state.lastTry.sendCodeLeft === 1 ? ( + <i18n.Translate> + We can send the code one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + We can send the code {state.lastTry.sendCodeLeft} more + times. + </i18n.Translate> + )} + </p> </div> </div> </form> diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx index 829cdaccc..dc60562b7 100644 --- a/packages/challenger-ui/src/pages/AskChallenge.tsx +++ b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; import { Attention, Button, @@ -48,13 +48,15 @@ export function AskChallenge({ focus, }: Props): VNode { const { state, accepted, completed } = useSessionState(); + const { lib, config } = useChallengerApiContext(); + const status = state?.lastStatus; const prevEmail = !status || !status.last_address ? undefined : status.last_address["email"]; - const regexEmail = - !status || !status.restrictions ? undefined : status.restrictions["email"]; + const regexEmail = !config.restrictions + ? undefined + : config.restrictions["email"]; - const { lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [email, setEmail] = useState<string | undefined>(); @@ -91,8 +93,12 @@ export function AskChallenge({ completed(new URL(ok.body.redirect_url)); } else { accepted({ - attemptsLeft: ok.body.attempts_left, - nextSend: ok.body.next_tx_time, + changeTargetLeft: ok.body.attempts_left, + checkPinLeft: state?.lastStatus?.auth_attempts_left ?? 0, + sendCodeLeft: state?.lastStatus?.pin_transmissions_left ?? 0, + nextSend: AbsoluteTime.fromProtocolTimestamp( + ok.body.retransmission_time, + ), transmitted: ok.body.transmitted, }); } @@ -101,17 +107,15 @@ export function AskChallenge({ (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str``; - case HttpStatusCode.Forbidden: - return i18n.str``; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; } }, ); @@ -122,7 +126,7 @@ export function AskChallenge({ return ( <Fragment> - <LocalNotificationBanner notification={notification} /> + <LocalNotificationBanner notification={notification} showDebug={true} /> <div class="isolate bg-white px-6 py-12"> <div class="mx-auto max-w-2xl text-center"> @@ -213,16 +217,22 @@ export function AskChallenge({ </div> )} - {!status.changes_left ? ( - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate>No more changes left</i18n.Translate> - </p> - ) : ( - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate> - You can change your email address another{" "} - {status.changes_left} times. - </i18n.Translate> + {state.lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can't change the email anymore. + </i18n.Translate> + ) : state.lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the email one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the email {state.lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} </p> )} </div> diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx index 612eced0b..dd2a13d8c 100644 --- a/packages/challenger-ui/src/pages/Frame.tsx +++ b/packages/challenger-ui/src/pages/Frame.tsx @@ -14,56 +14,121 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { + Footer, + Header, + ToastBanner, + notifyError, + notifyException, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useSettingsContext } from "../context/settings.js"; +import { useEffect, useErrorBoundary } from "preact/hooks"; +import { TranslatedString } from "@gnu-taler/taler-util"; +import { + getAllBooleanPreferences, + getLabelForPreferences, + usePreferences, +} from "../context/preferences.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]); + return ( - <Fragment> - <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400"> - <div class="flex flex-row h-16 items-center "> - <div class="flex px-2 justify-start"> - <div class="flex-shrink-0 bg-white rounded-lg"> - <a href="#"> - <img - class="h-8 w-auto" - src='data:image/svg+xml,<?xml version="1.0" encoding="UTF-8" standalone="no"?>%0A<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">%0A <g fill="%230042b3" fill-rule="evenodd" stroke-width=".3">%0A <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />%0A <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />%0A <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />%0A </g>%0A <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />%0A</svg>' - alt="GNU Taler" - style="height: 1.5rem; margin: 0.5rem;" - /> - </a> - </div> - <span class="flex items-center text-white text-lg font-bold ml-4"> - Challenger - </span> + <div + class="min-h-full flex flex-col m-0 bg-slate-200" + style="min-height: 100vh;" + > + <Header + title="Challenger" + onLogout={undefined} + iconLinkURL="#" + sites={preferences.showChallangeSetup ? [ + ["New challenge","#/setup/1"] + ] :[]} + supportedLangs={["en"]} + > + <li> + <div class="text-xs font-semibold leading-6 text-gray-400"> + <i18n.Translate>Preferences</i18n.Translate> </div> - <div class="block flex-1 ml-6 "></div> - <div class="flex justify-end"></div> + <ul role="list" class="space-y-4"> + {getAllBooleanPreferences().map((set) => { + const isOn: boolean = !!preferences[set]; + return ( + <li key={set} class="pl-2"> + <div class="flex items-center justify-between"> + <span class="flex flex-grow flex-col"> + <span + class="text-sm text-black font-medium leading-6 " + id="availability-label" + > + {getLabelForPreferences(set, i18n)} + </span> + </span> + <button + type="button" + name={`${set} switch`} + data-enabled={isOn} + class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" + role="switch" + aria-checked="false" + aria-labelledby="availability-label" + aria-describedby="availability-description" + onClick={() => { + updatePreferences(set, !isOn); + }} + > + <span + aria-hidden="true" + data-enabled={isOn} + class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + ></span> + </button> + </div> + </li> + ); + })} + </ul> + </li> + </Header> + + <div class="fixed z-20 top-14 w-full"> + <div class="mx-auto w-4/5"> + <ToastBanner /> </div> - </header> + </div> <main class="flex-1">{children}</main> - - <footer class="bottom-4 mb-4"> - <div class="mt-8 mx-8 md:order-1 md:mt-0"> - <div> - <p class="text-xs leading-5 text-gray-400"> - Learn more about{" "} - <a - target="_blank" - rel="noreferrer noopener" - class="font-semibold text-gray-500 hover:text-gray-400" - href="https://taler.net" - > - GNU Taler - </a> - </p> - </div> - <div style="flex-grow: 1;"></div> - <p class="text-xs leading-5 text-gray-400"> - Copyright © 2014—2023 Taler Systems SA.{" "} - </p> - </div> - </footer> - </Fragment> + + <Footer + testingUrlKey="challenger-base-url" + GIT_HASH={GIT_HASH} + VERSION={VERSION} + /> + </div> ); } diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json index 3c581d537..a176fadc9 100644 --- a/packages/idb-bridge/package.json +++ b/packages/idb-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/idb-bridge", - "version": "0.12.1", + "version": "0.12.2", "description": "IndexedDB implementation that uses SQLite3 as storage", "main": "./dist/idb-bridge.js", "module": "./lib/index.js", diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json index c611f61c2..683329245 100644 --- a/packages/merchant-backend-ui/package.json +++ b/packages/merchant-backend-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/merchant-backend-ui", - "version": "0.12.1", + "version": "0.12.2", "license": "AGPL-3.0-or-later", "scripts": { "compile": "tsc && ./build.mjs", diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index 0701a12e5..babadbb6a 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/merchant-backoffice-ui", - "version": "0.12.1", + "version": "0.12.2", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po index 42cb6a76f..58a3745ac 100644 --- a/packages/merchant-backoffice-ui/src/i18n/es.po +++ b/packages/merchant-backoffice-ui/src/i18n/es.po @@ -17,7 +17,7 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2024-06-26 08:05+0000\n" +"PO-Revision-Date: 2024-06-28 00:57+0000\n" "Last-Translator: Luis Avalos <avalos.diaz.0577@gmail.com>\n" "Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" "merchant-backoffice/es/>\n" @@ -746,8 +746,8 @@ msgid "" "Time until which the wallet will automatically check for refunds without " "user interaction." msgstr "" -"Tiempo hasta el cual la billetera será automáticamente revisada por " -"reembolsos win interación por parte del usuario." +"Tiempo hasta el cual la cartera será automáticamente revisada por reembolsos " +"win interación por parte del usuario." #: src/paths/instance/orders/create/CreatePage.tsx:502 #, c-format @@ -841,7 +841,7 @@ msgstr "" #: src/paths/instance/orders/create/CreatePage.tsx:541 #, c-format msgid "You must enter a value in JavaScript Object Notation (JSON)." -msgstr "" +msgstr "Debes introducir un valor en JavaScript Object Notation (JSON)." #: src/components/picker/DurationPicker.tsx:55 #, c-format @@ -1037,6 +1037,7 @@ msgstr "Máxima comisión" #, c-format msgid "maximum total deposit fee accepted by the merchant for this contract" msgstr "" +"tasa máxima total de depósito aceptada por el comerciante para este contrato" #: src/paths/instance/orders/details/DetailPage.tsx:93 #, c-format @@ -1046,7 +1047,7 @@ msgstr "Impuesto de transferencia máximo" #: src/paths/instance/orders/details/DetailPage.tsx:94 #, c-format msgid "maximum wire fee accepted by the merchant" -msgstr "" +msgstr "comisión máxima por transferencia aceptada por el comerciante" #: src/paths/instance/orders/details/DetailPage.tsx:100 #, c-format @@ -1063,23 +1064,23 @@ msgstr "Creado en" #: src/paths/instance/orders/details/DetailPage.tsx:106 #, c-format msgid "time when this contract was generated" -msgstr "" +msgstr "momento en que se generó este contrato" #: src/paths/instance/orders/details/DetailPage.tsx:112 #, c-format msgid "after this deadline has passed no refunds will be accepted" -msgstr "" +msgstr "pasado este plazo no se aceptarán devoluciones" #: src/paths/instance/orders/details/DetailPage.tsx:118 #, c-format msgid "" "after this deadline, the merchant won't accept payments for the contract" -msgstr "" +msgstr "pasado este plazo, el comerciante no aceptará pagos por el contrato" #: src/paths/instance/orders/details/DetailPage.tsx:124 #, c-format msgid "transfer deadline for the exchange" -msgstr "" +msgstr "plazo de transferencia para el intercambio" #: src/paths/instance/orders/details/DetailPage.tsx:130 #, c-format @@ -1089,7 +1090,7 @@ msgstr "" #: src/paths/instance/orders/details/DetailPage.tsx:136 #, c-format msgid "where the order will be delivered" -msgstr "" +msgstr "dónde se entregará el pedido" #: src/paths/instance/orders/details/DetailPage.tsx:144 #, fuzzy, c-format @@ -1101,6 +1102,8 @@ msgstr "Plazo de reembolso automático" msgid "" "how long the wallet should try to get an automatic refund for the purchase" msgstr "" +"cuánto tiempo debe intentar la cartera obtener el reembolso automático de la " +"compra" #: src/paths/instance/orders/details/DetailPage.tsx:150 #, fuzzy, c-format @@ -1111,6 +1114,7 @@ msgstr "Información extra" #, c-format msgid "extra data that is only interpreted by the merchant frontend" msgstr "" +"datos adicionales que solo son interpretados por la interfaz del comerciante" #: src/paths/instance/orders/details/DetailPage.tsx:219 #, c-format @@ -1163,9 +1167,9 @@ msgid "refunded" msgstr "reembolzado" #: src/paths/instance/orders/details/DetailPage.tsx:480 -#, fuzzy, c-format +#, c-format msgid "refund order" -msgstr "reembolzado" +msgstr "reembolsado" #: src/paths/instance/orders/details/DetailPage.tsx:481 #, fuzzy, c-format @@ -1180,12 +1184,12 @@ msgstr "reembolzar" #: src/paths/instance/orders/details/DetailPage.tsx:553 #, c-format msgid "Refunded amount" -msgstr "Monto reembolzado" +msgstr "Monto reembolsado" #: src/paths/instance/orders/details/DetailPage.tsx:560 -#, fuzzy, c-format +#, c-format msgid "Refund taken" -msgstr "Reembolzado" +msgstr "Reembolsado" #: src/paths/instance/orders/details/DetailPage.tsx:570 #, fuzzy, c-format @@ -1248,7 +1252,7 @@ msgstr "No se pudo create el reembolso" #: src/paths/instance/orders/list/ListPage.tsx:78 #, c-format msgid "select date to show nearby orders" -msgstr "" +msgstr "seleccione la fecha para mostrar pedidos cercanos" #: src/paths/instance/orders/list/ListPage.tsx:94 #, c-format @@ -1258,17 +1262,17 @@ msgstr "ID de la orden" #: src/paths/instance/orders/list/ListPage.tsx:100 #, c-format msgid "jump to order with the given order ID" -msgstr "" +msgstr "saltar al pedido con el ID de pedido proporcionado" #: src/paths/instance/orders/list/ListPage.tsx:122 #, c-format msgid "remove all filters" -msgstr "" +msgstr "eliminar todos los filtros" #: src/paths/instance/orders/list/ListPage.tsx:132 #, c-format msgid "only show paid orders" -msgstr "" +msgstr "mostrar sólo pedidos pagados" #: src/paths/instance/orders/list/ListPage.tsx:135 #, c-format @@ -1291,6 +1295,8 @@ msgid "" "only show orders where customers paid, but wire payments from payment " "provider are still pending" msgstr "" +"mostrar sólo los pedidos en los que los clientes han pagado, pero los pagos " +"por transferencia del proveedor de pago siguen pendientes" #: src/paths/instance/orders/list/ListPage.tsx:155 #, c-format @@ -1300,7 +1306,7 @@ msgstr "No transferido" #: src/paths/instance/orders/list/ListPage.tsx:170 #, c-format msgid "clear date filter" -msgstr "" +msgstr "borrar filtro de fechas" #: src/paths/instance/orders/list/ListPage.tsx:184 #, c-format @@ -1333,6 +1339,8 @@ msgid "" "click here to configure the stock of the product, leave it as is and the " "backend will not control stock" msgstr "" +"pulse aquí para configurar el stock del producto, déjelo como está y el " +"backend no controlará el stock" #: src/components/form/InputStock.tsx:109 #, c-format @@ -1342,7 +1350,7 @@ msgstr "Administrar stock" #: src/components/form/InputStock.tsx:115 #, c-format msgid "this product has been configured without stock control" -msgstr "" +msgstr "este producto se ha configurado sin control de existencias" #: src/components/form/InputStock.tsx:119 #, c-format @@ -1372,7 +1380,7 @@ msgstr "Actual" #: src/components/form/InputStock.tsx:196 #, c-format msgid "remove stock control for this product" -msgstr "" +msgstr "eliminar el control de existencias de este producto" #: src/components/form/InputStock.tsx:202 #, c-format @@ -1393,26 +1401,27 @@ msgstr "Dirección de entrega" #, c-format msgid "product identification to use in URLs (for internal use only)" msgstr "" +"Identificación del producto para usar en las URL (solo para uso interno)" #: src/components/product/ProductForm.tsx:139 #, c-format msgid "illustration of the product for customers" -msgstr "" +msgstr "ilustración del producto para los clientes" #: src/components/product/ProductForm.tsx:145 #, c-format msgid "product description for customers" -msgstr "" +msgstr "descripción del producto para los clientes" #: src/components/product/ProductForm.tsx:149 #, c-format msgid "Age restricted" -msgstr "" +msgstr "Restricción de edad" #: src/components/product/ProductForm.tsx:150 #, c-format msgid "is this product restricted for customer below certain age?" -msgstr "" +msgstr "¿este producto está restringido para clientes menores de cierta edad?" #: src/components/product/ProductForm.tsx:155 #, c-format @@ -1420,12 +1429,16 @@ msgid "" "unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 " "items, 5 meters) for customers" msgstr "" +"unidad que describe la cantidad de producto vendido (por ejemplo, 2 " +"kilogramos, 5 litros, 3 artículos, 5 metros) para los clientes" #: src/components/product/ProductForm.tsx:160 #, c-format msgid "" "sale price for customers, including taxes, for above units of the product" msgstr "" +"precio de venta para los clientes, impuestos incluidos, por encima de las " +"unidades del producto" #: src/components/product/ProductForm.tsx:164 #, c-format @@ -1437,16 +1450,18 @@ msgstr "Existencias" msgid "" "product inventory for products with finite supply (for internal use only)" msgstr "" +"inventario de productos para productos con suministro finito (sólo para uso " +"interno)" #: src/components/product/ProductForm.tsx:171 #, c-format msgid "taxes included in the product price, exposed to customers" -msgstr "" +msgstr "impuestos incluidos en el precio del producto, expuestos a los clientes" #: src/paths/instance/products/create/CreatePage.tsx:66 #, c-format msgid "Need to complete marked fields" -msgstr "" +msgstr "Necesita completar los campos marcados" #: src/paths/instance/products/create/index.tsx:51 #, c-format @@ -1461,7 +1476,7 @@ msgstr "Productos" #: src/paths/instance/products/list/Table.tsx:73 #, c-format msgid "add product to inventory" -msgstr "" +msgstr "añadir producto al inventario" #: src/paths/instance/products/list/Table.tsx:137 #, c-format @@ -1496,27 +1511,27 @@ msgstr "Actualizar" #: src/paths/instance/products/list/Table.tsx:260 #, c-format msgid "remove this product from the database" -msgstr "" +msgstr "eliminar este producto de la base de datos" #: src/paths/instance/products/list/Table.tsx:331 #, c-format msgid "update the product with new price" -msgstr "" +msgstr "actualizar el producto con el nuevo precio" #: src/paths/instance/products/list/Table.tsx:341 #, c-format msgid "update product with new price" -msgstr "" +msgstr "actualizar producto con nuevo precio" #: src/paths/instance/products/list/Table.tsx:399 #, c-format msgid "add more elements to the inventory" -msgstr "" +msgstr "añadir más elementos al inventario" #: src/paths/instance/products/list/Table.tsx:404 #, c-format msgid "report elements lost in the inventory" -msgstr "" +msgstr "informar de elementos perdidos en el inventario" #: src/paths/instance/products/list/Table.tsx:409 #, fuzzy, c-format @@ -1744,7 +1759,7 @@ msgstr "" #: src/paths/instance/reserves/list/Table.tsx:210 #, c-format msgid "authorize new tip from selected reserve" -msgstr "" +msgstr "autorizar nueva punta de reserva seleccionada" #: src/paths/instance/reserves/list/Table.tsx:237 #, fuzzy, c-format @@ -1780,32 +1795,32 @@ msgstr "no puede ser vacío" #: src/paths/instance/templates/create/CreatePage.tsx:100 #, c-format msgid "to short" -msgstr "" +msgstr "demasiado corta" #: src/paths/instance/templates/create/CreatePage.tsx:108 #, c-format msgid "just letters and numbers from 2 to 7" -msgstr "" +msgstr "sólo letras y números del 2 al 7" #: src/paths/instance/templates/create/CreatePage.tsx:110 #, c-format msgid "size of the key should be 32" -msgstr "" +msgstr "el tamaño de la clave debe ser 32" #: src/paths/instance/templates/create/CreatePage.tsx:137 #, c-format msgid "Identifier" -msgstr "" +msgstr "Identificador" #: src/paths/instance/templates/create/CreatePage.tsx:138 #, c-format msgid "Name of the template in URLs." -msgstr "" +msgstr "Nombre de la plantilla en las URL." #: src/paths/instance/templates/create/CreatePage.tsx:144 #, c-format msgid "Describe what this template stands for" -msgstr "" +msgstr "Describa lo que representa esta plantilla" #: src/paths/instance/templates/create/CreatePage.tsx:149 #, fuzzy, c-format @@ -1815,7 +1830,7 @@ msgstr "Estado de orden" #: src/paths/instance/templates/create/CreatePage.tsx:150 #, c-format msgid "If specified, this template will create order with the same summary" -msgstr "" +msgstr "Si se especifica, esta plantilla creará pedidos con el mismo resumen" #: src/paths/instance/templates/create/CreatePage.tsx:154 #, fuzzy, c-format @@ -1825,7 +1840,7 @@ msgstr "precio unitario" #: src/paths/instance/templates/create/CreatePage.tsx:155 #, c-format msgid "If specified, this template will create order with the same price" -msgstr "" +msgstr "Si se especifica, esta plantilla creará pedidos con el mismo precio" #: src/paths/instance/templates/create/CreatePage.tsx:159 #, c-format @@ -1835,7 +1850,7 @@ msgstr "Edad mínima" #: src/paths/instance/templates/create/CreatePage.tsx:161 #, c-format msgid "Is this contract restricted to some age?" -msgstr "" +msgstr "¿Este contrato está restringido a alguna edad?" #: src/paths/instance/templates/create/CreatePage.tsx:165 #, fuzzy, c-format @@ -1848,56 +1863,58 @@ msgid "" "How much time has the customer to complete the payment once the order was " "created." msgstr "" +"Cuánto tiempo tiene el cliente para completar el pago una vez creado el " +"pedido." #: src/paths/instance/templates/create/CreatePage.tsx:171 #, c-format msgid "Verification algorithm" -msgstr "" +msgstr "Algoritmo de verificación" #: src/paths/instance/templates/create/CreatePage.tsx:172 #, c-format msgid "Algorithm to use to verify transaction in offline mode" -msgstr "" +msgstr "Algoritmo a utilizar para verificar la transacción en modo offline" #: src/paths/instance/templates/create/CreatePage.tsx:180 #, c-format msgid "Point-of-sale key" -msgstr "" +msgstr "Clave punto de venta" #: src/paths/instance/templates/create/CreatePage.tsx:182 #, c-format msgid "Useful to validate the purchase" -msgstr "" +msgstr "Útil para validar la compra" #: src/paths/instance/templates/create/CreatePage.tsx:196 #, c-format msgid "generate random secret key" -msgstr "" +msgstr "generar clave secreta aleatoria" #: src/paths/instance/templates/create/CreatePage.tsx:203 #, c-format msgid "random" -msgstr "" +msgstr "aleatorio" #: src/paths/instance/templates/create/CreatePage.tsx:208 #, c-format msgid "show secret key" -msgstr "" +msgstr "mostrar clave secreta" #: src/paths/instance/templates/create/CreatePage.tsx:209 #, c-format msgid "hide secret key" -msgstr "" +msgstr "ocultar clave secreta" #: src/paths/instance/templates/create/CreatePage.tsx:216 #, c-format msgid "hide" -msgstr "" +msgstr "ocultar" #: src/paths/instance/templates/create/CreatePage.tsx:218 #, c-format msgid "show" -msgstr "" +msgstr "mostrar" #: src/paths/instance/templates/create/index.tsx:52 #, fuzzy, c-format @@ -1912,7 +1929,7 @@ msgstr "Login necesario" #: src/paths/instance/templates/use/UsePage.tsx:58 #, c-format msgid "Order summary is required" -msgstr "" +msgstr "Se requiere resumen del pedido" #: src/paths/instance/templates/use/UsePage.tsx:86 #, fuzzy, c-format @@ -1922,7 +1939,7 @@ msgstr "cargar viejas transferencias" #: src/paths/instance/templates/use/UsePage.tsx:108 #, c-format msgid "Amount of the order" -msgstr "" +msgstr "Importe del pedido" #: src/paths/instance/templates/use/UsePage.tsx:113 #, fuzzy, c-format @@ -1940,16 +1957,19 @@ msgid "" "Here you can specify a default value for fields that are not fixed. Default " "values can be edited by the customer before the payment." msgstr "" +"Aquí puede especificar un valor por defecto para los campos que no son " +"fijos. Los valores por defecto pueden ser editados por el cliente antes del " +"pago." #: src/paths/instance/templates/qr/QrPage.tsx:148 -#, fuzzy, c-format +#, c-format msgid "Fixed amount" -msgstr "Monto reembolzado" +msgstr "Importe fijo" #: src/paths/instance/templates/qr/QrPage.tsx:149 -#, fuzzy, c-format +#, c-format msgid "Default amount" -msgstr "Monto reembolzado" +msgstr "Importe por defecto" #: src/paths/instance/templates/qr/QrPage.tsx:161 #, fuzzy, c-format @@ -1959,27 +1979,27 @@ msgstr "Estado de orden" #: src/paths/instance/templates/qr/QrPage.tsx:177 #, c-format msgid "Print" -msgstr "" +msgstr "Imprimir" #: src/paths/instance/templates/qr/QrPage.tsx:184 #, c-format msgid "Setup TOTP" -msgstr "" +msgstr "Configurar TOTP" #: src/paths/instance/templates/list/Table.tsx:65 #, c-format msgid "Templates" -msgstr "" +msgstr "Plantillas" #: src/paths/instance/templates/list/Table.tsx:70 #, c-format msgid "add new templates" -msgstr "" +msgstr "añadir nuevas plantillas" #: src/paths/instance/templates/list/Table.tsx:142 #, c-format msgid "load more templates before the first one" -msgstr "" +msgstr "cargar más plantillas antes de la primera" #: src/paths/instance/templates/list/Table.tsx:146 #, fuzzy, c-format @@ -1989,12 +2009,12 @@ msgstr "cargar nuevas transferencias" #: src/paths/instance/templates/list/Table.tsx:181 #, c-format msgid "delete selected templates from the database" -msgstr "" +msgstr "eliminar las plantillas seleccionadas de la base de datos" #: src/paths/instance/templates/list/Table.tsx:188 #, c-format msgid "use template to create new order" -msgstr "" +msgstr "utilizar la plantilla para crear un nuevo pedido" #: src/paths/instance/templates/list/Table.tsx:195 #, fuzzy, c-format @@ -2004,7 +2024,7 @@ msgstr "No se pudo create el reembolso" #: src/paths/instance/templates/list/Table.tsx:210 #, c-format msgid "load more templates after the last one" -msgstr "" +msgstr "cargar más plantillas después de la última" #: src/paths/instance/templates/list/Table.tsx:214 #, fuzzy, c-format @@ -2039,27 +2059,27 @@ msgstr "deberían ser iguales" #: src/paths/instance/webhooks/create/CreatePage.tsx:85 #, c-format msgid "Webhook ID to use" -msgstr "" +msgstr "ID de webhook a utilizar" #: src/paths/instance/webhooks/create/CreatePage.tsx:89 #, c-format msgid "Event" -msgstr "" +msgstr "Evento" #: src/paths/instance/webhooks/create/CreatePage.tsx:90 #, c-format msgid "The event of the webhook: why the webhook is used" -msgstr "" +msgstr "El evento del webhook: por qué se utiliza el webhook" #: src/paths/instance/webhooks/create/CreatePage.tsx:94 #, c-format msgid "Method" -msgstr "" +msgstr "Método" #: src/paths/instance/webhooks/create/CreatePage.tsx:95 #, c-format msgid "Method used by the webhook" -msgstr "" +msgstr "Método utilizado por el webhook" #: src/paths/instance/webhooks/create/CreatePage.tsx:99 #, c-format @@ -2069,12 +2089,12 @@ msgstr "URL" #: src/paths/instance/webhooks/create/CreatePage.tsx:100 #, c-format msgid "URL of the webhook where the customer will be redirected" -msgstr "" +msgstr "URL del webhook al que se redirigirá al cliente" #: src/paths/instance/webhooks/create/CreatePage.tsx:104 #, c-format msgid "Header" -msgstr "" +msgstr "Cabecera" #: src/paths/instance/webhooks/create/CreatePage.tsx:106 #, c-format @@ -2084,7 +2104,7 @@ msgstr "" #: src/paths/instance/webhooks/create/CreatePage.tsx:111 #, c-format msgid "Body" -msgstr "" +msgstr "Cuerpo" #: src/paths/instance/webhooks/create/CreatePage.tsx:112 #, c-format @@ -2114,17 +2134,17 @@ msgstr "cargar nuevas ordenes" #: src/paths/instance/webhooks/list/Table.tsx:151 #, c-format msgid "Event type" -msgstr "" +msgstr "Tipo de evento" #: src/paths/instance/webhooks/list/Table.tsx:176 #, c-format msgid "delete selected webhook from the database" -msgstr "" +msgstr "eliminar el webhook seleccionado de la base de datos" #: src/paths/instance/webhooks/list/Table.tsx:198 #, c-format msgid "load more webhooks after the last one" -msgstr "" +msgstr "cargar más webhooks después del último" #: src/paths/instance/webhooks/list/Table.tsx:202 #, fuzzy, c-format @@ -2164,17 +2184,17 @@ msgstr "La URL no tiene el formato correcto" #: src/paths/instance/transfers/create/CreatePage.tsx:98 #, c-format msgid "Credited bank account" -msgstr "" +msgstr "Abono en cuenta bancaria" #: src/paths/instance/transfers/create/CreatePage.tsx:100 #, c-format msgid "Select one account" -msgstr "" +msgstr "Selecciona una cuenta" #: src/paths/instance/transfers/create/CreatePage.tsx:101 #, c-format msgid "Bank account of the merchant where the payment was received" -msgstr "" +msgstr "Cuenta bancaria del comerciante donde se recibió el pago" #: src/paths/instance/transfers/create/CreatePage.tsx:105 #, fuzzy, c-format @@ -2187,6 +2207,8 @@ msgid "" "unique identifier of the wire transfer used by the exchange, must be 52 " "characters long" msgstr "" +"identificador único de la transferencia utilizado por la bolsa, debe tener " +"52 caracteres" #: src/paths/instance/transfers/create/CreatePage.tsx:112 #, c-format @@ -2194,16 +2216,18 @@ msgid "" "Base URL of the exchange that made the transfer, should have been in the " "wire transfer subject" msgstr "" +"URL base de la bolsa que realizó la transferencia, debería haber estado en " +"el asunto de la transferencia bancaria" #: src/paths/instance/transfers/create/CreatePage.tsx:117 #, c-format msgid "Amount credited" -msgstr "" +msgstr "Monto abonado" #: src/paths/instance/transfers/create/CreatePage.tsx:118 #, c-format msgid "Actual amount that was wired to the merchant's bank account" -msgstr "" +msgstr "Monto real que se transfirió a la cuenta bancaria del comerciante" #: src/paths/instance/transfers/create/index.tsx:58 #, c-format @@ -2223,7 +2247,7 @@ msgstr "cargar nuevas transferencias" #: src/paths/instance/transfers/list/Table.tsx:129 #, c-format msgid "load more transfers before the first one" -msgstr "" +msgstr "cargar más transferencias antes de la primera" #: src/paths/instance/transfers/list/Table.tsx:133 #, c-format @@ -2293,7 +2317,7 @@ msgstr "Dirección de cuenta" #: src/paths/instance/transfers/list/ListPage.tsx:100 #, c-format msgid "only show wire transfers confirmed by the merchant" -msgstr "" +msgstr "mostrar sólo las transferencias confirmadas por el comerciante" #: src/paths/instance/transfers/list/ListPage.tsx:110 #, c-format @@ -2308,7 +2332,7 @@ msgstr "Verificado" #: src/paths/admin/create/CreatePage.tsx:69 #, c-format msgid "is not valid" -msgstr "" +msgstr "no es válido" #: src/paths/admin/create/CreatePage.tsx:94 #, fuzzy, c-format @@ -2398,12 +2422,12 @@ msgstr "Dirección de cuenta" #: src/components/form/InputPaytoForm.tsx:273 #, c-format msgid "Business Identifier Code." -msgstr "" +msgstr "Código de identificación de la empresa." #: src/components/form/InputPaytoForm.tsx:282 #, c-format msgid "Bank Account Number." -msgstr "" +msgstr "Número de cuenta bancaria." #: src/components/form/InputPaytoForm.tsx:292 #, c-format @@ -2413,17 +2437,17 @@ msgstr "Interfaz de pago unificado." #: src/components/form/InputPaytoForm.tsx:301 #, c-format msgid "Bitcoin protocol." -msgstr "" +msgstr "Protocolo Bitcoin." #: src/components/form/InputPaytoForm.tsx:310 #, c-format msgid "Ethereum protocol." -msgstr "" +msgstr "Protocolo Ethereum." #: src/components/form/InputPaytoForm.tsx:319 #, c-format msgid "Interledger protocol." -msgstr "" +msgstr "Protocolo Interledger." #: src/components/form/InputPaytoForm.tsx:328 #, c-format @@ -2438,17 +2462,17 @@ msgstr "" #: src/components/form/InputPaytoForm.tsx:334 #, c-format msgid "Bank account." -msgstr "" +msgstr "Cuenta bancaria." #: src/components/form/InputPaytoForm.tsx:343 #, c-format msgid "Bank account owner's name." -msgstr "" +msgstr "Nombre del titular de la cuenta bancaria." #: src/components/form/InputPaytoForm.tsx:370 #, c-format msgid "No accounts yet." -msgstr "" +msgstr "Aún no hay cuentas." #: src/components/instance/DefaultInstanceFormFields.tsx:52 #, c-format @@ -2456,6 +2480,8 @@ msgid "" "Name of the instance in URLs. The 'default' instance is special in that it " "is used to administer other instances." msgstr "" +"Nombre de la instancia en URL. La instancia \"por defecto\" es especial, ya " +"que se utiliza para administrar otras instancias." #: src/components/instance/DefaultInstanceFormFields.tsx:58 #, fuzzy, c-format @@ -2465,7 +2491,7 @@ msgstr "Nombre de edificio" #: src/components/instance/DefaultInstanceFormFields.tsx:59 #, c-format msgid "Legal name of the business represented by this instance." -msgstr "" +msgstr "Nombre legal de la empresa representada por esta instancia." #: src/components/instance/DefaultInstanceFormFields.tsx:64 #, c-format @@ -2485,17 +2511,17 @@ msgstr "URL de sitio web" #: src/components/instance/DefaultInstanceFormFields.tsx:71 #, c-format msgid "URL." -msgstr "" +msgstr "URL." #: src/components/instance/DefaultInstanceFormFields.tsx:76 #, c-format msgid "Logo" -msgstr "" +msgstr "Logotipo" #: src/components/instance/DefaultInstanceFormFields.tsx:77 #, c-format msgid "Logo image." -msgstr "" +msgstr "Imagen del logotipo." #: src/components/instance/DefaultInstanceFormFields.tsx:82 #, c-format @@ -2505,7 +2531,7 @@ msgstr "Cuenta bancaria" #: src/components/instance/DefaultInstanceFormFields.tsx:83 #, c-format msgid "URI specifying bank account for crediting revenue." -msgstr "" +msgstr "URI que especifica la cuenta bancaria para acreditar los ingresos." #: src/components/instance/DefaultInstanceFormFields.tsx:88 #, c-format @@ -2517,6 +2543,8 @@ msgstr "Impuesto máximo de deposito por omisión" msgid "" "Maximum deposit fees this merchant is willing to pay per order by default." msgstr "" +"Comisiones de depósito máximas que este comerciante está dispuesto a pagar " +"por pedido por defecto." #: src/components/instance/DefaultInstanceFormFields.tsx:94 #, c-format @@ -2529,6 +2557,8 @@ msgid "" "Maximum wire fees this merchant is willing to pay per wire transfer by " "default." msgstr "" +"Comisiones de transferencia máximas que este comerciante está dispuesto a " +"pagar por transferencia por defecto." #: src/components/instance/DefaultInstanceFormFields.tsx:100 #, c-format @@ -2541,11 +2571,13 @@ msgid "" "Number of orders excess wire transfer fees will be divided by to compute per " "order surcharge." msgstr "" +"El número de pedidos que excedan las tarifas de transferencia bancaria se " +"dividirá para calcular el recargo por pedido." #: src/components/instance/DefaultInstanceFormFields.tsx:107 #, c-format msgid "Physical location of the merchant." -msgstr "" +msgstr "Ubicación física del comerciante." #: src/components/instance/DefaultInstanceFormFields.tsx:114 #, c-format @@ -2567,6 +2599,8 @@ msgstr "Retrazo de pago por omisión" msgid "" "Time customers have to pay an order before the offer expires by default." msgstr "" +"Tiempo que los clientes tienen para pagar un pedido antes de que caduque la " +"oferta de forma predeterminada." #: src/components/instance/DefaultInstanceFormFields.tsx:129 #, c-format @@ -2580,6 +2614,10 @@ msgid "" "enabling it to aggregate smaller payments into larger wire transfers and " "reducing wire fees." msgstr "" +"Tiempo máximo que se le permite a un intercambio retrasar la transferencia " +"de fondos al comerciante, lo que le permite agregar pagos más pequeños en " +"transferencias electrónicas más grandes y reducir las tarifas de " +"transferencia." #: src/paths/instance/update/UpdatePage.tsx:164 #, c-format @@ -2610,7 +2648,7 @@ msgstr "Login necesario" #: src/components/exception/login.tsx:80 #, c-format msgid "Please enter your access token." -msgstr "" +msgstr "Por favor, introduzca su clave de acceso." #: src/components/exception/login.tsx:108 #, fuzzy, c-format @@ -2620,7 +2658,7 @@ msgstr "Acceso denegado" #: src/InstanceRoutes.tsx:171 #, c-format msgid "The request to the backend take too long and was cancelled" -msgstr "" +msgstr "La petición al backend tardó demasiado y fue cancelada" #: src/InstanceRoutes.tsx:172 #, c-format diff --git a/packages/pogen/package.json b/packages/pogen/package.json index ac57a41f0..f57dbad40 100644 --- a/packages/pogen/package.json +++ b/packages/pogen/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/pogen", - "version": "0.12.1", + "version": "0.12.2", "bin": { "pogen": "bin/pogen" }, diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog index e088d2992..64f750458 100644 --- a/packages/taler-harness/debian/changelog +++ b/packages/taler-harness/debian/changelog @@ -1,3 +1,9 @@ +taler-harness (0.12.2) unstable; urgency=low + + * Release 0.12.2 + + -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200 + taler-harness (0.12.1) unstable; urgency=low * Release 0.12.1 diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json index 3b59339a6..02bebcfce 100644 --- a/packages/taler-harness/package.json +++ b/packages/taler-harness/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-harness", - "version": "0.12.1", + "version": "0.12.2", "description": "", "engines": { "node": ">=0.12.0" diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts new file mode 100644 index 000000000..3cd02882b --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts @@ -0,0 +1,101 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + TransactionMajorState, + TransactionMinorState, + TransactionType, + WithdrawalType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; + +/** + * Test for a withdrawal that is externally confirmed. + */ +export async function runWithdrawalExternalTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); + + // Create a withdrawal operation + + const bankUser = await bankClient.createRandomBankUser(); + bankClient.setAuth(bankUser); + const wop = await bankClient.createWithdrawalOperation( + bankUser.username, + "TESTKUDOS:10", + ); + + const talerWithdrawUri = wop.taler_withdraw_uri + "?external-confirmation=1"; + + // Hand it to the wallet + + const detResp = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: talerWithdrawUri, + }, + ); + + const acceptResp = await walletClient.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: detResp.defaultExchangeBaseUrl!!, + talerWithdrawUri, + }, + ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: acceptResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BankConfirmTransfer, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: acceptResp.transactionId, + }, + ); + + // Now we check that the external-confirmation=1 flag actually did something! + + t.assertDeepEqual(txDetails.type, TransactionType.Withdrawal); + t.assertDeepEqual( + txDetails.withdrawalDetails.type, + WithdrawalType.TalerBankIntegrationApi, + ); + t.assertDeepEqual(txDetails.withdrawalDetails.externalConfirmation, true); + t.assertDeepEqual(txDetails.withdrawalDetails.bankConfirmationUrl, undefined); + + t.logStep("confirming withdrawal operation"); + + await bankClient.confirmWithdrawalOperation(bankUser.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); +} + +runWithdrawalExternalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 238bf3b98..eb71396e7 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -108,6 +108,7 @@ import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js"; import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js"; import { runWalletGenDbTest } from "./test-wallet-gendb.js"; import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js"; +import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js"; import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWalletObservabilityTest } from "./test-wallet-observability.js"; import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js"; @@ -118,13 +119,13 @@ import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js"; import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js"; import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js"; +import { runWithdrawalExternalTest } from "./test-withdrawal-external.js"; import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js"; import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js"; import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; -import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js"; /** * Test runner. @@ -240,6 +241,7 @@ const allTests: TestMainFunction[] = [ runWithdrawalFlexTest, runExchangeMasterPubChangeTest, runMerchantCategoriesTest, + runWithdrawalExternalTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json index 11213cdfa..c165489b3 100644 --- a/packages/taler-util/package.json +++ b/packages/taler-util/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-util", - "version": "0.12.1", + "version": "0.12.2", "description": "Generic helper functionality for GNU Taler", "type": "module", "types": "./lib/index.node.d.ts", diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts index e7b128b02..6a920749c 100644 --- a/packages/taler-util/src/http-client/challenger.ts +++ b/packages/taler-util/src/http-client/challenger.ts @@ -164,8 +164,6 @@ export class ChallengerHttpClient { } case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: @@ -205,7 +203,7 @@ export class ChallengerHttpClient { case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); + return opKnownAlternativeFailure(resp, HttpStatusCode.Forbidden, codecForChallengeInvalidPinResponse()); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index 6e758773c..3816b1598 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse = .property("name", codecForConstString("challenger")) .property("version", codecForString()) .property("implementation", codecOptional(codecForString())) + .property("restrictions", codecOptional(codecForMap(codecForAny()))) + .property( + "address_type", + codecForEither( + codecForConstString("phone"), + codecForConstString("email"), + ), + ) .build("ChallengerApi.ChallengerTermsOfServiceResponse"); export const codecForChallengeSetupResponse = @@ -1578,10 +1586,13 @@ export const codecForChallengeSetupResponse = export const codecForChallengeStatus = (): Codec<ChallengerApi.ChallengeStatus> => buildCodecForObject<ChallengerApi.ChallengeStatus>() - .property("restrictions", codecOptional(codecForMap(codecForAny()))) .property("fix_address", codecForBoolean()) + .property("solved", codecForBoolean()) .property("last_address", codecOptional(codecForMap(codecForAny()))) .property("changes_left", codecForNumber()) + .property("retransmission_time", codecForTimestamp) + .property("pin_transmissions_left", codecForNumber()) + .property("auth_attempts_left", codecForNumber()) .build("ChallengerApi.ChallengeStatus"); export const codecForChallengeResponse = @@ -1596,10 +1607,10 @@ export const codecForChallengeCreateResponse = (): Codec<ChallengerApi.ChallengeCreateResponse> => buildCodecForObject<ChallengerApi.ChallengeCreateResponse>() .property("attempts_left", codecForNumber()) - .property("address", codecForAny()) .property("type", codecForConstString("created")) + .property("address", codecForAny()) .property("transmitted", codecForBoolean()) - .property("next_tx_time", codecForString()) + .property("retransmission_time", codecForTimestamp) .build("ChallengerApi.ChallengeCreateResponse"); export const codecForChallengeRedirect = @@ -5385,6 +5396,19 @@ export namespace ChallengerApi { // URN of the implementation (needed to interpret 'revision' in version). // @since v0, may become mandatory in the future. implementation?: string; + + // Object; map of keys (names of the fields of the address + // to be entered by the user) to objects with a "regex" (string) + // containing an extended Posix regular expression for allowed + // address field values, and a "hint"/"hint_i18n" giving a + // human-readable explanation to display if the value entered + // by the user does not match the regex. Keys that are not mapped + // to such an object have no restriction on the value provided by + // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. + restrictions: Record<string, Restriction> | undefined; + + // @since v2. + address_type: "email" | "phone"; } export interface ChallengeSetupResponse { @@ -5399,16 +5423,6 @@ export namespace ChallengerApi { } export interface ChallengeStatus { - // Object; map of keys (names of the fields of the address - // to be entered by the user) to objects with a "regex" (string) - // containing an extended Posix regular expression for allowed - // address field values, and a "hint"/"hint_i18n" giving a - // human-readable explanation to display if the value entered - // by the user does not match the regex. Keys that are not mapped - // to such an object have no restriction on the value provided by - // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. - restrictions: Record<string, Restriction> | undefined; - // indicates if the given address cannot be changed anymore, the // form should be read-only if set to true. fix_address: boolean; @@ -5420,6 +5434,25 @@ export namespace ChallengerApi { // number of times the address can still be changed, may or may not be // shown to the user changes_left: Integer; + + // is the challenge already solved? + solved: boolean; + + // when we would re-transmit the challenge the next + // time (at the earliest) if requested by the user + // only present if challenge already created + // @since v2 + retransmission_time: Timestamp; + + // how many times might the PIN still be retransmitted + // only present if challenge already created + // @since v2 + pin_transmissions_left: Integer; + + // how many times might the user still try entering the PIN code + // only present if challenge already created + // @since v2 + auth_attempts_left: Integer; } export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse; @@ -5447,7 +5480,7 @@ export namespace ChallengerApi { // timestamp explaining when we would re-transmit the challenge the next // time (at the earliest) if requested by the user - next_tx_time: string; + retransmission_time: TalerProtocolTimestamp; } export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse; diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts index e2ab9d4e4..2d17238dc 100644 --- a/packages/taler-util/src/operation.ts +++ b/packages/taler-util/src/operation.ts @@ -146,7 +146,10 @@ export function opKnownTalerFailure<T extends TalerErrorCode>( return { type: "fail", case: s, detail }; } -export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never { +export function opUnknownFailure( + resp: HttpResponse, + error: TalerErrorDetail, +): never { throw TalerError.fromDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, { @@ -179,15 +182,51 @@ export function narrowOpSuccessOrThrow<Body, ErrorEnum>( } } +export async function succeedOrThrow<R, E>( + promise: Promise<OperationResult<R, E>>, +): Promise<R> { + const resp = await promise; + if (isOperationOk(resp)) { + return resp.body; + } + + if (isOperationFail(resp)) { + throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any); + } + throw TalerError.fromException(resp); +} + +export async function failOrThrow<E>( + s: E, + promise: Promise<OperationResult<unknown, E>>, +): Promise<TalerErrorDetail | undefined> { + const resp = await promise; + if (isOperationOk(resp)) { + throw TalerError.fromException( + new Error(`request succeed but failure "${s}" was expected`), + ); + } + if (isOperationFail(resp) && resp.case === s) { + return resp.detail; + } + throw TalerError.fromException( + new Error( + `request failed with "${JSON.stringify( + resp, + )}" but case "${s}" was expected`, + ), + ); +} + export type ResultByMethod< TT extends object, p extends keyof TT, > = TT[p] extends (...args: any[]) => infer Ret ? Ret extends Promise<infer Result> - ? Result extends OperationResult<any, any> - ? Result - : never - : never //api always use Promises + ? Result extends OperationResult<any, any> + ? Result + : never + : never //api always use Promises : never; //error cases just for functions export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude< @@ -195,4 +234,4 @@ export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude< OperationOk<any> >; -export type RedirectResult = { redirectURL: URL } +export type RedirectResult = { redirectURL: URL }; diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts index 372291250..4d90ccf14 100644 --- a/packages/taler-util/src/qr.ts +++ b/packages/taler-util/src/qr.ts @@ -34,6 +34,9 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { return { type: "skip" }; } const amountStr = parsedPayto.params["amount"]; + if (amountStr === undefined) { + return { type: "skip" }; + } const iban = parsedPayto.targetPath; const countryCode = iban.slice(0, 2); const lines = [ @@ -105,7 +108,9 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult { "", // optional BIC parsedPayto.params["receiver-name"], // Beneficiary name parsedPayto.targetPath, // Beneficiary IBAN - `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}`, // Amount + amountStr !== undefined + ? `${Amounts.currencyOf(amountStr)}${Amounts.stringifyValue(amountStr, 2)}` + : "", // Amount (optional) "", // AT-44 Purpose parsedPayto.params["message"], // AT-05 Unstructured remittance information ]; diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 66f98ea9a..ac42ca278 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -723,6 +723,8 @@ export class ExchangeKeysJson { currency: string; + currency_specification?: CurrencySpecification; + /** * The exchange's master public key. */ @@ -1504,6 +1506,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> => buildCodecForObject<ExchangeKeysJson>() .property("base_url", codecForString()) .property("currency", codecForString()) + .property("currency_specification", codecOptional(codecForCurrencySpecificiation())) .property("master_public_key", codecForString()) .property("auditors", codecForList(codecForAuditor())) .property("list_issue_date", codecForTimestamp) diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index b92366fb3..d80470dab 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -54,6 +54,18 @@ test("taler withdraw uri parsing", (t) => { t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/"); }); +test("taler withdraw uri parsing with external confirmation", (t) => { + const url1 = "taler://withdraw/bank.example.com/12345?external-confirmation=1"; + const r1 = parseWithdrawUri(url1); + if (!r1) { + t.fail(); + return; + } + t.is(r1.externalConfirmation, true); + t.is(r1.withdrawalOperationId, "12345"); + t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/"); +}); + test("taler withdraw uri parsing (http)", (t) => { const url1 = "taler+http://withdraw/bank.example.com/12345"; const r1 = parseWithdrawUri(url1); diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index 54b7525e3..d3186d2f5 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -29,6 +29,7 @@ import { opFixedSuccess, opKnownTalerFailure } from "./operation.js"; import { TalerErrorCode } from "./taler-error-codes.js"; import { AmountString } from "./taler-types.js"; import { URL, URLSearchParams } from "./url.js"; + /** * A parsed taler URI. */ @@ -89,6 +90,7 @@ export interface WithdrawUriResult { type: TalerUriAction.Withdraw; bankIntegrationApiBaseUrl: string; withdrawalOperationId: string; + externalConfirmation?: boolean; } export interface RefundUriResult { @@ -140,7 +142,12 @@ export function parseWithdrawUriWithError(s: string) { if (pi.type === "fail") { return pi; } - const parts = pi.body.rest.split("/"); + + const c = pi.body.rest.split("?", 2); + const path = c[0]; + const q = new URLSearchParams(c[1] ?? ""); + + const parts = path.split("/"); if (parts.length < 2) { return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, { @@ -166,6 +173,7 @@ export function parseWithdrawUriWithError(s: string) { `${pi.body.innerProto}://${p}/`, ), withdrawalOperationId: withdrawId, + externalConfirmation: q.get("external-confirmation") == "1", }; return opFixedSuccess(result); } diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts index a6ac5aec6..b4e2738ee 100644 --- a/packages/taler-util/src/transactions-types.ts +++ b/packages/taler-util/src/transactions-types.ts @@ -299,6 +299,11 @@ interface WithdrawalDetailsForTalerBankIntegrationApi { */ reserveIsReady: boolean; + /** + * Is the bank transfer for the withdrawal externally confirmed? + */ + externalConfirmation?: boolean; + exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[]; } diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 2c92d9295..ec401f3f6 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -558,11 +558,13 @@ export enum ScopeType { } export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string }; + export type ScopeInfoExchange = { type: ScopeType.Exchange; currency: string; url: string; }; + export type ScopeInfoAuditor = { type: ScopeType.Auditor; currency: string; @@ -571,6 +573,22 @@ export type ScopeInfoAuditor = { export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor; +/** + * Encode scope info as a string. + * + * Format must be stable as it's used in the database. + */ +export function stringifyScopeInfo(si: ScopeInfo): string { + switch (si.type) { + case ScopeType.Global: + return `taler-si:global/${si.currency}}`; + case ScopeType.Auditor: + return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`; + case ScopeType.Exchange: + return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`; + } +} + export interface BalancesResponse { balances: WalletBalance[]; } @@ -3439,3 +3457,9 @@ export const codecForGetQrCodesForPaytoRequest = () => export interface GetQrCodesForPaytoResponse { codes: QrCodeSpec[]; } + +export type EmptyObject = Record<string, never>; + +export const codecForEmptyObject = (): Codec<EmptyObject> => + buildCodecForObject<EmptyObject>() + .build("EmptyObject");
\ No newline at end of file diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog index fde3084fa..7ef8445dc 100644 --- a/packages/taler-wallet-cli/debian/changelog +++ b/packages/taler-wallet-cli/debian/changelog @@ -1,14 +1,14 @@ -taler-wallet-cli (0.12.1) unstable; urgency=low +taler-wallet-cli (0.12.2) unstable; urgency=low - * Release 0.12.1 + * Release 0.12.2 - -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600 + -- Florian Dold <dold@taler.net> Thu, 27 Jun 2024 20:19:19 +0200 -taler-wallet-cli (v0.12.1) unstable; urgency=low +taler-wallet-cli (0.12.1) unstable; urgency=low - * Release v0.12.1 + * Release 0.12.1 - -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:20 -0600 + -- Florian Dold <dold@taler.net> Wed, 26 Jun 2024 09:30:32 -0600 taler-wallet-cli (0.12.0) unstable; urgency=low diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json index f04fa43fa..3430d525d 100644 --- a/packages/taler-wallet-cli/package.json +++ b/packages/taler-wallet-cli/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-wallet-cli", - "version": "0.12.1", + "version": "0.12.2", "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index c165b548c..be74e464b 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -1380,13 +1380,8 @@ advancedCli advancedCli .subcommand("pending", "pending", { help: "Show pending operations." }) .action(async (args) => { - await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { - const pending = await wallet.client.call( - WalletApiOperation.GetPendingOperations, - {}, - ); - console.log(JSON.stringify(pending, undefined, 2)); - }); + console.error("Subcommand removed due to deprecation."); + process.exit(1); }); advancedCli diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json index ff068806a..273ad75f6 100644 --- a/packages/taler-wallet-core/package.json +++ b/packages/taler-wallet-core/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-wallet-core", - "version": "0.12.1", + "version": "0.12.2", "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 3438cbdc7..336ffab67 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -40,6 +40,7 @@ import { CoinPublicKeyString, CoinRefreshRequest, CoinStatus, + CurrencySpecification, DenomLossEventType, DenomSelectionState, DenominationInfo, @@ -51,6 +52,7 @@ import { HashCodeString, Logger, RefreshReason, + ScopeInfo, TalerErrorDetail, TalerPreciseTimestamp, TalerProtocolDuration, @@ -61,6 +63,7 @@ import { WireInfo, WithdrawalExchangeAccountDetails, codecForAny, + stringifyScopeInfo, } from "@gnu-taler/taler-util"; import { DbRetryInfo, TaskIdentifiers } from "./common.js"; import { @@ -400,6 +403,8 @@ export interface ReserveBankInfo { wireTypes: string[] | undefined; currency: string | undefined; + + externalConfirmation?: boolean; } /** @@ -907,8 +912,8 @@ export interface CoinRecord { /** * History item for a coin. - * - * DB-specific format, + * + * DB-specific format, */ export type DbWalletCoinHistoryItem = | { @@ -2368,6 +2373,23 @@ export interface DenomLossEventRecord { exchangeBaseUrl: string; } +export interface CurrencyInfoRecord { + /** + * Stringified scope info. + */ + scopeInfoStr: string; + + /** + * Currency specification. + */ + currencySpec: CurrencySpecification; + + /** + * How did the currency info get set? + */ + source: "exchange" | "user" | "preset"; +} + /** * Schema definition for the IndexedDB * wallet database. @@ -2402,6 +2424,11 @@ export const WalletStoresV1 = { }), }, }), + currencyInfo: describeStoreV2({ + recordCodec: passthroughCodec<CurrencyInfoRecord>(), + storeName: "currencyInfo", + keyPath: "scopeInfoStr", + }), globalCurrencyAuditors: describeStoreV2({ recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(), storeName: "globalCurrencyAuditors", @@ -3360,3 +3387,75 @@ export async function deleteTalerDatabase( req.onsuccess = () => resolve(); }); } + +/** + * High-level helpers to access the database. + * Eventually all access to the database should + * go through helpers in this namespace. + */ +export namespace WalletDbHelpers { + export interface GetCurrencyInfoDbResult { + /** + * Currency specification. + */ + currencySpec: CurrencySpecification; + + /** + * How did the currency info get set? + */ + source: "exchange" | "user" | "preset"; + } + + export interface StoreCurrencyInfoDbRequest { + scopeInfo: ScopeInfo; + currencySpec: CurrencySpecification; + source: "exchange" | "user" | "preset"; + } + + export async function getCurrencyInfo( + tx: WalletDbReadOnlyTransaction<["currencyInfo"]>, + scopeInfo: ScopeInfo, + ): Promise<GetCurrencyInfoDbResult | undefined> { + const s = stringifyScopeInfo(scopeInfo); + const res = await tx.currencyInfo.get(s); + if (!res) { + return undefined; + } + return { + currencySpec: res.currencySpec, + source: res.source, + }; + } + + /** + * Store currency info for a scope. + * + * Overrides existing currency infos. + */ + export async function upsertCurrencyInfo( + tx: WalletDbReadWriteTransaction<["currencyInfo"]>, + req: StoreCurrencyInfoDbRequest, + ): Promise<void> { + await tx.currencyInfo.put({ + scopeInfoStr: stringifyScopeInfo(req.scopeInfo), + currencySpec: req.currencySpec, + source: req.source, + }); + } + + export async function insertCurrencyInfoUnlessExists( + tx: WalletDbReadWriteTransaction<["currencyInfo"]>, + req: StoreCurrencyInfoDbRequest, + ): Promise<void> { + const scopeInfoStr = stringifyScopeInfo(req.scopeInfo); + const oldRec = await tx.currencyInfo.get(scopeInfoStr); + if (oldRec) { + return; + } + await tx.currencyInfo.put({ + scopeInfoStr: stringifyScopeInfo(req.scopeInfo), + currencySpec: req.currencySpec, + source: req.source, + }); + } +} diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index 8fa439715..ab3f95214 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -31,6 +31,7 @@ import { CancellationToken, CoinRefreshRequest, CoinStatus, + CurrencySpecification, DeleteExchangeRequest, DenomKeyType, DenomLossEventType, @@ -124,6 +125,7 @@ import { ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, ExchangeEntryRecord, + WalletDbHelpers, WalletDbReadOnlyTransaction, WalletDbReadWriteTransaction, WalletStoresV1, @@ -710,6 +712,7 @@ export interface ExchangeKeysDownloadResult { globalFees: GlobalFees[]; accounts: ExchangeWireAccount[]; wireFees: { [methodName: string]: WireFeesJson[] }; + currencySpecification?: CurrencySpecification; } /** @@ -872,6 +875,7 @@ async function downloadExchangeKeysInfo( globalFees: exchangeKeysJsonUnchecked.global_fees, accounts: exchangeKeysJsonUnchecked.accounts, wireFees: exchangeKeysJsonUnchecked.wire_fees, + currencySpecification: exchangeKeysJsonUnchecked.currency_specification, }; } @@ -1470,6 +1474,7 @@ export async function updateExchangeFromUrlHandler( "recoupGroups", "coinAvailability", "denomLossEvents", + "currencyInfo", ], }, async (tx) => { @@ -1575,6 +1580,19 @@ export async function updateExchangeFromUrlHandler( r.updateStatus = ExchangeEntryDbUpdateStatus.Ready; r.cachebreakNextUpdate = false; await tx.exchanges.put(r); + + if (keysInfo.currencySpecification) { + await WalletDbHelpers.insertCurrencyInfoUnlessExists(tx, { + currencySpec: keysInfo.currencySpecification, + scopeInfo: { + type: ScopeType.Exchange, + currency: newDetails.currency, + url: exchangeBaseUrl, + }, + source: "exchange", + }); + } + const drRowId = await tx.exchangeDetails.put(newDetails); checkDbInvariant( typeof drRowId.key === "number", diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts index 8268828be..0649f9ce2 100644 --- a/packages/taler-wallet-core/src/transactions.ts +++ b/packages/taler-wallet-core/src/transactions.ts @@ -768,7 +768,10 @@ function buildTransactionForBankIntegratedWithdraw( confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false, exchangeCreditAccountDetails: wg.wgInfo.exchangeCreditAccounts, reservePub: wg.reservePub, - bankConfirmationUrl: wg.wgInfo.bankInfo.confirmUrl, + bankConfirmationUrl: wg.wgInfo.bankInfo.externalConfirmation + ? undefined + : wg.wgInfo.bankInfo.confirmUrl, + externalConfirmation: wg.wgInfo.bankInfo.externalConfirmation, reserveIsReady: wg.status === WithdrawalGroupStatus.Done || wg.status === WithdrawalGroupStatus.PendingReady, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index ce8be2927..cd17bc8cd 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -59,6 +59,7 @@ import { DeleteExchangeRequest, DeleteStoredBackupRequest, DeleteTransactionRequest, + EmptyObject, ExchangeDetailedResponse, ExchangesListResponse, ExchangesShortListResponse, @@ -202,7 +203,6 @@ export enum WalletApiOperation { GetUserAttentionRequests = "getUserAttentionRequests", GetUserAttentionUnreadCount = "getUserAttentionUnreadCount", MarkAttentionRequestAsRead = "markAttentionRequestAsRead", - GetPendingOperations = "getPendingOperations", GetActiveTasks = "getActiveTasks", SetExchangeTosAccepted = "setExchangeTosAccepted", SetExchangeTosForgotten = "setExchangeTosForgotten", @@ -287,8 +287,6 @@ export enum WalletApiOperation { // group: Initialization -type EmptyObject = Record<string, never>; - /** * Initialize wallet-core. * @@ -1142,17 +1140,6 @@ export type GetUserAttentionsUnreadCount = { response: UserAttentionsCountResponse; }; -/** - * Get wallet-internal pending tasks. - * - * @deprecated - */ -export type GetPendingTasksOp = { - op: WalletApiOperation.GetPendingOperations; - request: EmptyObject; - response: any; -}; - export type GetActiveTasksOp = { op: WalletApiOperation.GetActiveTasks; request: EmptyObject; @@ -1301,7 +1288,6 @@ export type WalletOperations = { [WalletApiOperation.GetTransactionById]: GetTransactionByIdOp; [WalletApiOperation.GetWithdrawalTransactionByUri]: GetWithdrawalTransactionByUriOp; [WalletApiOperation.RetryPendingNow]: RetryPendingNowOp; - [WalletApiOperation.GetPendingOperations]: GetPendingTasksOp; [WalletApiOperation.GetActiveTasks]: GetActiveTasksOp; [WalletApiOperation.GetUserAttentionRequests]: GetUserAttentionRequests; [WalletApiOperation.GetUserAttentionUnreadCount]: GetUserAttentionsUnreadCount; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 4e5fdab71..536f559d4 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -24,24 +24,48 @@ */ import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge"; import { + AbortTransactionRequest, AbsoluteTime, + AcceptManualWithdrawalRequest, + AcceptManualWithdrawalResult, ActiveTask, + AddExchangeRequest, + AddKnownBankAccountsRequest, AmountJson, AmountString, Amounts, CancellationToken, CoinDumpJson, CoinStatus, + ConfirmPayRequest, + ConfirmPayResult, CoreApiResponse, CreateStoredBackupResponse, DeleteStoredBackupRequest, DenominationInfo, Duration, + EmptyObject, ExchangesShortListResponse, + FailTransactionRequest, + ForgetKnownBankAccountsRequest, + GetActiveTasksResponse, + GetContractTermsDetailsRequest, + GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, + GetDepositWireTypesForCurrencyRequest, + GetDepositWireTypesForCurrencyResponse, + GetExchangeTosRequest, + GetExchangeTosResult, + GetQrCodesForPaytoRequest, + GetQrCodesForPaytoResponse, + HintNetworkAvailabilityRequest, + InitRequest, InitResponse, + IntegrationTestArgs, + IntegrationTestV2Args, KnownBankAccounts, KnownBankAccountsInfo, + ListExchangesForScopedCurrencyRequest, ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, Logger, @@ -54,22 +78,34 @@ import { PrepareWithdrawExchangeRequest, PrepareWithdrawExchangeResponse, RecoverStoredBackupRequest, + SharePaymentRequest, + SharePaymentResult, + StartRefundQueryRequest, StoredBackupList, + SuspendTransactionRequest, TalerBankIntegrationHttpClient, TalerError, TalerErrorCode, TalerProtocolTimestamp, TalerUriAction, + TestingGetDenomStatsRequest, TestingGetDenomStatsResponse, + TestingGetReserveHistoryRequest, + TestingListTasksForTransactionRequest, TestingListTasksForTransactionsResponse, TestingWaitTransactionRequest, TimerAPI, TimerGroup, TransactionType, + TransactionsResponse, + UpdateExchangeEntryRequest, + ValidateIbanRequest, ValidateIbanResponse, + WalletContractData, WalletCoreVersion, WalletNotification, WalletRunConfig, + WithdrawTestBalanceRequest, canonicalizeBaseUrl, checkDbInvariant, codecForAbortTransaction, @@ -95,6 +131,7 @@ import { codecForDeleteExchangeRequest, codecForDeleteStoredBackupRequest, codecForDeleteTransactionRequest, + codecForEmptyObject, codecForFailTransactionRequest, codecForForceRefreshRequest, codecForForgetKnownBankAccounts, @@ -156,7 +193,6 @@ import { parseTalerUri, performanceNow, safeStringifyException, - sampleWalletCoreTransactions, setDangerousTimetravel, validateIban, } from "@gnu-taler/taler-util"; @@ -170,6 +206,7 @@ import { markAttentionRequestAsRead, } from "./attention.js"; import { + RunBackupCycleRequest, addBackupProvider, codecForAddBackupProviderRequest, codecForRemoveBackupProvider, @@ -191,6 +228,7 @@ import { CoinSourceType, ConfigRecordKey, DenominationRecord, + WalletDbHelpers, WalletDbReadOnlyTransaction, WalletStoresV1, clearDatabase, @@ -680,16 +718,493 @@ async function handlePrepareWithdrawExchange( }; } -/** - * Response returned from the pending operations API. - * - * @deprecated this is a placeholder for the response type of a deprecated wallet-core request. - */ -export interface PendingOperationsResponse { - /** - * List of pending operations. - */ - pendingOperations: any[]; +async function handleRetryPendingNow( + wex: WalletExecutionContext, +): Promise<EmptyObject> { + logger.error("retryPendingNow currently not implemented"); + return {}; +} + +async function handleSharePayment( + wex: WalletExecutionContext, + req: SharePaymentRequest, +): Promise<SharePaymentResult> { + return await sharePayment(wex, req.merchantBaseUrl, req.orderId); +} + +async function handleDeleteStoredBackup( + wex: WalletExecutionContext, + req: DeleteStoredBackupRequest, +): Promise<EmptyObject> { + await deleteStoredBackup(wex, req); + return {}; +} + +async function handleRecoverStoredBackup( + wex: WalletExecutionContext, + req: RecoverStoredBackupRequest, +): Promise<EmptyObject> { + await recoverStoredBackup(wex, req); + return {}; +} + +async function handleSetWalletRunConfig( + wex: WalletExecutionContext, + req: InitRequest, +) { + if (logger.shouldLogTrace()) { + const initType = wex.ws.initCalled + ? "repeat initialization" + : "first initialization"; + logger.trace(`init request (${initType}): ${j2s(req)}`); + } + + // Write to the DB to make sure that we're failing early in + // case the DB is not writeable. + try { + await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => { + tx.config.put({ + key: ConfigRecordKey.LastInitInfo, + value: timestampProtocolToDb(TalerProtocolTimestamp.now()), + }); + }); + } catch (e) { + logger.error("error writing to database during initialization"); + throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, { + innerError: getErrorDetailFromException(e), + }); + } + wex.ws.initWithConfig(applyRunConfigDefaults(req.config)); + + if (wex.ws.config.testing.skipDefaults) { + logger.trace("skipping defaults"); + } else { + logger.trace("filling defaults"); + await fillDefaults(wex); + } + const resp: InitResponse = { + versionInfo: handleGetVersion(wex), + }; + + if (req.config?.lazyTaskLoop) { + logger.trace("lazily starting task loop"); + } else { + await wex.taskScheduler.ensureRunning(); + } + + wex.ws.initCalled = true; + return resp; +} + +async function handleWithdrawTestkudos(wex: WalletExecutionContext) { + await withdrawTestBalance(wex, { + amount: "TESTKUDOS:10" as AmountString, + corebankApiBaseUrl: "https://bank.test.taler.net/", + exchangeBaseUrl: "https://exchange.test.taler.net/", + }); + // FIXME: Is this correct? + return { + versionInfo: handleGetVersion(wex), + }; +} + +async function handleWithdrawTestBalance( + wex: WalletExecutionContext, + req: WithdrawTestBalanceRequest, +): Promise<EmptyObject> { + await withdrawTestBalance(wex, req); + return {}; +} + +async function handleTestingListTasksForTransaction( + wex: WalletExecutionContext, + req: TestingListTasksForTransactionRequest, +): Promise<TestingListTasksForTransactionsResponse> { + return { + taskIdList: listTaskForTransactionId(req.transactionId), + }; +} + +async function handleRunIntegrationTest( + wex: WalletExecutionContext, + req: IntegrationTestArgs, +): Promise<EmptyObject> { + await runIntegrationTest(wex, req); + return {}; +} + +async function handleRunIntegrationTestV2( + wex: WalletExecutionContext, + req: IntegrationTestV2Args, +): Promise<EmptyObject> { + await runIntegrationTest2(wex, req); + return {}; +} + +async function handleValidateIban( + wex: WalletExecutionContext, + req: ValidateIbanRequest, +): Promise<ValidateIbanResponse> { + const valRes = validateIban(req.iban); + const resp: ValidateIbanResponse = { + valid: valRes.type === "valid", + }; + return resp; +} + +async function handleAddExchange( + wex: WalletExecutionContext, + req: AddExchangeRequest, +): Promise<EmptyObject> { + await fetchFreshExchange(wex, req.exchangeBaseUrl, {}); + return {}; +} + +async function handleUpdateExchangeEntry( + wex: WalletExecutionContext, + req: UpdateExchangeEntryRequest, +): Promise<EmptyObject> { + await fetchFreshExchange(wex, req.exchangeBaseUrl, { + forceUpdate: !!req.force, + }); + return {}; +} + +async function handleTestingGetDenomStats( + wex: WalletExecutionContext, + req: TestingGetDenomStatsRequest, +): Promise<TestingGetDenomStatsResponse> { + const denomStats: TestingGetDenomStatsResponse = { + numKnown: 0, + numLost: 0, + numOffered: 0, + }; + await wex.db.runReadOnlyTx({ storeNames: ["denominations"] }, async (tx) => { + const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll( + req.exchangeBaseUrl, + ); + for (const d of denoms) { + denomStats.numKnown++; + if (d.isOffered) { + denomStats.numOffered++; + } + if (d.isLost) { + denomStats.numLost++; + } + } + }); + return denomStats; +} + +async function handleListExchangesForScopedCurrency( + wex: WalletExecutionContext, + req: ListExchangesForScopedCurrencyRequest, +): Promise<ExchangesShortListResponse> { + const exchangesResp = await listExchanges(wex); + const result: ExchangesShortListResponse = { + exchanges: [], + }; + // Right now we only filter on the currency, as wallet-core doesn't + // fully support scoped currencies yet. + for (const exch of exchangesResp.exchanges) { + if (exch.currency === req.scope.currency) { + result.exchanges.push({ + exchangeBaseUrl: exch.exchangeBaseUrl, + }); + } + } + return result; +} + +async function handleAddKnownBankAccount( + wex: WalletExecutionContext, + req: AddKnownBankAccountsRequest, +): Promise<EmptyObject> { + await addKnownBankAccounts(wex, req.payto, req.alias, req.currency); + return {}; +} + +async function handleForgetKnownBankAccounts( + wex: WalletExecutionContext, + req: ForgetKnownBankAccountsRequest, +): Promise<EmptyObject> { + await forgetKnownBankAccounts(wex, req.payto); + return {}; +} + +// FIXME: Doesn't have proper type! +async function handleTestingGetReserveHistory( + wex: WalletExecutionContext, + req: TestingGetReserveHistoryRequest, +): Promise<any> { + const reserve = await wex.db.runReadOnlyTx( + { storeNames: ["reserves"] }, + async (tx) => { + return tx.reserves.indexes.byReservePub.get(req.reservePub); + }, + ); + if (!reserve) { + throw Error("no reserve pub found"); + } + const sigResp = await wex.cryptoApi.signReserveHistoryReq({ + reservePriv: reserve.reservePriv, + startOffset: 0, + }); + const exchangeBaseUrl = req.exchangeBaseUrl; + const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl); + const resp = await wex.http.fetch(url.href, { + headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, + }); + const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); + return historyJson; +} + +async function handleAcceptManualWithdrawal( + wex: WalletExecutionContext, + req: AcceptManualWithdrawalRequest, +): Promise<AcceptManualWithdrawalResult> { + const res = await createManualWithdrawal(wex, { + amount: Amounts.parseOrThrow(req.amount), + exchangeBaseUrl: req.exchangeBaseUrl, + restrictAge: req.restrictAge, + forceReservePriv: req.forceReservePriv, + }); + return res; +} + +async function handleGetExchangeTos( + wex: WalletExecutionContext, + req: GetExchangeTosRequest, +): Promise<GetExchangeTosResult> { + return getExchangeTos( + wex, + req.exchangeBaseUrl, + req.acceptedFormat, + req.acceptLanguage, + ); +} + +async function handleGetContractTermsDetails( + wex: WalletExecutionContext, + req: GetContractTermsDetailsRequest, +): Promise<WalletContractData> { + if (req.proposalId) { + // FIXME: deprecated path + return getContractTermsDetails(wex, req.proposalId); + } + if (req.transactionId) { + const parsedTx = parseTransactionIdentifier(req.transactionId); + if (parsedTx?.tag === TransactionType.Payment) { + return getContractTermsDetails(wex, parsedTx.proposalId); + } + throw Error("transactionId is not a payment transaction"); + } + throw Error("transactionId missing"); +} + +async function handleGetQrCodesForPayto( + wex: WalletExecutionContext, + req: GetQrCodesForPaytoRequest, +): Promise<GetQrCodesForPaytoResponse> { + return { + codes: getQrCodesForPayto(req.paytoUri), + }; +} + +async function handleConfirmPay( + wex: WalletExecutionContext, + req: ConfirmPayRequest, +): Promise<ConfirmPayResult> { + let transactionId; + if (req.proposalId) { + // legacy client support + transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId: req.proposalId, + }); + } else if (req.transactionId) { + transactionId = req.transactionId; + } else { + throw Error("transactionId or (deprecated) proposalId required"); + } + return await confirmPay(wex, transactionId, req.sessionId); +} + +async function handleAbortTransaction( + wex: WalletExecutionContext, + req: AbortTransactionRequest, +): Promise<EmptyObject> { + await abortTransaction(wex, req.transactionId); + return {}; +} + +async function handleSuspendTransaction( + wex: WalletExecutionContext, + req: SuspendTransactionRequest, +): Promise<EmptyObject> { + await suspendTransaction(wex, req.transactionId); + return {}; +} + +async function handleGetActiveTasks( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<GetActiveTasksResponse> { + const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; + + const tasksInfo = await Promise.all( + allTasksId.map(async (id) => { + return await wex.db.runReadOnlyTx( + { storeNames: ["operationRetries"] }, + async (tx) => { + return tx.operationRetries.get(id); + }, + ); + }), + ); + + const tasks = allTasksId.map((taskId, i): ActiveTask => { + const transaction = convertTaskToTransactionId(taskId); + const d = tasksInfo[i]; + + const firstTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.firstTry); + const nextTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.nextRetry); + const counter = d?.retryInfo.retryCounter; + const lastError = d?.lastError; + + return { + taskId: taskId, + retryCounter: counter, + firstTry, + nextTry, + lastError, + transaction, + }; + }); + return { tasks }; +} + +async function handleFailTransaction( + wex: WalletExecutionContext, + req: FailTransactionRequest, +): Promise<EmptyObject> { + await failTransaction(wex, req.transactionId); + return {}; +} + +async function handleTestingGetSampleTransactions( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<TransactionsResponse> { + // FIXME! + return { transactions: [] }; + // These are out of date! + //return { transactions: sampleWalletCoreTransactions }; +} + +async function handleStartRefundQuery( + wex: WalletExecutionContext, + req: StartRefundQueryRequest, +): Promise<EmptyObject> { + const txIdParsed = parseTransactionIdentifier(req.transactionId); + if (!txIdParsed) { + throw Error("invalid transaction ID"); + } + if (txIdParsed.tag !== TransactionType.Payment) { + throw Error("expected payment transaction ID"); + } + await startQueryRefund(wex, txIdParsed.proposalId); + return {}; +} + +async function handleAddBackupProvider( + wex: WalletExecutionContext, + req: RunBackupCycleRequest, +): Promise<EmptyObject> { + await runBackupCycle(wex, req); + return {}; +} + +async function handleHintNetworkAvailability( + wex: WalletExecutionContext, + req: HintNetworkAvailabilityRequest, +): Promise<EmptyObject> { + wex.ws.networkAvailable = req.isNetworkAvailable; + // When network becomes available, restart tasks as they're blocked + // waiting for the network. + // When network goes down, restart tasks so they notice the network + // is down and wait. + await restartAllRunningTasks(wex); + return {}; +} + +async function handleGetDepositWireTypesForCurrency( + wex: WalletExecutionContext, + req: GetDepositWireTypesForCurrencyRequest, +): Promise<GetDepositWireTypesForCurrencyResponse> { + const wtSet: Set<string> = new Set(); + await wex.db.runReadOnlyTx( + { storeNames: ["exchanges", "exchangeDetails"] }, + async (tx) => { + const exchanges = await tx.exchanges.getAll(); + for (const exchange of exchanges) { + const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl); + if (!det) { + continue; + } + if (det.currency !== req.currency) { + continue; + } + for (const acc of det.wireInfo.accounts) { + let usable = true; + for (const dr of acc.debit_restrictions) { + if (dr.type === "deny") { + usable = false; + break; + } + } + if (!usable) { + break; + } + const parsedPayto = parsePaytoUri(acc.payto_uri); + if (!parsedPayto) { + continue; + } + wtSet.add(parsedPayto.targetType); + } + } + }, + ); + return { + wireTypes: [...wtSet], + }; +} + +async function handleListGlobalCurrencyExchanges( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<ListGlobalCurrencyExchangesResponse> { + const resp: ListGlobalCurrencyExchangesResponse = { + exchanges: [], + }; + await wex.db.runReadOnlyTx( + { storeNames: ["globalCurrencyExchanges"] }, + async (tx) => { + const gceList = await tx.globalCurrencyExchanges.iter().toArray(); + for (const gce of gceList) { + resp.exchanges.push({ + currency: gce.currency, + exchangeBaseUrl: gce.exchangeBaseUrl, + exchangeMasterPub: gce.exchangeMasterPub, + }); + } + }, + ); + return resp; } /** @@ -710,105 +1225,45 @@ async function dispatchRequestInternal( // definitions we already have? switch (operation) { case WalletApiOperation.CreateStoredBackup: - return createStoredBackup(wex); + return await createStoredBackup(wex); case WalletApiOperation.DeleteStoredBackup: { const req = codecForDeleteStoredBackupRequest().decode(payload); - await deleteStoredBackup(wex, req); - return {}; + return await handleDeleteStoredBackup(wex, req); } case WalletApiOperation.ListStoredBackups: return listStoredBackups(wex); case WalletApiOperation.RecoverStoredBackup: { const req = codecForRecoverStoredBackupRequest().decode(payload); - await recoverStoredBackup(wex, req); - return {}; + return await handleRecoverStoredBackup(wex, req); } case WalletApiOperation.SetWalletRunConfig: case WalletApiOperation.InitWallet: { const req = codecForInitRequest().decode(payload); - - if (logger.shouldLogTrace()) { - const initType = wex.ws.initCalled - ? "repeat initialization" - : "first initialization"; - logger.trace(`init request (${initType}): ${j2s(req)}`); - } - - // Write to the DB to make sure that we're failing early in - // case the DB is not writeable. - try { - await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => { - tx.config.put({ - key: ConfigRecordKey.LastInitInfo, - value: timestampProtocolToDb(TalerProtocolTimestamp.now()), - }); - }); - } catch (e) { - logger.error("error writing to database during initialization"); - throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, { - innerError: getErrorDetailFromException(e), - }); - } - wex.ws.initWithConfig(applyRunConfigDefaults(req.config)); - - if (wex.ws.config.testing.skipDefaults) { - logger.trace("skipping defaults"); - } else { - logger.trace("filling defaults"); - await fillDefaults(wex); - } - const resp: InitResponse = { - versionInfo: getVersion(wex), - }; - - if (req.config?.lazyTaskLoop) { - logger.trace("lazily starting task loop"); - } else { - await wex.taskScheduler.ensureRunning(); - } - - wex.ws.initCalled = true; - return resp; + return await handleSetWalletRunConfig(wex, req); } case WalletApiOperation.WithdrawTestkudos: { - await withdrawTestBalance(wex, { - amount: "TESTKUDOS:10" as AmountString, - corebankApiBaseUrl: "https://bank.test.taler.net/", - exchangeBaseUrl: "https://exchange.test.taler.net/", - }); - return { - versionInfo: getVersion(wex), - }; + return await handleWithdrawTestkudos(wex); } case WalletApiOperation.WithdrawTestBalance: { const req = codecForWithdrawTestBalance().decode(payload); - await withdrawTestBalance(wex, req); - return {}; + return await handleWithdrawTestBalance(wex, req); } case WalletApiOperation.TestingListTaskForTransaction: { const req = codecForTestingListTasksForTransactionRequest().decode(payload); - return { - taskIdList: listTaskForTransactionId(req.transactionId), - } satisfies TestingListTasksForTransactionsResponse; + return await handleTestingListTasksForTransaction(wex, req); } case WalletApiOperation.RunIntegrationTest: { const req = codecForIntegrationTestArgs().decode(payload); - await runIntegrationTest(wex, req); - return {}; + return await handleRunIntegrationTest(wex, req); } case WalletApiOperation.RunIntegrationTestV2: { const req = codecForIntegrationTestV2Args().decode(payload); - await runIntegrationTest2(wex, req); - return {}; + return await handleRunIntegrationTestV2(wex, req); } case WalletApiOperation.ValidateIban: { const req = codecForValidateIbanRequest().decode(payload); - const valRes = validateIban(req.iban); - const resp: ValidateIbanResponse = { - valid: valRes.type === "valid", - }; - return resp; + return handleValidateIban(wex, req); } case WalletApiOperation.TestPay: { const req = codecForTestPayArgs().decode(payload); @@ -828,45 +1283,18 @@ async function dispatchRequestInternal( } case WalletApiOperation.AddExchange: { const req = codecForAddExchangeRequest().decode(payload); - await fetchFreshExchange(wex, req.exchangeBaseUrl, {}); - return {}; + return await handleAddExchange(wex, req); } case WalletApiOperation.TestingPing: { return {}; } case WalletApiOperation.UpdateExchangeEntry: { const req = codecForUpdateExchangeEntryRequest().decode(payload); - await fetchFreshExchange(wex, req.exchangeBaseUrl, { - forceUpdate: !!req.force, - }); - return {}; + return await handleUpdateExchangeEntry(wex, req); } case WalletApiOperation.TestingGetDenomStats: { const req = codecForTestingGetDenomStatsRequest().decode(payload); - const denomStats: TestingGetDenomStatsResponse = { - numKnown: 0, - numLost: 0, - numOffered: 0, - }; - await wex.db.runReadOnlyTx( - { storeNames: ["denominations"] }, - async (tx) => { - const denoms = - await tx.denominations.indexes.byExchangeBaseUrl.getAll( - req.exchangeBaseUrl, - ); - for (const d of denoms) { - denomStats.numKnown++; - if (d.isOffered) { - denomStats.numOffered++; - } - if (d.isLost) { - denomStats.numLost++; - } - } - }, - ); - return denomStats; + return handleTestingGetDenomStats(wex, req); } case WalletApiOperation.ListExchanges: { return await listExchanges(wex); @@ -878,20 +1306,7 @@ async function dispatchRequestInternal( case WalletApiOperation.ListExchangesForScopedCurrency: { const req = codecForListExchangesForScopedCurrencyRequest().decode(payload); - const exchangesResp = await listExchanges(wex); - const result: ExchangesShortListResponse = { - exchanges: [], - }; - // Right now we only filter on the currency, as wallet-core doesn't - // fully support scoped currencies yet. - for (const exch of exchangesResp.exchanges) { - if (exch.currency === req.scope.currency) { - result.exchanges.push({ - exchangeBaseUrl: exch.exchangeBaseUrl, - }); - } - } - return result; + return await handleListExchangesForScopedCurrency(wex, req); } case WalletApiOperation.GetExchangeDetailedInfo: { const req = codecForAddExchangeRequest().decode(payload); @@ -903,13 +1318,11 @@ async function dispatchRequestInternal( } case WalletApiOperation.AddKnownBankAccounts: { const req = codecForAddKnownBankAccounts().decode(payload); - await addKnownBankAccounts(wex, req.payto, req.alias, req.currency); - return {}; + return await handleAddKnownBankAccount(wex, req); } case WalletApiOperation.ForgetKnownBankAccounts: { const req = codecForForgetKnownBankAccounts().decode(payload); - await forgetKnownBankAccounts(wex, req.payto); - return {}; + return await handleForgetKnownBankAccounts(wex, req); } case WalletApiOperation.GetWithdrawalDetailsForUri: { const req = codecForGetWithdrawalDetailsForUri().decode(payload); @@ -917,48 +1330,16 @@ async function dispatchRequestInternal( } case WalletApiOperation.TestingGetReserveHistory: { const req = codecForTestingGetReserveHistoryRequest().decode(payload); - const reserve = await wex.db.runReadOnlyTx( - { storeNames: ["reserves"] }, - async (tx) => { - return tx.reserves.indexes.byReservePub.get(req.reservePub); - }, - ); - if (!reserve) { - throw Error("no reserve pub found"); - } - const sigResp = await wex.cryptoApi.signReserveHistoryReq({ - reservePriv: reserve.reservePriv, - startOffset: 0, - }); - const exchangeBaseUrl = req.exchangeBaseUrl; - const url = new URL( - `reserves/${req.reservePub}/history`, - exchangeBaseUrl, - ); - const resp = await wex.http.fetch(url.href, { - headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, - }); - const historyJson = await readSuccessResponseJsonOrThrow( - resp, - codecForAny(), - ); - return historyJson; + return await handleTestingGetReserveHistory(wex, req); } case WalletApiOperation.AcceptManualWithdrawal: { const req = codecForAcceptManualWithdrawalRequest().decode(payload); - const res = await createManualWithdrawal(wex, { - amount: Amounts.parseOrThrow(req.amount), - exchangeBaseUrl: req.exchangeBaseUrl, - restrictAge: req.restrictAge, - forceReservePriv: req.forceReservePriv, - }); - return res; + return await handleAcceptManualWithdrawal(wex, req); } case WalletApiOperation.GetWithdrawalDetailsForAmount: { const req = codecForGetWithdrawalDetailsForAmountRequest().decode(payload); - const resp = await getWithdrawalDetailsForAmount(wex, cts, req); - return resp; + return await getWithdrawalDetailsForAmount(wex, cts, req); } case WalletApiOperation.GetBalances: { return await getBalances(wex); @@ -979,12 +1360,6 @@ async function dispatchRequestInternal( const req = codecForUserAttentionsRequest().decode(payload); return await getUserAttentionsUnreadCount(wex, req); } - case WalletApiOperation.GetPendingOperations: { - // FIXME: Eventually remove the handler after deprecation period. - return { - pendingOperations: [], - } satisfies PendingOperationsResponse; - } case WalletApiOperation.SetExchangeTosAccepted: { const req = codecForAcceptExchangeTosRequest().decode(payload); await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl); @@ -1017,39 +1392,22 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetExchangeTos: { const req = codecForGetExchangeTosRequest().decode(payload); - return getExchangeTos( - wex, - req.exchangeBaseUrl, - req.acceptedFormat, - req.acceptLanguage, - ); + return await handleGetExchangeTos(wex, req); } case WalletApiOperation.GetContractTermsDetails: { const req = codecForGetContractTermsDetails().decode(payload); - if (req.proposalId) { - // FIXME: deprecated path - return getContractTermsDetails(wex, req.proposalId); - } - if (req.transactionId) { - const parsedTx = parseTransactionIdentifier(req.transactionId); - if (parsedTx?.tag === TransactionType.Payment) { - return getContractTermsDetails(wex, parsedTx.proposalId); - } - throw Error("transactionId is not a payment transaction"); - } - throw Error("transactionId missing"); + return handleGetContractTermsDetails(wex, req); } case WalletApiOperation.RetryPendingNow: { - logger.error("retryPendingNow currently not implemented"); - return {}; + return handleRetryPendingNow(wex); } case WalletApiOperation.SharePayment: { const req = codecForSharePaymentRequest().decode(payload); - return await sharePayment(wex, req.merchantBaseUrl, req.orderId); + return await handleSharePayment(wex, req); } case WalletApiOperation.PrepareWithdrawExchange: { const req = codecForPrepareWithdrawExchangeRequest().decode(payload); - return handlePrepareWithdrawExchange(wex, req); + return await handlePrepareWithdrawExchange(wex, req); } case WalletApiOperation.CheckPayForTemplate: { const req = codecForCheckPayTemplateRequest().decode(payload); @@ -1065,78 +1423,26 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetQrCodesForPayto: { const req = codecForGetQrCodesForPaytoRequest().decode(payload); - return { - codes: getQrCodesForPayto(req.paytoUri), - }; + return handleGetQrCodesForPayto(wex, req); } case WalletApiOperation.ConfirmPay: { const req = codecForConfirmPayRequest().decode(payload); - let transactionId; - if (req.proposalId) { - // legacy client support - transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId: req.proposalId, - }); - } else if (req.transactionId) { - transactionId = req.transactionId; - } else { - throw Error("transactionId or (deprecated) proposalId required"); - } - return await confirmPay(wex, transactionId, req.sessionId); + return handleConfirmPay(wex, req); } case WalletApiOperation.AbortTransaction: { const req = codecForAbortTransaction().decode(payload); - await abortTransaction(wex, req.transactionId); - return {}; + return handleAbortTransaction(wex, req); } case WalletApiOperation.SuspendTransaction: { const req = codecForSuspendTransaction().decode(payload); - await suspendTransaction(wex, req.transactionId); - return {}; + return handleSuspendTransaction(wex, req); } case WalletApiOperation.GetActiveTasks: { - const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; - - const tasksInfo = await Promise.all( - allTasksId.map(async (id) => { - return await wex.db.runReadOnlyTx( - { storeNames: ["operationRetries"] }, - async (tx) => { - return tx.operationRetries.get(id); - }, - ); - }), - ); - - const tasks = allTasksId.map((taskId, i): ActiveTask => { - const transaction = convertTaskToTransactionId(taskId); - const d = tasksInfo[i]; - - const firstTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.firstTry); - const nextTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.nextRetry); - const counter = d?.retryInfo.retryCounter; - const lastError = d?.lastError; - - return { - taskId: taskId, - retryCounter: counter, - firstTry, - nextTry, - lastError, - transaction, - }; - }); - return { tasks }; + return await handleGetActiveTasks(wex, {}); } case WalletApiOperation.FailTransaction: { const req = codecForFailTransactionRequest().decode(payload); - await failTransaction(wex, req.transactionId); - return {}; + return await handleFailTransaction(wex, req); } case WalletApiOperation.ResumeTransaction: { const req = codecForResumeTransaction().decode(payload); @@ -1152,7 +1458,8 @@ async function dispatchRequestInternal( return {}; } case WalletApiOperation.TestingGetSampleTransactions: - return { transactions: sampleWalletCoreTransactions }; + const req = codecForEmptyObject().decode(payload); + return handleTestingGetSampleTransactions(wex, req); case WalletApiOperation.ForceRefresh: { const req = codecForForceRefreshRequest().decode(payload); return await forceRefresh(wex, req); @@ -1163,15 +1470,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.StartRefundQuery: { const req = codecForStartRefundQueryRequest().decode(payload); - const txIdParsed = parseTransactionIdentifier(req.transactionId); - if (!txIdParsed) { - throw Error("invalid transaction ID"); - } - if (txIdParsed.tag !== TransactionType.Payment) { - throw Error("expected payment transaction ID"); - } - await startQueryRefund(wex, txIdParsed.proposalId); - return {}; + return handleStartRefundQuery(wex, req); } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); @@ -1179,8 +1478,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.RunBackupCycle: { const req = codecForRunBackupCycle().decode(payload); - await runBackupCycle(wex, req); - return {}; + return handleAddBackupProvider(wex, req); } case WalletApiOperation.RemoveBackupProvider: { const req = codecForRemoveBackupProvider().decode(payload); @@ -1197,48 +1495,8 @@ async function dispatchRequestInternal( return {}; } case WalletApiOperation.GetCurrencySpecification: { - // Ignore result, just validate in this mock implementation const req = codecForGetCurrencyInfoRequest().decode(payload); - // Hard-coded mock for KUDOS and TESTKUDOS - if (req.scope.currency === "KUDOS") { - const kudosResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: "Kudos (Taler Demonstrator)", - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": "ク", - }, - }, - }; - return kudosResp; - } else if (req.scope.currency === "TESTKUDOS") { - const testkudosResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: "Test (Taler Unstable Demonstrator)", - num_fractional_input_digits: 0, - num_fractional_normal_digits: 0, - num_fractional_trailing_zero_digits: 0, - alt_unit_names: { - "0": "テ", - }, - }, - }; - return testkudosResp; - } - const defaultResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: req.scope.currency, - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": req.scope.currency, - }, - }, - }; - return defaultResp; + return handleGetCurrencySpecification(wex, req); } case WalletApiOperation.ImportBackupRecovery: { const req = codecForAny().decode(payload); @@ -1247,13 +1505,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.HintNetworkAvailability: { const req = codecForHintNetworkAvailabilityRequest().decode(payload); - wex.ws.networkAvailable = req.isNetworkAvailable; - // When network becomes available, restart tasks as they're blocked - // waiting for the network. - // When network goes down, restart tasks so they notice the network - // is down and wait. - await restartAllRunningTasks(wex); - return {}; + return await handleHintNetworkAvailability(wex, req); } case WalletApiOperation.ConvertDepositAmount: { const req = codecForConvertAmountRequest.decode(payload); @@ -1325,61 +1577,11 @@ async function dispatchRequestInternal( case WalletApiOperation.GetDepositWireTypesForCurrency: { const req = codecForGetDepositWireTypesForCurrencyRequest().decode(payload); - const wtSet: Set<string> = new Set(); - await wex.db.runReadOnlyTx( - { storeNames: ["exchanges", "exchangeDetails"] }, - async (tx) => { - const exchanges = await tx.exchanges.getAll(); - for (const exchange of exchanges) { - const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl); - if (!det) { - continue; - } - if (det.currency !== req.currency) { - continue; - } - for (const acc of det.wireInfo.accounts) { - let usable = true; - for (const dr of acc.debit_restrictions) { - if (dr.type === "deny") { - usable = false; - break; - } - } - if (!usable) { - break; - } - const parsedPayto = parsePaytoUri(acc.payto_uri); - if (!parsedPayto) { - continue; - } - wtSet.add(parsedPayto.targetType); - } - } - }, - ); - return { - wireTypes: [...wtSet], - }; + return handleGetDepositWireTypesForCurrency(wex, req); } case WalletApiOperation.ListGlobalCurrencyExchanges: { - const resp: ListGlobalCurrencyExchangesResponse = { - exchanges: [], - }; - await wex.db.runReadOnlyTx( - { storeNames: ["globalCurrencyExchanges"] }, - async (tx) => { - const gceList = await tx.globalCurrencyExchanges.iter().toArray(); - for (const gce of gceList) { - resp.exchanges.push({ - currency: gce.currency, - exchangeBaseUrl: gce.exchangeBaseUrl, - exchangeMasterPub: gce.exchangeMasterPub, - }); - } - }, - ); - return resp; + const req = codecForEmptyObject().decode(payload); + return await handleListGlobalCurrencyExchanges(wex, req); } case WalletApiOperation.ListGlobalCurrencyAuditors: { const resp: ListGlobalCurrencyAuditorsResponse = { @@ -1554,7 +1756,7 @@ async function dispatchRequestInternal( return {}; } case WalletApiOperation.GetVersion: { - return getVersion(wex); + return handleGetVersion(wex); } case WalletApiOperation.TestingWaitTransactionsFinal: return await waitUntilAllTransactionsFinal(wex); @@ -1626,7 +1828,66 @@ async function dispatchRequestInternal( ); } -export function getVersion(wex: WalletExecutionContext): WalletCoreVersion { +export async function handleGetCurrencySpecification( + wex: WalletExecutionContext, + req: GetCurrencySpecificationRequest, +): Promise<GetCurrencySpecificationResponse> { + const spec = await wex.db.runReadOnlyTx( + { + storeNames: ["currencyInfo"], + }, + async (tx) => { + return WalletDbHelpers.getCurrencyInfo(tx, req.scope); + }, + ); + if (spec) { + return { + currencySpecification: spec.currencySpec, + }; + } + // Hard-coded mock for KUDOS and TESTKUDOS + if (req.scope.currency === "KUDOS") { + const kudosResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: "Kudos (Taler Demonstrator)", + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": "ク", + }, + }, + }; + return kudosResp; + } else if (req.scope.currency === "TESTKUDOS") { + const testkudosResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: "Test (Taler Unstable Demonstrator)", + num_fractional_input_digits: 0, + num_fractional_normal_digits: 0, + num_fractional_trailing_zero_digits: 0, + alt_unit_names: { + "0": "テ", + }, + }, + }; + return testkudosResp; + } + const defaultResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: req.scope.currency, + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": req.scope.currency, + }, + }, + }; + return defaultResp; +} + +function handleGetVersion(wex: WalletExecutionContext): WalletCoreVersion { const result: WalletCoreVersion = { implementationSemver: walletCoreBuildInfo.implementationSemver, implementationGitHash: walletCoreBuildInfo.implementationGitHash, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 087db7938..083fa2a2d 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -64,6 +64,7 @@ import { TalerErrorCode, TalerErrorDetail, TalerPreciseTimestamp, + TalerUriAction, Transaction, TransactionAction, TransactionIdStr, @@ -95,6 +96,7 @@ import { getRandomBytes, j2s, makeErrorDetail, + parseTalerUri, parseWithdrawUri, } from "@gnu-taler/taler-util"; import { @@ -3064,6 +3066,17 @@ export async function prepareBankIntegratedWithdrawal( }, ); + const parsedUri = parseTalerUri(req.talerWithdrawUri); + if (parsedUri?.type !== TalerUriAction.Withdraw) { + throw TalerError.fromDetail(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {}); + } + + const externalConfirmation = parsedUri.externalConfirmation; + + logger.info( + `creating withdrawal with externalConfirmation=${externalConfirmation}`, + ); + const withdrawInfo = await getBankWithdrawalInfo( wex.http, req.talerWithdrawUri, @@ -3099,6 +3112,7 @@ export async function prepareBankIntegratedWithdrawal( timestampReserveInfoPosted: undefined, wireTypes: withdrawInfo.wireTypes, currency: withdrawInfo.currency, + externalConfirmation, }, }, reserveStatus: WithdrawalGroupStatus.DialogProposed, @@ -3220,22 +3234,14 @@ export async function confirmWithdrawal( rec.denomsSel = initalDenoms; rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost; rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue; - - rec.wgInfo = { - withdrawalType: WithdrawalRecordType.BankIntegrated, - exchangeCreditAccounts: withdrawalAccountList, - bankInfo: { - exchangePaytoUri, - talerWithdrawUri, - confirmUrl: confirmUrl, - timestampBankConfirmed: undefined, - timestampReserveInfoPosted: undefined, - wireTypes: bankWireTypes, - currency: bankCurrency, - }, - }; - pending = true; + checkDbInvariant( + rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated, + "withdrawal type mismatch", + ); + rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList; + rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri; rec.status = WithdrawalGroupStatus.PendingRegisteringBank; + pending = true; return TransitionResult.transition(rec); } default: { diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json index df91d5829..dbdb23673 100644 --- a/packages/taler-wallet-embedded/package.json +++ b/packages/taler-wallet-embedded/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-wallet-embedded", - "version": "0.12.1", + "version": "0.12.2", "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json index f80786e7c..01248e964 100644 --- a/packages/taler-wallet-webextension/manifest-common.json +++ b/packages/taler-wallet-webextension/manifest-common.json @@ -2,7 +2,7 @@ "name": "GNU Taler Wallet (git)", "description": "Privacy preserving and transparent payments", "author": "GNU Taler Developers", - "version": "0.12.1", + "version": "0.12.2", "icons": { "16": "static/img/taler-logo-16.png", "19": "static/img/taler-logo-19.png", @@ -14,5 +14,5 @@ "256": "static/img/taler-logo-256.png", "512": "static/img/taler-logo-512.png" }, - "version_name": "0.12.1" + "version_name": "0.12.2" } diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 9acc0d0d4..5c622da70 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/taler-wallet-webextension", - "version": "0.12.1", + "version": "0.12.2", "description": "GNU Taler Wallet browser extension", "main": "./build/index.js", "types": "./build/index.d.ts", diff --git a/packages/web-util/package.json b/packages/web-util/package.json index ae7d98819..fe8a4a3f7 100644 --- a/packages/web-util/package.json +++ b/packages/web-util/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/web-util", - "version": "0.12.1", + "version": "0.12.2", "description": "Generic helper functionality for GNU Taler Web Apps", "type": "module", "types": "./lib/index.node.d.ts", |