fix: #7753
This commit is contained in:
parent
b72729f065
commit
c67d94c56e
8
packages/demobank-ui/src/declaration.d.ts
vendored
8
packages/demobank-ui/src/declaration.d.ts
vendored
@ -169,6 +169,8 @@ namespace SandboxBackend {
|
||||
balance: Balance;
|
||||
// payto://-URI of the account. (New)
|
||||
paytoUri: string;
|
||||
// Number indicating the max debit allowed for the requesting user.
|
||||
debitThreshold: Amount;
|
||||
}
|
||||
interface BankAccountCreateWithdrawalRequest {
|
||||
// Amount to withdraw.
|
||||
@ -369,6 +371,9 @@ namespace SandboxBackend {
|
||||
// Contains ratios and fees related to buying
|
||||
// and selling the circuit currency.
|
||||
ratios_and_fees: RatiosAndFees;
|
||||
// Fiat currency. That is the currency in which
|
||||
// cash-out operations ultimately wire money.
|
||||
fiat_currency: string;
|
||||
}
|
||||
interface RatiosAndFees {
|
||||
// Exchange rate to buy the circuit currency from fiat.
|
||||
@ -379,9 +384,6 @@ namespace SandboxBackend {
|
||||
buy_in_fee: float;
|
||||
// Fee to subtract after applying the sell ratio.
|
||||
sell_out_fee: float;
|
||||
// Fiat currency. That is the currency in which
|
||||
// cash-out operations ultimately wire money.
|
||||
fiat_currency: string;
|
||||
}
|
||||
interface Cashouts {
|
||||
// Every string represents a cash-out operation UUID.
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
|
||||
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||
import _useSWR, { SWRHook } from "swr";
|
||||
import { Amounts } from "@gnu-taler/taler-util";
|
||||
const useSWR = _useSWR as unknown as SWRHook;
|
||||
|
||||
export function useAccessAPI(): AccessAPI {
|
||||
@ -180,7 +181,21 @@ export function useAccountDetails(
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
if (data) return data;
|
||||
//FIXME: remove optional when libeufin sandbox has implemented the feature
|
||||
if (data && typeof data.data.debitThreshold === "undefined") {
|
||||
data.data.debitThreshold = "100";
|
||||
}
|
||||
//FIXME: sandbox server should return amount string
|
||||
if (data) {
|
||||
const d = structuredClone(data);
|
||||
const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
|
||||
d.data.debitThreshold = Amounts.stringify({
|
||||
currency,
|
||||
value: Number.parseInt(d.data.debitThreshold, 10),
|
||||
fraction: 0,
|
||||
});
|
||||
return d;
|
||||
}
|
||||
if (error) return error.info;
|
||||
return { loading: true };
|
||||
}
|
||||
|
@ -299,12 +299,6 @@ export function useRatiosAndFeeConfig(): HttpResponse<
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
// data.data.ratios_and_fees.sell_out_fee = 2
|
||||
if (!data.data.ratios_and_fees.fiat_currency) {
|
||||
data.data.ratios_and_fees.fiat_currency = "FIAT";
|
||||
}
|
||||
}
|
||||
if (data) return data;
|
||||
if (error) return error.info;
|
||||
return { loading: true };
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import App from "./components/app.js";
|
||||
|
||||
import { h, render } from "preact";
|
||||
import "./scss/main.scss";
|
||||
|
||||
|
@ -20,8 +20,6 @@ import {
|
||||
useTranslationContext,
|
||||
} from "@gnu-taler/web-util/lib/index.browser";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Cashouts } from "../components/Cashouts/index.js";
|
||||
import { Transactions } from "../components/Transactions/index.js";
|
||||
import { useAccountDetails } from "../hooks/access.js";
|
||||
import { PaymentOptions } from "./PaymentOptions.js";
|
||||
@ -44,8 +42,8 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
|
||||
}
|
||||
|
||||
const { data } = result;
|
||||
const balance = Amounts.parse(data.balance.amount);
|
||||
const errorParsingBalance = !balance;
|
||||
const balance = Amounts.parseOrThrow(data.balance.amount);
|
||||
const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
|
||||
const payto = parsePaytoUri(data.paytoUri);
|
||||
if (!payto || !payto.isKnown || payto.targetType !== "iban") {
|
||||
return (
|
||||
@ -54,7 +52,9 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
|
||||
}
|
||||
const accountNumber = payto.iban;
|
||||
const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
|
||||
|
||||
const limit = balanceIsDebit
|
||||
? Amounts.sub(debitThreshold, balance).amount
|
||||
: Amounts.add(balance, debitThreshold).amount;
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
@ -66,44 +66,29 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{errorParsingBalance ? (
|
||||
<div class="informational informational-fail" style={{ marginTop: 8 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p>
|
||||
<b>Server Error: invalid balance</b>
|
||||
</p>
|
||||
</div>
|
||||
<p>Your account is in an invalid state.</p>
|
||||
<section id="assets">
|
||||
<div class="asset-summary">
|
||||
<h2>{i18n.str`Bank account balance`}</h2>
|
||||
{!balance ? (
|
||||
<div class="large-amount" style={{ color: "gray" }}>
|
||||
Waiting server response...
|
||||
</div>
|
||||
) : (
|
||||
<div class="large-amount amount">
|
||||
{balanceIsDebit ? <b>-</b> : null}
|
||||
<span class="value">{`${Amounts.stringifyValue(balance)}`}</span>
|
||||
|
||||
<span class="currency">{`${balance.currency}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Fragment>
|
||||
<section id="assets">
|
||||
<div class="asset-summary">
|
||||
<h2>{i18n.str`Bank account balance`}</h2>
|
||||
{!balance ? (
|
||||
<div class="large-amount" style={{ color: "gray" }}>
|
||||
Waiting server response...
|
||||
</div>
|
||||
) : (
|
||||
<div class="large-amount amount">
|
||||
{balanceIsDebit ? <b>-</b> : null}
|
||||
<span class="value">{`${Amounts.stringifyValue(
|
||||
balance,
|
||||
)}`}</span>
|
||||
|
||||
<span class="currency">{`${balance.currency}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section id="payments">
|
||||
<div class="payments">
|
||||
<h2>{i18n.str`Payments`}</h2>
|
||||
<PaymentOptions currency={balance.currency} />
|
||||
</div>
|
||||
</section>
|
||||
</Fragment>
|
||||
)}
|
||||
</section>
|
||||
<section id="payments">
|
||||
<div class="payments">
|
||||
<h2>{i18n.str`Payments`}</h2>
|
||||
<PaymentOptions limit={limit} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style={{ marginTop: "2em" }}>
|
||||
<div class="active">
|
||||
|
@ -212,8 +212,7 @@ function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
|
||||
oldResult.ratios_and_fees.sell_at_ratio ||
|
||||
result.data.ratios_and_fees.sell_out_fee !==
|
||||
oldResult.ratios_and_fees.sell_out_fee ||
|
||||
result.data.ratios_and_fees.fiat_currency !==
|
||||
oldResult.ratios_and_fees.fiat_currency);
|
||||
result.data.fiat_currency !== oldResult.fiat_currency);
|
||||
|
||||
return {
|
||||
...result,
|
||||
@ -238,16 +237,19 @@ function CreateCashout({
|
||||
if (!result.ok) return onLoadNotOk(result);
|
||||
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
|
||||
const config = ratiosResult.data;
|
||||
const maybeBalance = Amounts.parse(result.data.balance.amount);
|
||||
if (!maybeBalance) return <div>error</div>;
|
||||
const balance = maybeBalance;
|
||||
const balance = Amounts.parseOrThrow(result.data.balance.amount);
|
||||
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||
const zero = Amounts.zeroOfCurrency(balance.currency);
|
||||
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
|
||||
const limit = balanceIsDebit
|
||||
? Amounts.sub(debitThreshold, balance).amount
|
||||
: Amounts.add(balance, debitThreshold).amount;
|
||||
|
||||
const sellRate = config.ratios_and_fees.sell_at_ratio;
|
||||
const sellFee = !config.ratios_and_fees.sell_out_fee
|
||||
? zero
|
||||
: Amounts.fromFloat(config.ratios_and_fees.sell_out_fee, balance.currency);
|
||||
const fiatCurrency = config.ratios_and_fees.fiat_currency;
|
||||
const fiatCurrency = config.fiat_currency;
|
||||
|
||||
if (!sellRate || sellRate < 0) return <div>error rate</div>;
|
||||
|
||||
@ -278,12 +280,12 @@ function CreateCashout({
|
||||
? i18n.str`required`
|
||||
: !amount
|
||||
? i18n.str`could not be parsed`
|
||||
: Amounts.cmp(balance, amount_debit) === -1
|
||||
: Amounts.cmp(limit, amount_debit) === -1
|
||||
? i18n.str`balance is not enough`
|
||||
: Amounts.cmp(credit_before_fee, sellFee) === -1
|
||||
? i18n.str`amount is not enough`
|
||||
? i18n.str`the total amount to transfer does not cover the fees`
|
||||
: Amounts.isZero(amount_credit)
|
||||
? i18n.str`amount is not enough`
|
||||
? i18n.str`the total transfer at destination will be zero`
|
||||
: undefined,
|
||||
channel: !form.channel ? i18n.str`required` : undefined,
|
||||
});
|
||||
|
@ -14,6 +14,7 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { AmountJson } from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -25,7 +26,7 @@ import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
|
||||
* Let the user choose a payment option,
|
||||
* then specify the details trigger the action.
|
||||
*/
|
||||
export function PaymentOptions({ currency }: { currency: string }): VNode {
|
||||
export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const { pageStateSetter } = usePageContext();
|
||||
|
||||
@ -62,7 +63,7 @@ export function PaymentOptions({ currency }: { currency: string }): VNode {
|
||||
<h3>{i18n.str`Obtain digital cash`}</h3>
|
||||
<WalletWithdrawForm
|
||||
focus
|
||||
currency={currency}
|
||||
limit={limit}
|
||||
onSuccess={(data) => {
|
||||
pageStateSetter((prevState: PageStateType) => ({
|
||||
...prevState,
|
||||
@ -80,7 +81,7 @@ export function PaymentOptions({ currency }: { currency: string }): VNode {
|
||||
<h3>{i18n.str`Transfer to bank account`}</h3>
|
||||
<PaytoWireTransferForm
|
||||
focus
|
||||
currency={currency}
|
||||
limit={limit}
|
||||
onSuccess={() => {
|
||||
pageStateSetter((prevState: PageStateType) => ({
|
||||
...prevState,
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
buildPayto,
|
||||
HttpStatusCode,
|
||||
@ -30,7 +31,11 @@ import { h, VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { PageStateType } from "../context/pageState.js";
|
||||
import { useAccessAPI } from "../hooks/access.js";
|
||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||
import {
|
||||
buildRequestErrorMessage,
|
||||
undefinedIfEmpty,
|
||||
validateIBAN,
|
||||
} from "../utils.js";
|
||||
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
|
||||
|
||||
const logger = new Logger("PaytoWireTransferForm");
|
||||
@ -39,12 +44,12 @@ export function PaytoWireTransferForm({
|
||||
focus,
|
||||
onError,
|
||||
onSuccess,
|
||||
currency,
|
||||
limit,
|
||||
}: {
|
||||
focus?: boolean;
|
||||
onError: (e: PageStateType["error"]) => void;
|
||||
onSuccess: () => void;
|
||||
currency: string;
|
||||
limit: AmountJson;
|
||||
}): VNode {
|
||||
// const backend = useBackendContext();
|
||||
// const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
|
||||
@ -65,7 +70,8 @@ export function PaytoWireTransferForm({
|
||||
if (focus) ref.current?.focus();
|
||||
}, [focus, isRawPayto]);
|
||||
|
||||
let parsedAmount = undefined;
|
||||
const trimmedAmountStr = amount?.trim();
|
||||
const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
|
||||
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
||||
|
||||
const errorsWire = undefinedIfEmpty({
|
||||
@ -73,14 +79,16 @@ export function PaytoWireTransferForm({
|
||||
? i18n.str`Missing IBAN`
|
||||
: !IBAN_REGEX.test(iban)
|
||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||
: undefined,
|
||||
: validateIBAN(iban, i18n),
|
||||
subject: !subject ? i18n.str`Missing subject` : undefined,
|
||||
amount: !amount
|
||||
amount: !trimmedAmountStr
|
||||
? i18n.str`Missing amount`
|
||||
: !(parsedAmount = Amounts.parse(`${currency}:${amount}`))
|
||||
: !parsedAmount
|
||||
? i18n.str`Amount is not valid`
|
||||
: Amounts.isZero(parsedAmount)
|
||||
? i18n.str`Should be greater than 0`
|
||||
: Amounts.cmp(limit, parsedAmount) === -1
|
||||
? i18n.str`balance is not enough`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@ -143,10 +151,10 @@ export function PaytoWireTransferForm({
|
||||
type="text"
|
||||
readonly
|
||||
class="currency-indicator"
|
||||
size={currency?.length}
|
||||
maxLength={currency?.length}
|
||||
size={limit.currency.length}
|
||||
maxLength={limit.currency.length}
|
||||
tabIndex={-1}
|
||||
value={currency}
|
||||
value={limit.currency}
|
||||
/>
|
||||
|
||||
<input
|
||||
@ -185,7 +193,7 @@ export function PaytoWireTransferForm({
|
||||
try {
|
||||
await createTransaction({
|
||||
paytoUri,
|
||||
amount: `${currency}:${amount}`,
|
||||
amount: `${limit.currency}:${amount}`,
|
||||
});
|
||||
onSuccess();
|
||||
setAmount(undefined);
|
||||
@ -257,7 +265,7 @@ export function PaytoWireTransferForm({
|
||||
? i18n.str`only "IBAN" target are supported`
|
||||
: !IBAN_REGEX.test(parsed.iban)
|
||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||
: undefined,
|
||||
: validateIBAN(parsed.iban, i18n),
|
||||
});
|
||||
|
||||
return (
|
||||
@ -296,7 +304,8 @@ export function PaytoWireTransferForm({
|
||||
<div style={{ fontSize: "small", marginTop: 4 }}>
|
||||
Hint:
|
||||
<code>
|
||||
payto://iban/[receiver-iban]?message=[subject]&amount=[{currency}
|
||||
payto://iban/[receiver-iban]?message=[subject]&amount=[
|
||||
{limit.currency}
|
||||
:X.Y]
|
||||
</code>
|
||||
</div>
|
||||
|
@ -14,7 +14,12 @@
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
HttpStatusCode,
|
||||
Logger,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
RequestError,
|
||||
useTranslationContext,
|
||||
@ -30,11 +35,11 @@ const logger = new Logger("WalletWithdrawForm");
|
||||
|
||||
export function WalletWithdrawForm({
|
||||
focus,
|
||||
currency,
|
||||
limit,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
currency: string;
|
||||
limit: AmountJson;
|
||||
focus?: boolean;
|
||||
onError: (e: PageStateType["error"]) => void;
|
||||
onSuccess: (
|
||||
@ -52,20 +57,20 @@ export function WalletWithdrawForm({
|
||||
if (focus) ref.current?.focus();
|
||||
}, [focus]);
|
||||
|
||||
// Beware: We never ever want to treat the amount as a float!
|
||||
|
||||
const trimmedAmountStr = amountStr?.trim();
|
||||
|
||||
const parsedAmount = trimmedAmountStr
|
||||
? Amounts.parse(`${currency}:${trimmedAmountStr}`)
|
||||
? Amounts.parse(`${limit.currency}:${trimmedAmountStr}`)
|
||||
: undefined;
|
||||
|
||||
const errors = undefinedIfEmpty({
|
||||
amount:
|
||||
trimmedAmountStr == null
|
||||
? i18n.str`required`
|
||||
: parsedAmount == null
|
||||
: !parsedAmount
|
||||
? i18n.str`invalid`
|
||||
: Amounts.cmp(limit, parsedAmount) === -1
|
||||
? i18n.str`balance is not enough`
|
||||
: undefined,
|
||||
});
|
||||
return (
|
||||
@ -87,10 +92,10 @@ export function WalletWithdrawForm({
|
||||
type="text"
|
||||
readonly
|
||||
class="currency-indicator"
|
||||
size={currency.length}
|
||||
maxLength={currency.length}
|
||||
size={limit.currency.length}
|
||||
maxLength={limit.currency.length}
|
||||
tabIndex={-1}
|
||||
value={currency}
|
||||
value={limit.currency}
|
||||
/>
|
||||
|
||||
<input
|
||||
|
@ -34,3 +34,50 @@ export function compose<SType extends { status: string }, PType>(
|
||||
return h();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param obj VNode
|
||||
* @returns
|
||||
*/
|
||||
export function saveVNodeForInspection<T>(obj: T): T {
|
||||
// @ts-ignore
|
||||
window["showVNodeInfo"] = function showVNodeInfo() {
|
||||
inspect(obj);
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
function inspect(obj: any) {
|
||||
if (!obj) return;
|
||||
if (obj.__c && obj.__c.__H) {
|
||||
const componentName = obj.__c.constructor.name;
|
||||
const hookState = obj.__c.__H;
|
||||
const stateList = hookState.__ as Array<any>;
|
||||
console.log("==============", componentName);
|
||||
stateList.forEach((hook) => {
|
||||
const { __: value, c: context, __h: factory, __H: args } = hook;
|
||||
if (typeof context !== "undefined") {
|
||||
const { __c: contextId } = context;
|
||||
console.log("context:", contextId, hook);
|
||||
} else if (typeof factory === "function") {
|
||||
console.log("memo:", value, "deps:", args);
|
||||
} else if (typeof value === "function") {
|
||||
const effectName = value.name;
|
||||
console.log("effect:", effectName, "deps:", args);
|
||||
} else if (typeof value.current !== "undefined") {
|
||||
const ref = value.current;
|
||||
console.log("ref:", ref instanceof Element ? ref.outerHTML : ref);
|
||||
} else if (value instanceof Array) {
|
||||
console.log("state:", value[0]);
|
||||
} else {
|
||||
console.log(hook);
|
||||
}
|
||||
});
|
||||
}
|
||||
const children = obj.__k;
|
||||
if (children instanceof Array) {
|
||||
children.forEach((e) => inspect(e));
|
||||
} else {
|
||||
inspect(children);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user