taler-typescript-core

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

commit 41eb1e4ef4823d38e825697d61ea7635d74582c9
parent e86f7d14882fe75e5ef008cee5c0dbed28ee91b1
Author: Antoine A <>
Date:   Tue, 22 Jul 2025 14:41:26 +0200

bank-ui: conversion rate setup

Diffstat:
Mpackages/bank-ui/src/components/Cashouts/views.tsx | 34++++++++++++++++++++++++++++++----
Mpackages/bank-ui/src/hooks/regional.ts | 33---------------------------------
Mpackages/bank-ui/src/pages/ConversionRateClassDetails.tsx | 7+++++--
Mpackages/bank-ui/src/pages/regional/ConversionConfig.tsx | 49++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 81++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-util/src/http-client/bank-conversion.ts | 1-
Mpackages/taler-util/src/types-taler-corebank.ts | 10----------
7 files changed, 132 insertions(+), 83 deletions(-)

diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx @@ -18,18 +18,21 @@ import { AbsoluteTime, Amounts, HttpStatusCode, + TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, + Loading, Time, - useBankCoreApiContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; +import { useConversionInfo } from "../../hooks/regional.js"; +import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; const TALER_SCREEN_ID = 3; @@ -56,7 +59,6 @@ export function ReadyView({ routeCashoutDetails, }: State.Ready): VNode { const { i18n, dateLocale } = useTranslationContext(); - const { config } = useBankCoreApiContext(); if (!cashouts.length) return <div />; const txByDate = cashouts.reduce( @@ -75,6 +77,30 @@ export function ReadyView({ }, {} as Record<string, typeof cashouts>, ); + const conversionResp = useConversionInfo(); + if (!conversionResp) { + return <Loading />; + } else if (conversionResp instanceof TalerError) { + return <ErrorLoadingWithDebug error={conversionResp} />; + } else if (conversionResp.type === "fail") { + switch (conversionResp.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(conversionResp); + } + } + const { fiat_currency_specification, regional_currency_specification } = + conversionResp.body; + return ( <div class="px-4 mt-4"> <div class="sm:flex sm:items-center"> @@ -167,13 +193,13 @@ export function ReadyView({ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer"> <RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} - spec={config.currency_specification} + spec={regional_currency_specification} /> </td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer"> <RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} - spec={config.fiat_currency_specification!} + spec={fiat_currency_specification!} /> </td> diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts @@ -130,39 +130,6 @@ export function useConversionRateForUser( return undefined; } -export function useConversionInfoForClass(classId: number) { - const { - lib: { conversionForClass }, - config, - } = useBankCoreApiContext(); - - async function fetcher() { - return await conversionForClass(classId).getConfig(); - } - const { data, error } = useSWR< - TalerBankConversionResultByMethod<"getConfig">, - TalerHttpError - >( - !config.allow_conversion ? undefined : ["useConversionInfoForClass"], - fetcher, - { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - keepPreviousData: true, - }, - ); - - if (data) return data; - if (error) return error; - return undefined; -} - function buildEstimatorWithTheBackend( conversion: TalerBankConversionHttpClient, token: AccessToken | undefined, diff --git a/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx b/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx @@ -39,7 +39,6 @@ import { useCashoutEstimator, useCashoutEstimatorForClass, useConversionInfo, - useConversionInfoForClass, useConversionRateClassDetails, useConversionRateClassUsers, } from "../hooks/regional.js"; @@ -431,10 +430,12 @@ function Form({ minimum={form?.conv?.cashin_min_amount} ratio={form?.conv?.cashin_ratio} rounding={form?.conv?.cashin_rounding_mode} + tiny={undefined} fallback_fee={default_rate.cashin_fee.split(":")[1]} fallback_minimum={default_rate.cashin_min_amount.split(":")[1]} fallback_ratio={default_rate.cashin_ratio} fallback_rounding={default_rate.cashin_rounding_mode} + fallback_tiny={default_rate.cashin_tiny_amount} /> )} @@ -448,10 +449,12 @@ function Form({ minimum={form?.conv?.cashout_min_amount} ratio={form?.conv?.cashout_ratio} rounding={form?.conv?.cashout_rounding_mode} + tiny={undefined} fallback_fee={default_rate.cashout_fee.split(":")[1]} fallback_minimum={default_rate.cashout_min_amount.split(":")[1]} fallback_ratio={default_rate.cashout_ratio} fallback_rounding={default_rate.cashout_rounding_mode} + fallback_tiny={default_rate.cashout_tiny_amount} /> </Fragment> )} @@ -775,7 +778,7 @@ function TestConversionClass({ classId }: { classId: number }): VNode { const { i18n } = useTranslationContext(); const [notification, notify, handleError] = useLocalNotification(); - const result = useConversionInfoForClass(classId); + const result = useConversionInfo(); const info = result && !(result instanceof TalerError) && result.type === "ok" ? result.body diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx @@ -359,6 +359,7 @@ function useComponentState({ minimum={form?.conv?.cashin_min_amount} ratio={form?.conv?.cashin_ratio} rounding={form?.conv?.cashin_rounding_mode} + tiny={form?.conv?.cashin_tiny_amount} /> )} @@ -372,6 +373,7 @@ function useComponentState({ minimum={form?.conv?.cashout_min_amount} ratio={form?.conv?.cashout_ratio} rounding={form?.conv?.cashout_rounding_mode} + tiny={form?.conv?.cashout_tiny_amount} /> </Fragment> )} @@ -681,6 +683,21 @@ function createFormValidator( : Number.isNaN(cashout_ratio) ? i18n.str`Rnvalid` : undefined, + + cashin_tiny_amount: !state.conv.cashin_tiny_amount + ? i18n.str`Required` + : !cashin_tiny_amount + ? i18n.str`Invalid` + : +state.conv.cashin_tiny_amount == 0 + ? i18n.str`Must be > 0` + : undefined, + cashout_tiny_amount: !state.conv.cashout_tiny_amount + ? i18n.str`Required` + : !cashout_tiny_amount + ? i18n.str`Invalid` + : +state.conv.cashout_tiny_amount == 0 + ? i18n.str`Must be > 0` + : undefined, }), amount: !state.amount @@ -739,10 +756,12 @@ export function ConversionForm({ minimum, ratio, rounding, + tiny, fallback_fee, fallback_minimum, fallback_ratio, fallback_rounding, + fallback_tiny, }: { inputCurrency: string; outputCurrency: string; @@ -752,6 +771,8 @@ export function ConversionForm({ fallback_fee?: string; rounding: UIField | undefined; fallback_rounding?: string; + tiny: UIField | undefined; + fallback_tiny?: string; ratio: UIField | undefined; fallback_ratio?: string; id: string; @@ -833,6 +854,31 @@ export function ConversionForm({ <div class="sm:col-span-5"> <label class="block text-sm font-medium leading-6 text-gray-900" + for={`${id}_tiny_amount`} + > + {i18n.str`Tiny amount`} + </label> + <InputAmount + name={`${id}_tiny_amount`} + left + currency={inputCurrency} + value={tiny?.value ?? ""} + onChange={tiny?.onUpdate} + placeholder={fallback_tiny ?? "0.01"} + /> + <ShowInputErrorLabel + message={tiny?.error} + isDirty={tiny?.value !== undefined} + /> + </div> + </div> + </div> + + <div class="px-6 pt-6"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" for={`${id}_channel`} > {i18n.str`Rounding mode`} @@ -1142,7 +1188,8 @@ export function ConversionForm({ <p class="text-gray-900 my-4"> <i18n.Translate> With the "up" mode the value will be rounded to 1.3 - </i18n.Translate>.0 + </i18n.Translate> + .0 </p> </details> </section> diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -44,6 +44,7 @@ import { useBankState } from "../../hooks/bank-state.js"; import { TransferCalculation, useCashoutEstimatorByUser, + useConversionInfo, useConversionRateForUser, } from "../../hooks/regional.js"; import { useSessionState } from "../../hooks/session.js"; @@ -85,23 +86,10 @@ export function CreateCashout({ routeClose, }: Props): VNode { const { i18n } = useTranslationContext(); - const resultAccount = useAccountDetails(accountName); - const { - estimateByCredit: calculateFromCredit, - estimateByDebit: calculateFromDebit, - } = useCashoutEstimatorByUser(accountName); - const { state: credentials } = useSessionState(); - const creds = credentials.status !== "loggedIn" ? undefined : credentials; - const [, updateBankState] = useBankState(); - const { lib: { bank: api }, config, } = useBankCoreApiContext(); - const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); - const [notification, notify, handleError] = useLocalNotification(); - const resp = useConversionRateForUser(accountName, creds?.token); - if (!config.allow_conversion) { return ( <Fragment> @@ -123,13 +111,24 @@ export function CreateCashout({ ); } + const resultAccount = useAccountDetails(accountName); + const { + estimateByCredit: calculateFromCredit, + estimateByDebit: calculateFromDebit, + } = useCashoutEstimatorByUser(accountName); + const { state: credentials } = useSessionState(); + const creds = credentials.status !== "loggedIn" ? undefined : credentials; + const [, updateBankState] = useBankState(); + const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); + const [notification, notify, handleError] = useLocalNotification(); + const rateResp = useConversionRateForUser(accountName, creds?.token); + const conversionResp = useConversionInfo(); + if (!resultAccount) { return <Loading />; - } - if (resultAccount instanceof TalerError) { + } else if (resultAccount instanceof TalerError) { return <ErrorLoadingWithDebug error={resultAccount} />; - } - if (resultAccount.type === "fail") { + } else if (resultAccount.type === "fail") { switch (resultAccount.case) { case HttpStatusCode.Unauthorized: return ( @@ -149,15 +148,40 @@ export function CreateCashout({ assertUnreachable(resultAccount); } } - if (!resp) { + + if (!conversionResp) { return <Loading />; + } else if (conversionResp instanceof TalerError) { + return <ErrorLoadingWithDebug error={conversionResp} />; + } else if (conversionResp.type === "fail") { + switch (conversionResp.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(conversionResp); + } } + const { + fiat_currency, + regional_currency, + fiat_currency_specification, + regional_currency_specification, + } = conversionResp.body; - if (resp instanceof TalerError) { - return <ErrorLoadingWithDebug error={resp} />; - } - if (resp.type === "fail") { - switch (resp.case) { + if (!rateResp) { + return <Loading />; + } else if (rateResp instanceof TalerError) { + return <ErrorLoadingWithDebug error={rateResp} />; + } else if (rateResp.type === "fail") { + switch (rateResp.case) { case HttpStatusCode.NotImplemented: { return ( <Attention type="danger" title={i18n.str`Cashout is disabled`}> @@ -169,23 +193,16 @@ export function CreateCashout({ ); } default: - assertUnreachable(resp); + assertUnreachable(rateResp); } } - const rate = resp.body; - + const rate = rateResp.body; if (!rate) { return ( <div>conversion enabled but server replied without conversion_rate</div> ); } - const { - fiat_currency, - currency: regional_currency, - fiat_currency_specification, - currency_specification: regional_currency_specification, - } = config; const regionalZero = Amounts.zeroOfCurrency(regional_currency); const fiatZero = Amounts.zeroOfCurrency(fiat_currency!); diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts @@ -110,7 +110,6 @@ export class TalerBankConversionHttpClient { if (auth) { headers.Authorization = makeBearerTokenAuthHeader(auth); } - console.log(auth) const resp = await this.httpLib.fetch(url.href, { method: "GET", headers diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -118,14 +118,6 @@ export interface TalerCorebankConfigResponse { // How the bank SPA should render this currency. currency_specification: CurrencySpecification; - // External currency used during conversion. - // None if conversion is disabled - fiat_currency?: string; - - // How the bank SPA should render this currency. - // None if conversion is disabled - fiat_currency_specification?: CurrencySpecification; - // TAN channels supported by the server supported_tan_channels?: TanChannel[]; @@ -857,8 +849,6 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankConfigResponse> => .property("default_debit_threshold", codecOptional(codecForAmountString())) .property("currency", codecForString()) .property("currency_specification", codecForCurrencySpecificiation()) - .property("fiat_currency", codecOptional(codecForString())) - .property("fiat_currency_specification", codecOptional(codecForCurrencySpecificiation())) .property( "supported_tan_channels", codecOptional(