taler-typescript-core

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

commit 4ab6829caf8b013231cff6f11494bd1b8b8954ca
parent 3916386eb2235d9e998dabb31458b437dafa4f59
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 15 Oct 2025 15:02:27 -0300

fixing payto parse

Diffstat:
Mpackages/bank-ui/src/Routing.tsx | 3---
Mpackages/bank-ui/src/components/Transactions/state.ts | 18+++++-------------
Mpackages/bank-ui/src/context/wallet-integration.ts | 5+++--
Mpackages/bank-ui/src/pages/AccountPage/state.ts | 13+++++++------
Mpackages/bank-ui/src/pages/OperationState/index.ts | 3++-
Mpackages/bank-ui/src/pages/OperationState/state.ts | 55+++++++++++--------------------------------------------
Mpackages/bank-ui/src/pages/OperationState/views.tsx | 38+++++++++++++++-----------------------
Mpackages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 93+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/bank-ui/src/pages/QrCodeSection.stories.tsx | 10++++++++--
Mpackages/bank-ui/src/pages/QrCodeSection.tsx | 8++++----
Dpackages/bank-ui/src/pages/SolveChallengePage.tsx | 938-------------------------------------------------------------------------------
Mpackages/bank-ui/src/pages/WalletWithdrawForm.tsx | 40+++++++---------------------------------
Mpackages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 31++++++++++---------------------
Mpackages/bank-ui/src/pages/WithdrawalOperationPage.tsx | 16++++++----------
Mpackages/bank-ui/src/pages/WithdrawalQRCode.tsx | 46+++++++++++++++++++++-------------------------
Mpackages/bank-ui/src/pages/account/ShowAccountDetails.tsx | 8++++----
Mpackages/bank-ui/src/pages/admin/AccountForm.tsx | 48++++++++++++++++++++++++++++++------------------
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 12++++++------
18 files changed, 187 insertions(+), 1198 deletions(-)

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -201,7 +201,6 @@ function PublicRounting({ return ( <WithdrawalOperationPage operationId={location.values.wopid} - routeWithdrawalDetails={publicPages.operationDetails} origin="from-wallet-ui" onOperationAborted={() => navigateTo(publicPages.login.url({}))} routeClose={publicPages.login} @@ -314,7 +313,6 @@ function PrivateRouting({ return ( <WithdrawalOperationPage operationId={location.values.wopid} - routeWithdrawalDetails={privatePages.operationDetails} origin="from-wallet-ui" onOperationAborted={() => navigateTo(privatePages.home.url({}))} routeClose={privatePages.home} @@ -325,7 +323,6 @@ function PrivateRouting({ return ( <WithdrawalOperationPage operationId={location.values.wopid} - routeWithdrawalDetails={privatePages.operationDetails} origin="from-bank-ui" onOperationAborted={() => navigateTo(privatePages.home.url({}))} routeClose={privatePages.home} diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts @@ -17,10 +17,8 @@ import { AbsoluteTime, Amounts, - HttpStatusCode, - TalerError, - assertUnreachable, - parsePaytoUri, + Paytos, + TalerError } from "@gnu-taler/taler-util"; import { useTransactions } from "../../hooks/account.js"; import { Props, State, Transaction } from "./index.js"; @@ -52,19 +50,13 @@ export function useComponentState({ const transactions = result.body .map((tx) => { const negative = tx.direction === "debit"; - const cp = parsePaytoUri( + const cp = Paytos.fromString( negative ? tx.creditor_payto_uri : tx.debtor_payto_uri, ); const counterpart = - (cp === undefined || !cp.isKnown + cp === undefined || cp.type === "fail" ? undefined - : cp.targetType === "iban" - ? cp.iban - : cp.targetType === "x-taler-bank" - ? cp.account - : cp.targetType === "bitcoin" - ? `${cp.address.substring(0, 6)}...` - : undefined) ?? "unknown"; + : cp.body.displayName; const when = AbsoluteTime.fromProtocolTimestamp(tx.date); const amount = Amounts.parse(tx.amount); diff --git a/packages/bank-ui/src/context/wallet-integration.ts b/packages/bank-ui/src/context/wallet-integration.ts @@ -14,7 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { stringifyTalerUri, TalerUri } from "@gnu-taler/taler-util"; +import { TalerUris } from "@gnu-taler/taler-util"; +import { TalerUri } from "@gnu-taler/taler-util"; import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext } from "preact/hooks"; @@ -26,7 +27,7 @@ import { useContext } from "preact/hooks"; function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) { const meta = document.createElement("meta"); meta.setAttribute("name", "taler-uri"); - meta.setAttribute("content", stringifyTalerUri(uri)); + meta.setAttribute("content", TalerUris.toString(uri)); document.head.appendChild(meta); diff --git a/packages/bank-ui/src/pages/AccountPage/state.ts b/packages/bank-ui/src/pages/AccountPage/state.ts @@ -17,13 +17,14 @@ import { Amounts, HttpStatusCode, + PaytoType, + Paytos, TalerError, assertUnreachable, - parsePaytoUri, } from "@gnu-taler/taler-util"; import { useAccountDetails } from "../../hooks/account.js"; -import { Props, State } from "./index.js"; import { IntAmounts } from "../regional/CreateCashout.js"; +import { Props, State } from "./index.js"; export function useComponentState({ account, @@ -79,12 +80,12 @@ export function useComponentState({ const balance = Amounts.parseOrThrow(data.balance.amount); const debitThreshold = Amounts.parseOrThrow(data.debit_threshold); - const payto = parsePaytoUri(data.payto_uri); + const payto = Paytos.fromString(data.payto_uri); if ( - !payto || - !payto.isKnown || - (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank") + payto.type === "fail" || + !payto.body.targetType || + (payto.body.targetType !== PaytoType.IBAN && payto.body.targetType !== PaytoType.TalerBank) ) { return { status: "invalid-iban", diff --git a/packages/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts @@ -37,6 +37,7 @@ import { NeedConfirmationView, ReadyView, } from "./views.js"; +import { Paytos } from "@gnu-taler/taler-util"; export interface Props { routeClose: RouteDefinition; @@ -108,7 +109,7 @@ export namespace State { error: undefined; details: { - account: PaytoUri; + account: Paytos.URI; reserve: string; username: string; amount?: AmountJson; diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts @@ -16,14 +16,14 @@ import { Amounts, + HostPortPath, HttpStatusCode, + Paytos, TalerCoreBankErrorsByMethod, TalerCorebankApi, TalerError, + TalerUris, assertUnreachable, - parsePaytoUri, - parseWithdrawUri, - stringifyWithdrawUri, } from "@gnu-taler/taler-util"; import { useBankCoreApiContext, utils } from "@gnu-taler/web-util/browser"; import { useEffect, useState } from "preact/hooks"; @@ -33,7 +33,6 @@ import { useBankState } from "../../hooks/bank-state.js"; import { usePreferences } from "../../hooks/preferences.js"; import { useSessionState } from "../../hooks/session.js"; import { Props, State } from "./index.js"; -import { HostPortPath } from "@gnu-taler/taler-util"; export function useComponentState({ routeClose, @@ -46,7 +45,7 @@ export function useComponentState({ const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; const { - config, + config, lib: { bank }, } = useBankCoreApiContext(); @@ -97,43 +96,11 @@ export function useComponentState({ }; } - const wid = withdrawalOperationId; - - // async function doAbort(): Promise< - // | OperationFail<HttpStatusCode.NotFound> - // | OperationFail<HttpStatusCode.Conflict> - // | OperationFail<HttpStatusCode.BadRequest> - // | undefined - // > { - // if (!creds) return; - // const resp = await bank.abortWithdrawalById(creds, wid); - // if (resp.type === "ok") { - // // updateBankState("currentWithdrawalOperationId", undefined) - // onAbort(); - // } else { - // return resp; - // } - // return undefined; - // } - - // async function doConfirm(): Promise< - // TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined - // > { - // if (!creds) return; - // const resp = await bank.confirmWithdrawalById(creds, {}, wid); - // if (resp.type === "ok") { - // mutate(() => true); //clean withdrawal state - // } else { - // return resp; - // } - // return undefined; - // } - - const uri = stringifyWithdrawUri({ - bankIntegrationApiBaseUrl: bank.getIntegrationAPI().href as HostPortPath, + const parsedUri = TalerUris.createTalerWithdraw( + bank.getIntegrationAPI().href as HostPortPath, withdrawalOperationId, - }); - const parsedUri = parseWithdrawUri(uri); + ); + const uri = TalerUris.toString(parsedUri); if (!parsedUri) { return { status: "invalid-withdrawal", @@ -228,9 +195,9 @@ export function useComponentState({ const account = !data.selected_exchange_account ? undefined - : parsePaytoUri(data.selected_exchange_account); + : Paytos.fromString(data.selected_exchange_account); - if (!account) { + if (!account || account.type === "fail" || !account.body.targetType) { return { status: "invalid-payto", error: undefined, @@ -242,7 +209,7 @@ export function useComponentState({ status: "need-confirmation", error: undefined, details: { - account, + account: account.body, reserve: data.selected_reserve_pub, username: data.username, amount: !data.amount ? undefined : Amounts.parse(data.amount), diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx @@ -18,8 +18,8 @@ import { Amounts, HttpStatusCode, TalerErrorCode, + TalerUris, assertUnreachable, - stringifyWithdrawUri, } from "@gnu-taler/taler-util"; import { Attention, @@ -39,9 +39,10 @@ import { QR } from "../../components/QR.js"; import { usePreferences } from "../../hooks/preferences.js"; import { useSessionState } from "../../hooks/session.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; +import { SolveMFAChallenges } from "../SolveMFA.js"; import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; import { State } from "./index.js"; -import { SolveMFAChallenges } from "../SolveMFA.js"; +import { PaytoType } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 5; @@ -175,27 +176,14 @@ export function NeedConfirmationView({ <div class="w-full"> <dl class=""> {((): VNode => { - if (!details.account.isKnown) { - return ( - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900"> - <i18n.Translate> - Payment Service Provider's account - </i18n.Translate> - </dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {details.account.targetPath} - </dd> - </div> - ); - } switch (details.account.targetType) { - case "taler-reserve-http": - case "taler-reserve": { + case undefined: + case PaytoType.TalerReserveHttp: + case PaytoType.TalerReserve: { // FIXME: support wire transfer to wallet return <div>not yet supported</div>; } - case "iban": { + case PaytoType.IBAN: { const name = details.account.params["receiver-name"]; return ( <Fragment> @@ -224,7 +212,7 @@ export function NeedConfirmationView({ </Fragment> ); } - case "x-taler-bank": { + case PaytoType.TalerBank: { const name = details.account.params["receiver-name"]; return ( <Fragment> @@ -264,7 +252,7 @@ export function NeedConfirmationView({ </Fragment> ); } - case "bitcoin": { + case PaytoType.Bitcoin: { const name = details.account.params["receiver-name"]; return ( <Fragment> @@ -293,7 +281,7 @@ export function NeedConfirmationView({ </Fragment> ); } - case "ethereum": { + case PaytoType.Ethereum: { const name = details.account.params["receiver-name"]; return ( <Fragment> @@ -538,7 +526,11 @@ export function ReadyView({ lib: { bank }, } = useBankCoreApiContext(); - const talerWithdrawUri = stringifyWithdrawUri(uri); + const parsedUri = TalerUris.createTalerWithdraw( + uri.bankIntegrationApiBaseUrl, + uri.withdrawalOperationId, + ); + const talerWithdrawUri = TalerUris.toString(parsedUri); useEffect(() => { walletInegrationApi.publishTalerAction(uri); }, []); diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -23,15 +23,12 @@ import { HostPortPath, HttpStatusCode, IbanString, - PaytoString, - PaytoUri, + PaytoType, Paytos, TalerCorebankApi, TalerErrorCode, TranslatedString, - assertUnreachable, - parsePaytoUri, - stringifyPaytoUri, + assertUnreachable } from "@gnu-taler/taler-util"; import { ButtonBetter, @@ -135,42 +132,50 @@ export function PaytoWireTransferForm({ }); const foundPayto = searchPaytoInHumanReadableText(rawPaytoInput); - const parsed = !foundPayto ? undefined : parsePaytoUri(foundPayto); + const parsed = !foundPayto ? undefined : Paytos.fromString(foundPayto); const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n.str`Required` - : !parsed + : !parsed || parsed.type === "fail" ? i18n.str`Does not follow the pattern` - : validateRawPayto(parsed, limitWithFee, url.host, i18n, paytoType), + : validateRawPayto( + parsed.body, + limitWithFee, + url.host, + i18n, + paytoType, + ), }); - let payto_uri: PaytoString | undefined; + let payto_uri: Paytos.FullPaytoString | undefined; let sendingAmount: AmountString | undefined; let acName: string | undefined; if (isRawPayto) { - const p = parsePaytoUri(rawPaytoInput!); + const res = Paytos.fromString(rawPaytoInput!); - if (p) { + if (res && res.type === "ok") { + const p = res.body; sendingAmount = p.params.amount as AmountString; delete p.params.amount; // if this payto is valid then it already have message - payto_uri = stringifyPaytoUri(p); - acName = !p.isKnown - ? undefined - : p.targetType === "iban" - ? p.iban - : p.targetType === "taler-reserve" || - p.targetType === "taler-reserve-http" - ? undefined // FIXME: unsupported payto:// - : p.targetType === "bitcoin" - ? p.address - : p.targetType === "ethereum" + payto_uri = Paytos.toFullString(p); + acName = + p.targetType === undefined + ? undefined + : p.targetType === PaytoType.IBAN + ? p.iban + : p.targetType === PaytoType.TalerReserve || + p.targetType === PaytoType.TalerReserveHttp + ? undefined // FIXME: unsupported payto:// + : p.targetType === PaytoType.Bitcoin ? p.address - : p.targetType === "x-taler-bank" - ? p.account - : assertUnreachable(p); + : p.targetType === PaytoType.Ethereum + ? p.address + : p.targetType === PaytoType.TalerBank + ? p.account + : assertUnreachable(p); } } else if (account && subject) { // if (!account || !subject) return; @@ -287,39 +292,40 @@ export function PaytoWireTransferForm({ type="radio" name="input-type" onChange={() => { - if (parsed && parsed.isKnown) { - switch (parsed.targetType) { - case "ethereum": - case "bitcoin": - case "taler-reserve": - case "taler-reserve-http": { + if (parsed && parsed.type === "ok") { + switch (parsed.body.targetType) { + case PaytoType.Ethereum: + case PaytoType.Bitcoin: + case undefined: + case PaytoType.TalerReserve: + case PaytoType.TalerReserveHttp: { // FIXME: unsupported payto break; } - case "iban": { - setAccount(parsed.iban); + case PaytoType.IBAN: { + setAccount(parsed.body.iban); break; } - case "x-taler-bank": { - setAccount(parsed.account); + case PaytoType.TalerBank: { + setAccount(parsed.body.account); break; } default: { - assertUnreachable(parsed); + assertUnreachable(parsed.body); } } - const amountStr = !parsed.params + const amountStr = !parsed.body.params ? undefined - : parsed.params["amount"]; + : parsed.body.params["amount"]; if (amountStr) { const amount = Amounts.parse(amountStr); if (amount) { setAmount(Amounts.stringifyValue(amount)); } } - const subject = !parsed.params["message"] - ? parsed.params["subject"] - : parsed.params["message"]; + const subject = !parsed.body.params["message"] + ? parsed.body.params["subject"] + : parsed.body.params["message"]; if (subject) { setSubject(subject); } @@ -799,15 +805,12 @@ export function RenderAmount({ } function validateRawPayto( - parsed: PaytoUri, + parsed: Paytos.URI, limit: AmountJson, host: string, i18n: InternationalizationAPI, type: "iban" | "x-taler-bank", ): TranslatedString | undefined { - if (!parsed.isKnown) { - return i18n.str`The target type is unknown, use "${type}"`; - } let result: TranslatedString | undefined; switch (type) { case "x-taler-bank": { diff --git a/packages/bank-ui/src/pages/QrCodeSection.stories.tsx b/packages/bank-ui/src/pages/QrCodeSection.stories.tsx @@ -21,12 +21,18 @@ import * as tests from "@gnu-taler/web-util/testing"; import { QrCodeSection } from "./QrCodeSection.js"; -import { parseWithdrawUri } from "@gnu-taler/taler-util"; +import { TalerUriAction } from "@gnu-taler/taler-util"; +import { HostPortPath } from "@gnu-taler/taler-util"; export default { title: "Qr Code Selection", }; export const SimpleExample = tests.createExample(QrCodeSection, { - withdrawUri: parseWithdrawUri("taler://withdraw/bank.com/operationId"), + withdrawUri: { + bankIntegrationApiBaseUrl: "http://asd" as HostPortPath, + type: TalerUriAction.Withdraw, + withdrawalOperationId: "123", + externalConfirmation: false, + }, }); diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx @@ -16,12 +16,13 @@ import { HttpStatusCode, - stringifyWithdrawUri, - WithdrawUriResult, + TalerUris, + WithdrawUriResult } from "@gnu-taler/taler-util"; import { Button, LocalNotificationBanner, + useBankCoreApiContext, useLocalNotificationHandler, useTalerWalletIntegrationAPI, useTranslationContext, @@ -29,7 +30,6 @@ import { import { Fragment, h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useSessionState } from "../hooks/session.js"; const TALER_SCREEN_ID = 109; @@ -43,7 +43,7 @@ export function QrCodeSection({ }): VNode { const { i18n } = useTranslationContext(); const walletInegrationApi = useTalerWalletIntegrationAPI(); - const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); + const talerWithdrawUri = TalerUris.toString(withdrawUri); const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx @@ -1,938 +0,0 @@ -// /* -// 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 { -// AbsoluteTime, -// Amounts, -// Duration, -// HttpStatusCode, -// TalerCorebankApi, -// TalerError, -// TalerErrorCode, -// TokenSuccessResponse, -// TranslatedString, -// UserAndPassword, -// UserAndToken, -// assertUnreachable, -// createRFC8959AccessTokenEncoded, -// parsePaytoUri, -// } from "@gnu-taler/taler-util"; -// import { -// Attention, -// Loading, -// LocalNotificationBanner, -// RouteDefinition, -// ShowInputErrorLabel, -// Time, -// useBankCoreApiContext, -// useLocalNotification, -// useNavigationContext, -// useTranslationContext, -// } from "@gnu-taler/web-util/browser"; -// import { Fragment, VNode, h } from "preact"; -// import { useEffect, useState } from "preact/hooks"; -// -// import { useWithdrawalDetails } from "../hooks/account.js"; -// import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js"; -// import { useConversionInfo } from "../hooks/regional.js"; -// import { useSessionState } from "../hooks/session.js"; -// import { undefinedIfEmpty } from "../utils.js"; -// import { RenderAmount } from "./PaytoWireTransferForm.js"; -// import { OperationNotFound } from "./WithdrawalQRCode.js"; - -// const TALER_SCREEN_ID = 111; - -// type CredsType = -// | { type: "basic-operation"; password: UserAndPassword } -// | { type: "token-operation"; token: UserAndToken }; - -// const TAN_PREFIX = "T-"; -// const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/; -// export function SolveChallengePage({ -// onChallengeCompleted, -// routeClose, -// }: { -// onChallengeCompleted: () => void; -// routeClose: RouteDefinition; -// }): VNode { -// const { -// lib: { bank: api }, -// } = useBankCoreApiContext(); -// const { i18n } = useTranslationContext(); -// const [bankState, updateBankState] = useBankState(); -// const [code, setCode] = useState<string | undefined>(undefined); -// const [notification, notify, handleError] = useLocalNotification(); -// const { navigateTo } = useNavigationContext(); -// const session = useSessionState(); -// const userSession = -// session.state.status !== "loggedIn" ? undefined : session.state; - -// if (!bankState.currentChallenge) { -// return ( -// <div> -// <span>no challenge to solve </span> -// <a -// href={routeClose.url({})} -// name="close" -// class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" -// > -// <i18n.Translate>Continue</i18n.Translate> -// </a> -// </div> -// ); -// } - -// const ch = bankState.currentChallenge; -// const errors = undefinedIfEmpty({ -// code: !code -// ? i18n.str`Required` -// : !TAN_REGEX.test(code) -// ? i18n.str`Confirmation codes are numerical, possibly beginning with 'T-.'` -// : undefined, -// }); - -// const creds = -// ch.operation === "login" -// ? ({ -// type: "basic-operation", -// password: ch.request, -// } as CredsType) -// : userSession -// ? ({ -// type: "token-operation", -// token: userSession, -// } as CredsType) -// : undefined; - -// async function startChallenge() { -// if (!creds) return; -// await handleError(async () => { -// const resp = await (creds.type === "basic-operation" -// ? api.sendLoginChallenge(creds.password, ch.id) -// : api.sendChallenge(creds.token, ch.id)); - -// if (resp.type === "ok") { -// const newCh = structuredClone(ch); -// newCh.sent = AbsoluteTime.now(); -// newCh.info = resp.body; -// updateBankState("currentChallenge", newCh); -// } else { -// const newCh = structuredClone(ch); -// newCh.sent = AbsoluteTime.now(); -// newCh.info = undefined; -// updateBankState("currentChallenge", newCh); -// switch (resp.case) { -// case HttpStatusCode.NotFound: -// return notify({ -// type: "error", -// title: i18n.str`No cashout was found. The cashout process has probably already been aborted.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case HttpStatusCode.Unauthorized: -// return notify({ -// type: "error", -// title: i18n.str`No cashout was found. The cashout process has probably already been aborted.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case HttpStatusCode.Forbidden: -// return notify({ -// type: "error", -// title: i18n.str`You have no rights to complete the challenge.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case HttpStatusCode.TooManyRequests: -// return notify({ -// type: "error", -// title: i18n.str`Too many challenges are active right now, you must wait or confirm current challenges.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: -// return notify({ -// type: "error", -// title: i18n.str`No cashout was found. The cashout process has probably already been aborted.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// default: -// assertUnreachable(resp); -// } -// } -// }); -// } - -// async function completeChallenge() { -// if (!creds || !code) return; -// const tan = code.toUpperCase().startsWith(TAN_PREFIX) -// ? code.substring(TAN_PREFIX.length) -// : code; -// await handleError(async () => { -// { -// const resp = await (creds.type === "basic-operation" -// ? api.confirmLoginChallenge(creds.password, ch.id, { tan }) -// : api.confirmChallenge(creds.token, ch.id, { tan })); -// if (resp.type === "fail") { -// setCode(""); -// switch (resp.case) { -// case HttpStatusCode.NotFound: -// return notify({ -// type: "error", -// title: i18n.str`Challenge not found.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case HttpStatusCode.Unauthorized: -// return notify({ -// type: "error", -// title: i18n.str`This user is not authorized to complete this challenge.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case HttpStatusCode.TooManyRequests: -// return notify({ -// type: "error", -// title: i18n.str`Too many attempts, try another code.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: -// return notify({ -// type: "error", -// title: i18n.str`The confirmation code is wrong, try again.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: -// return notify({ -// type: "error", -// title: i18n.str`The operation expired.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// default: -// assertUnreachable(resp); -// } -// } -// } -// { -// const resp = await (async (ch: ChallengeInProgess) => { -// switch (ch.operation) { -// case "delete-account": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.deleteAccount(userSession, ch.id); -// } -// case "update-account": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.updateAccount(userSession, ch.request, ch.id); -// } -// case "update-password": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.updatePassword(userSession, ch.request, ch.id); -// } -// case "create-transaction": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.createTransaction( -// userSession, -// ch.request, -// ch.id, -// ); -// } -// case "confirm-withdrawal": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.confirmWithdrawalById( -// userSession, -// ch.request, -// ch.id, -// ); -// } -// case "create-cashout": { -// if (!userSession) { -// // this should not happen since creds is present -// throw Error( -// `Session lost after challenge completed and operation retry. Try again or report.`, -// ); -// } -// return await api.createCashout(userSession, ch.request, ch.id); -// } - -// case "login": { -// const result = await api.createAccessTokenBasic( -// ch.request.username, -// ch.request.password, -// ch.request.tokenRequest, -// ch.id, -// ); -// if (result.type === "ok") { -// const d = result.body as TokenSuccessResponse; -// session.logIn({ -// username: ch.request.username, -// token: createRFC8959AccessTokenEncoded(d.access_token), -// expiration: AbsoluteTime.fromProtocolTimestamp( -// result.body.expiration, -// ), -// }); -// } -// return result; -// } - -// default: -// assertUnreachable(ch); -// } -// })(ch); - -// if (resp.type === "fail") { -// if (resp.case !== HttpStatusCode.Accepted) { -// return notify({ -// type: "error", -// title: i18n.str`The operation failed.`, -// description: resp.detail?.hint as TranslatedString, -// debug: resp.detail, -// when: AbsoluteTime.now(), -// }); -// } -// // another challenge required, save the request and the ID -// // @ts-expect-error no need to check the type of request, since it will be the same as the previous request -// updateBankState("currentChallenge", { -// operation: ch.operation, -// id: String(resp.body.challenge_id), -// location: ch.location, -// sent: AbsoluteTime.never(), -// request: ch.request, -// }); -// return notify({ -// type: "info", -// title: i18n.str`The operation needs another confirmation to complete.`, -// when: AbsoluteTime.now(), -// }); -// } -// updateBankState("currentChallenge", undefined); -// return onChallengeCompleted(); -// } -// }); -// } - -// return ( -// <Fragment> -// <LocalNotificationBanner notification={notification} /> - // <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> - // <div class="px-4 sm:px-0"> - // <h2 class="text-base font-semibold leading-7 text-gray-900"> - // <span - // class="text-sm text-black font-semibold leading-6 " - // id="availability-label" - // > - // <i18n.Translate>Confirm the operation</i18n.Translate> - // </span> - // </h2> - // <p class="mt-2 text-sm text-gray-500"> - // <i18n.Translate> - // This operation is protected with second factor authentication. In - // order to complete it we need to verify your identity using the - // authentication channel you provided. - // </i18n.Translate> - // </p> - // </div> - - // <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"> -// <ChallengeDetails -// challenge={bankState.currentChallenge} -// onStart={startChallenge} -// onCancel={() => { -// updateBankState("currentChallenge", undefined); -// navigateTo(ch.location ?? routeClose.url({})); -// }} -// /> -// {ch.info && ( -// <div class="mt-2"> -// <form -// class="bg-white shadow-sm ring-1 ring-gray-900/5" -// autoCapitalize="none" -// autoCorrect="off" -// onSubmit={(e) => { -// e.preventDefault(); -// }} -// > -// <div class="px-4 py-4"> -// <label for="withdraw-amount"> -// <i18n.Translate>Enter the confirmation code</i18n.Translate> -// </label> -// <div class="mt-2"> -// <div class="relative rounded-md shadow-sm"> -// <input -// type="text" -// // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" -// aria-describedby="answer" -// autoFocus -// class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" -// value={code ?? ""} -// required -// onPaste={(e) => { -// e.preventDefault(); -// const pasted = e.clipboardData?.getData("text/plain"); -// if (!pasted) return; -// if (pasted.toUpperCase().startsWith(TAN_PREFIX)) { -// const sub = pasted.substring(TAN_PREFIX.length); -// setCode(sub); -// return; -// } -// setCode(pasted); -// }} -// name="answer" -// id="answer" -// autocomplete="off" -// onChange={(e): void => { -// setCode(e.currentTarget.value); -// }} -// /> -// </div> -// <ShowInputErrorLabel -// message={errors?.code} -// isDirty={code !== undefined} -// /> -// </div> -// <p class="mt-2 text-sm text-gray-500"> -// {((ch: TalerCorebankApi.TanChannel): VNode => { -// switch (ch) { -// case TalerCorebankApi.TanChannel.SMS: -// return ( -// <i18n.Translate> -// You should have received a code on your mobile -// phone. -// </i18n.Translate> -// ); -// case TalerCorebankApi.TanChannel.EMAIL: -// return ( -// <i18n.Translate> -// You should have received a code in your email. -// </i18n.Translate> -// ); -// default: -// assertUnreachable(ch); -// } -// })(ch.info.tan_channel)} -// </p> -// <p class="mt-2 text-sm text-gray-500"> -// <i18n.Translate> -// The confirmation code starts with "{TAN_PREFIX}" followed -// by numbers. -// </i18n.Translate> -// </p> -// </div> -// <div class="flex items-center justify-between border-gray-900/10 px-4 py-4 "> -// <div /> -// <button -// type="submit" -// name="confirm" -// class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" -// disabled={!!errors} -// onClick={(e) => { -// completeChallenge(); -// e.preventDefault(); -// }} -// > -// <i18n.Translate>Confirm</i18n.Translate> -// </button> -// </div> -// </form> -// </div> -// )} -// </div> -// </div> -// </Fragment> -// ); -// } - -// function ChallengeDetails({ -// challenge, -// onStart, -// onCancel, -// }: { -// challenge: ChallengeInProgess; -// onStart: () => void; -// onCancel: () => void; -// }): VNode { -// const { i18n } = useTranslationContext(); -// const { config } = useBankCoreApiContext(); - -// const firstTime = AbsoluteTime.isNever(challenge.sent); -// useEffect(() => { -// if (firstTime) { -// onStart(); -// } -// }, []); - -// const subtitle = ((op): TranslatedString => { -// switch (op) { -// case "delete-account": -// return i18n.str`Removing account`; -// case "update-account": -// return i18n.str`Updating account values`; -// case "update-password": -// return i18n.str`Updating password`; -// case "create-transaction": -// return i18n.str`Making a wire transfer`; -// case "confirm-withdrawal": -// return i18n.str`Confirming withdrawal`; -// case "create-cashout": -// return i18n.str`Making a cashout`; -// case "login": -// return i18n.str`Authentication`; -// } -// })(challenge.operation); - -// return ( - // <div class="px-4 mt-4 "> - // <div class="w-full"> - // <div class="border-gray-100"> - // <h2 class="text-base font-semibold leading-10 text-gray-900"> - // <span class=" text-black font-semibold leading-6 "> - // <i18n.Translate>Operation:</i18n.Translate> - // </span>{" "} - // &nbsp; - // <span class=" text-black font-normal leading-6 ">{subtitle}</span> - // </h2> - // <dl class="divide-y divide-gray-100"> -// {((): VNode => { -// switch (challenge.operation) { -// case "delete-account": -// return ( -// <Fragment> - // <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - // <dt class="text-sm font-medium leading-6 text-gray-900"> - // <i18n.Translate>Type</i18n.Translate> - // </dt> - // <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - // <i18n.Translate> - // Updating account settings - // </i18n.Translate> - // </dd> - // </div> - // <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - // <dt class="text-sm font-medium leading-6 text-gray-900"> - // <i18n.Translate>Account</i18n.Translate> - // </dt> - // <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - // {challenge.request} - // </dd> - // </div> -// </Fragment> -// ); -// case "create-transaction": { -// const payto = parsePaytoUri(challenge.request.payto_uri)!; -// return ( -// <Fragment> -// {challenge.request.amount && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Amount</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// <RenderAmount -// value={Amounts.parseOrThrow( -// challenge.request.amount, -// )} -// spec={config.currency_specification} -// /> -// </dd> -// </div> -// )} -// {payto.isKnown && payto.targetType === "iban" && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>To account</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {payto.iban} -// </dd> -// </div> -// )} -// </Fragment> -// ); -// } -// case "confirm-withdrawal": -// return ( -// <ShowWithdrawalDetails -// id={challenge.request.id} -// request={challenge.request} -// /> -// ); -// case "create-cashout": { -// return <ShowCashoutDetails request={challenge.request} />; -// } -// case "update-account": { -// return ( -// <Fragment> -// {challenge.request.cashout_payto_uri !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Cashout account</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.cashout_payto_uri} -// </dd> -// </div> -// )} -// {challenge.request.contact_data?.email !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Email</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.contact_data?.email} -// </dd> -// </div> -// )} -// {challenge.request.contact_data?.phone !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Phone</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.contact_data?.phone} -// </dd> -// </div> -// )} -// {challenge.request.debit_threshold !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Debit threshold</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// <RenderAmount -// value={Amounts.parseOrThrow( -// challenge.request.debit_threshold, -// )} -// spec={config.currency_specification} -// /> -// </dd> -// </div> -// )} -// {challenge.request.is_public !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate> -// Is this account public? -// </i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.is_public -// ? i18n.str`Enable` -// : i18n.str`Disable`} -// </dd> -// </div> -// )} -// {challenge.request.name !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Name</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.name} -// </dd> -// </div> -// )} -// {challenge.request.tan_channel !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate> -// Authentication channel -// </i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.tan_channel ?? i18n.str`Remove`} -// </dd> -// </div> -// )} -// </Fragment> -// ); -// } -// case "login": { -// return ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Username</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.username} -// </dd> -// </div> -// ); -// } -// case "update-password": { -// return ( -// <Fragment> -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>New password</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {challenge.request.new_password} -// </dd> -// </div> -// </Fragment> -// ); -// } -// default: -// assertUnreachable(challenge); -// } -// })()} -// </dl> -// {challenge.info && ( - // <h2 class="text-base font-semibold leading-7 text-gray-900 mt-4"> - // <span - // class="text-sm text-black font-semibold leading-6 " - // id="availability-label" - // > - // <i18n.Translate>Challenge details</i18n.Translate> - // </span> - // </h2> -// )} - // <dl class="divide-y divide-gray-100"> -// {challenge.sent.t_ms !== "never" && ( - // <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - // <dt class="text-sm font-medium leading-6 text-gray-900"> - // <i18n.Translate>Sent at</i18n.Translate> - // </dt> - // <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - // <Time - // format="dd/MM/yyyy HH:mm:ss" - // timestamp={challenge.sent} - // relative={Duration.fromSpec({ days: 1 })} - // /> - // </dd> - // </div> -// )} -// {challenge.info && ( - // <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - // <dt class="text-sm font-medium leading-6 text-gray-900"> - // {((ch: TalerCorebankApi.TanChannel): VNode => { - // switch (ch) { - // case TalerCorebankApi.TanChannel.SMS: - // return <i18n.Translate>To phone</i18n.Translate>; - // case TalerCorebankApi.TanChannel.EMAIL: - // return <i18n.Translate>To email</i18n.Translate>; - // default: - // assertUnreachable(ch); - // } - // })(challenge.info.tan_channel)} - // </dt> - // <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - // {challenge.info.tan_info} - // </dd> - // </div> -// )} -// </dl> -// </div> - // <div class="mt-6 mb-4 flex justify-between"> - // <button - // type="button" - // name="cancel" - // class="text-sm font-semibold leading-6 text-gray-900" - // onClick={onCancel} - // > - // <i18n.Translate>Cancel</i18n.Translate> - // </button> - // {challenge.info ? ( - // <button - // type="submit" - // name="send again" - // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - // onClick={(e) => { - // onStart(); - // e.preventDefault(); - // }} - // > - // <i18n.Translate>Send again</i18n.Translate> - // </button> - // ) : ( - // <div> sending code ...</div> - // )} - // </div> -// </div> -// </div> -// ); -// } - -// function ShowWithdrawalDetails({ -// id, -// request, -// }: { -// id: string; -// request: TalerCorebankApi.BankAccountConfirmWithdrawalRequest; -// }): VNode { -// const details = useWithdrawalDetails(id); -// const { i18n } = useTranslationContext(); -// const { config } = useBankCoreApiContext(); -// if (!details) { -// return <Loading />; -// } -// if (details instanceof TalerError) { -// return <ErrorLoading error={details} />; -// } -// if (details.type === "fail") { -// switch (details.case) { -// case HttpStatusCode.BadRequest: -// case HttpStatusCode.NotFound: -// return <OperationNotFound routeClose={undefined} />; -// default: -// assertUnreachable(details); -// } -// } - -// return ( -// <Fragment> -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {details.body.amount !== undefined ? ( -// <RenderAmount -// value={Amounts.parseOrThrow(details.body.amount)} -// spec={config.currency_specification} -// /> -// ) : ( -// <i18n.Translate>No amount has yet been determined.</i18n.Translate> -// )} -// </dd> -// </div> -// {details.body.selected_reserve_pub !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Withdraw reserve ID</i18n.Translate> -// </dt> -// <dd -// class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" -// title={details.body.selected_reserve_pub} -// > -// {details.body.selected_reserve_pub.substring(0, 16)}... -// </dd> -// </div> -// )} -// {details.body.selected_exchange_account !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>To account</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {details.body.selected_exchange_account} -// </dd> -// </div> -// )} -// </Fragment> -// ); -// } - -// function ShowCashoutDetails({ -// request, -// }: { -// request: TalerCorebankApi.CashoutRequest; -// }): VNode { -// const { i18n } = useTranslationContext(); -// const info = useConversionInfo(); -// if (!info) { -// return <Loading />; -// } - -// if (info instanceof TalerError) { -// return <ErrorLoading error={info} />; -// } -// if (info.type === "fail") { -// switch (info.case) { -// case HttpStatusCode.NotImplemented: { -// return ( -// <Attention type="danger" title={i18n.str`Cashout is disabled`}> -// <i18n.Translate> -// Cashout should be enabled in the configuration, the conversion -// rate should be initialized with fee(s), rates and a rounding mode. -// </i18n.Translate> -// </Attention> -// ); -// } -// default: -// assertUnreachable(info); -// } -// } - -// return ( -// <Fragment> -// {request.subject !== undefined && ( -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900"> -// <i18n.Translate>Subject</i18n.Translate> -// </dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// {request.subject} -// </dd> -// </div> -// )} -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// <RenderAmount -// value={Amounts.parseOrThrow(request.amount_credit)} -// spec={info.body.regional_currency_specification} -// /> -// </dd> -// </div> -// <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> -// <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt> -// <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> -// <RenderAmount -// value={Amounts.parseOrThrow(request.amount_credit)} -// spec={info.body.fiat_currency_specification} -// /> -// </dd> -// </div> -// </Fragment> -// ); -// } diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -20,9 +20,10 @@ import { Amounts, HttpStatusCode, TalerCorebankApi, + TalerUriAction, + TalerUris, TranslatedString, assertUnreachable, - parseWithdrawUri, } from "@gnu-taler/taler-util"; import { Attention, @@ -31,9 +32,7 @@ import { ShowInputErrorLabel, notifyError, useBankCoreApiContext, - useChallengeHandler, useLocalNotification, - useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -50,7 +49,7 @@ import { RenderAmount, doAutoFocus, } from "./PaytoWireTransferForm.js"; -import { IntAmountJson, IntAmounts } from "./regional/CreateCashout.js"; +import { IntAmountJson } from "./regional/CreateCashout.js"; const TALER_SCREEN_ID = 112; @@ -128,31 +127,6 @@ function OldWithdrawalForm({ `${settings.defaultSuggestedAmount ?? 1}`, ); const [notification, notify, handleError] = useLocalNotification(); - // const result = useWithdrawalDetails(bankState.currentWithdrawalOperationId); - // const loading = !result; - // const error = - // !loading && (result instanceof TalerError || result.type === "fail"); - // const pending = !loading && !error && result.body.status === "pending"; - - // if (pending) { - // // FIXME: doing the preventDefault is not optimal - - // // const suri = stringifyWithdrawUri({ - // // bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl, - // // withdrawalOperationId: bankState.currentWithdrawalOperationId, - // // }); - // // const uri = parseWithdrawUri(suri)! - // return ( - // <ThereIsAnOperationWarning - // onClose={() => { - // updateBankState("currentWithdrawalOperationId", undefined); - // }} - // routeOperationDetails={routeOperationDetails} - // wopid={bankState.currentWithdrawalOperationId!} - // focus - // /> - // ); - // } const trimmedAmountStr = amountStr?.trim(); @@ -184,8 +158,8 @@ function OldWithdrawalForm({ }; const resp = await api.createWithdrawal(creds, params); if (resp.type === "ok") { - const uri = parseWithdrawUri(resp.body.taler_withdraw_uri); - if (!uri) { + const uri = TalerUris.fromString(resp.body.taler_withdraw_uri); + if (uri.type === "fail" || uri.body.type !== TalerUriAction.Withdraw) { return notifyError( i18n.str`The server replied with an invalid taler://withdraw URI`, i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`, @@ -193,9 +167,9 @@ function OldWithdrawalForm({ } else { updateBankState( "currentWithdrawalOperationId", - uri.withdrawalOperationId, + uri.body.withdrawalOperationId, ); - onOperationCreated(uri.withdrawalOperationId); + onOperationCreated(uri.body.withdrawalOperationId); } } else { switch (resp.case) { diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -41,6 +41,8 @@ import { LoggedIn, useSessionState } from "../hooks/session.js"; import { LoginForm } from "./LoginForm.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "./SolveMFA.js"; +import { Paytos } from "@gnu-taler/taler-util"; +import { PaytoType } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 114; @@ -48,7 +50,7 @@ interface Props { onAborted: () => void; withdrawUri: WithdrawUriResult; details: { - account: PaytoUri; + account: Paytos.URI; reserve: string; username: string; amount?: AmountJson; @@ -189,27 +191,14 @@ export function WithdrawalConfirmationQuestion({ <div class="mt-6 border-t border-gray-100"> <dl class="divide-y divide-gray-100"> {((): VNode => { - if (!details.account.isKnown) { - return ( - <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> - <dt class="text-sm font-medium leading-6 text-gray-900"> - <i18n.Translate> - Payment Service Provider's account - </i18n.Translate> - </dt> - <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"> - {details.account.targetPath} - </dd> - </div> - ); - } switch (details.account.targetType) { - case "taler-reserve-http": - case "taler-reserve": { + case undefined: + case PaytoType.TalerReserveHttp: + case PaytoType.TalerReserve: { // FIXME: support wire transfer to wallet return <div>not yet supported</div>; } - case "iban": { + case PaytoType.IBAN: { const name = details.account.params["receiver-name"]; return ( @@ -240,7 +229,7 @@ export function WithdrawalConfirmationQuestion({ </Fragment> ); } - case "x-taler-bank": { + case PaytoType.TalerBank: { const name = details.account.params["receiver-name"]; return ( @@ -281,7 +270,7 @@ export function WithdrawalConfirmationQuestion({ </Fragment> ); } - case "bitcoin": { + case PaytoType.Bitcoin: { const name = details.account.params["receiver-name"]; return ( @@ -312,7 +301,7 @@ export function WithdrawalConfirmationQuestion({ </Fragment> ); } - case "ethereum": { + case PaytoType.Ethereum: { const name = details.account.params["receiver-name"]; return ( diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx @@ -14,7 +14,6 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; @@ -22,31 +21,29 @@ import { useBankState } from "../hooks/bank-state.js"; import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { WithdrawalQRCode } from "./WithdrawalQRCode.js"; import { HostPortPath } from "@gnu-taler/taler-util"; +import { TalerUris } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 115; export function WithdrawalOperationPage({ operationId, - onOperationAborted, routeClose, origin, - routeWithdrawalDetails, }: { operationId: string; origin: "from-bank-ui" | "from-wallet-ui"; onOperationAborted: () => void; routeClose: RouteDefinition; - routeWithdrawalDetails: RouteDefinition<{ wopid: string }>; }): VNode { const { lib: { bank: api }, } = useBankCoreApiContext(); - const uri = stringifyWithdrawUri({ - bankIntegrationApiBaseUrl: api.getIntegrationAPI().href as HostPortPath, - withdrawalOperationId: operationId, - }); - const parsedUri = parseWithdrawUri(uri); + const parsedUri = TalerUris.createTalerWithdraw( + api.getIntegrationAPI().href as HostPortPath, + operationId, + ); + const uri = TalerUris.toString(parsedUri); const { i18n } = useTranslationContext(); const [, updateBankState] = useBankState(); @@ -65,7 +62,6 @@ export function WithdrawalOperationPage({ <WithdrawalQRCode withdrawUri={parsedUri} origin={origin} - routeWithdrawalDetails={routeWithdrawalDetails} onOperationAborted={() => { updateBankState("currentWithdrawalOperationId", undefined); onOperationAborted(); diff --git a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx @@ -19,9 +19,7 @@ import { HttpStatusCode, TalerError, WithdrawUriResult, - assertUnreachable, - parsePaytoUri, - stringifyWithdrawUri, + assertUnreachable } from "@gnu-taler/taler-util"; import { Attention, @@ -33,6 +31,7 @@ import { } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; +import { Paytos, TalerUris } from "@gnu-taler/taler-util"; import { useWithdrawalDetails } from "../hooks/account.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; @@ -44,7 +43,6 @@ interface Props { origin: "from-bank-ui" | "from-wallet-ui"; onOperationAborted: () => void; routeClose: RouteDefinition; - routeWithdrawalDetails: RouteDefinition<{ wopid: string }>; } /** * Offer the QR code (and a clickable taler://-link) to @@ -56,7 +54,6 @@ export function WithdrawalQRCode({ onOperationAborted, routeClose, origin, - routeWithdrawalDetails, }: Props): VNode { const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); @@ -127,7 +124,7 @@ export function WithdrawalQRCode({ </div> ); } - const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); + const talerWithdrawUri = TalerUris.toString(withdrawUri); if (data.status === "confirmed") { return ( @@ -203,22 +200,22 @@ export function WithdrawalQRCode({ const account = !data.selected_exchange_account ? undefined - : parsePaytoUri(data.selected_exchange_account); + : Paytos.fromString(data.selected_exchange_account); - if (!data.selected_reserve_pub && account) { - return ( - <Attention - type="danger" - title={i18n.str`The operation is marked as selected, but a process during the withdrawal failed`} - > - <i18n.Translate> - The account was selected, but no withdrawal reserve ID was found. - </i18n.Translate> - </Attention> - ); - } - - if (!account && data.selected_reserve_pub) { + if (!account || account.type === "fail") { + if (!data.selected_reserve_pub) { + return ( + <Attention + type="danger" + title={i18n.str`The operation is marked as selected, but a process during the withdrawal failed`} + > + <i18n.Translate> + A withdrawal reserve ID was not found and no account has been + selected. + </i18n.Translate> + </Attention> + ); + } return ( <Attention type="danger" @@ -232,15 +229,14 @@ export function WithdrawalQRCode({ ); } - if (!account || !data.selected_reserve_pub) { + if (!data.selected_reserve_pub) { return ( <Attention type="danger" title={i18n.str`The operation is marked as selected, but a process during the withdrawal failed`} > <i18n.Translate> - A withdrawal reserve ID was not found and the no account has been - selected. + The account was selected, but no withdrawal reserve ID was found. </i18n.Translate> </Attention> ); @@ -251,7 +247,7 @@ export function WithdrawalQRCode({ withdrawUri={withdrawUri} details={{ username: data.username, - account, + account: account.body, reserve: data.selected_reserve_pub, amount: !data.amount ? undefined : Amounts.parseOrThrow(data.amount), }} diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -18,8 +18,7 @@ import { TalerCorebankApi, TalerError, TalerErrorCode, - assertUnreachable, - parsePaytoUri, + assertUnreachable } from "@gnu-taler/taler-util"; import { Attention, @@ -39,6 +38,7 @@ import { import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { Paytos } from "@gnu-taler/taler-util"; import { useAccountDetails } from "../../hooks/account.js"; import { usePreferences } from "../../hooks/preferences.js"; import { LoggedIn, useSessionState } from "../../hooks/session.js"; @@ -163,8 +163,8 @@ export function ShowAccountDetails({ const revenueURL = new URL(baseURL); revenueURL.username = account; revenueURL.password; - const ac = parsePaytoUri(result.body.payto_uri); - const payto = !ac?.isKnown ? undefined : ac; + const ac = Paytos.fromString(result.body.payto_uri); + const payto = ac.type === "fail" || !ac.body.targetType ? undefined : ac.body; if (mfa.pendingChallenge && repeatUpdate) { return ( diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx @@ -16,11 +16,12 @@ import { AmountString, Amounts, + HostPortPath, + IbanString, PaytoString, + Paytos, TalerCorebankApi, assertUnreachable, - parsePaytoUri, - stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { CopyButton, @@ -44,9 +45,7 @@ import { doAutoFocus, } from "../PaytoWireTransferForm.js"; import { getRandomPassword } from "../rnd.js"; -import { Paytos } from "@gnu-taler/taler-util"; -import { HostPortPath } from "@gnu-taler/taler-util"; -import { IbanString } from "@gnu-taler/taler-util"; +import { PaytoType } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 120; @@ -121,10 +120,13 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ isPublic: template?.is_public, name: template?.name ?? "", cashout_payto_uri: - getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? - ("" as PaytoString), + getAccountId( + cashoutPaytoType, + template?.cashout_payto_uri as Paytos.FullPaytoString, + ) ?? "", payto_uri: - getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString), + getAccountId(paytoType, template?.payto_uri as Paytos.FullPaytoString) ?? + "", email: template?.contact_data?.email ?? "", phone: template?.contact_data?.phone ?? "", username: username ?? "", @@ -235,7 +237,10 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ break; } case "iban": { - cashout = Paytos.createIban(newForm.cashout_payto_uri as IbanString, undefined); + cashout = Paytos.createIban( + newForm.cashout_payto_uri as IbanString, + undefined, + ); break; } default: @@ -246,11 +251,17 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ if (newForm.payto_uri) switch (paytoType) { case "x-taler-bank": { - internal = Paytos.createTalerBank(url.host as HostPortPath, newForm.payto_uri); + internal = Paytos.createTalerBank( + url.host as HostPortPath, + newForm.payto_uri, + ); break; } case "iban": { - internal = Paytos.createIban(newForm.payto_uri as IbanString, undefined); + internal = Paytos.createIban( + newForm.payto_uri as IbanString, + undefined, + ); break; } default: @@ -768,14 +779,15 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ function getAccountId( type: "iban" | "x-taler-bank", - s: PaytoString | undefined, + s: Paytos.FullPaytoString | undefined, ): string | undefined { if (s === undefined) return undefined; - const p = parsePaytoUri(s); - if (p === undefined) return undefined; - if (!p.isKnown) return "<unknown>"; - if (type === "iban" && p.targetType === "iban") return p.iban; - if (type === "x-taler-bank" && p.targetType === "x-taler-bank") - return p.account; + const p = Paytos.fromString(s); + if (p === undefined || p.type === "fail") return undefined; + if (!p.body.targetType === undefined) return "<unknown>"; + if (type === "iban" && p.body.targetType === PaytoType.IBAN) + return p.body.iban; + if (type === "x-taler-bank" && p.body.targetType === PaytoType.TalerBank) + return p.body.account; return "<unsupported>"; } diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -24,7 +24,6 @@ import { assertUnreachable, encodeCrock, getRandomBytes, - parsePaytoUri, } from "@gnu-taler/taler-util"; import { Attention, @@ -62,6 +61,7 @@ import { } from "../PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "../SolveMFA.js"; import { TalerCorebankApi } from "@gnu-taler/taler-util"; +import { Paytos } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 127; @@ -401,14 +401,14 @@ function CreateCashoutInternal({ const cashoutAccount = !accountData.cashout_payto_uri ? undefined - : parsePaytoUri(accountData.cashout_payto_uri); - const cashoutAccountName = !cashoutAccount + : Paytos.fromString(accountData.cashout_payto_uri); + const cashoutAccountName = !cashoutAccount || cashoutAccount.type === "fail" ? undefined - : cashoutAccount.targetPath; + : cashoutAccount.body.displayName; - const cashoutLegalName = !cashoutAccount + const cashoutLegalName = !cashoutAccount || cashoutAccount.type === "fail" ? undefined - : cashoutAccount.params["receiver-name"]; + : cashoutAccount.body.params["receiver-name"]; if (mfa.pendingChallenge && repeatCashout) { return (