commit 41eb1e4ef4823d38e825697d61ea7635d74582c9
parent e86f7d14882fe75e5ef008cee5c0dbed28ee91b1
Author: Antoine A <>
Date: Tue, 22 Jul 2025 14:41:26 +0200
bank-ui: conversion rate setup
Diffstat:
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(