This commit is contained in:
Sebastian 2023-03-11 18:19:38 -03:00
parent b72729f065
commit c67d94c56e
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
10 changed files with 147 additions and 88 deletions

View File

@ -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.

View File

@ -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 };
}

View File

@ -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 };

View File

@ -15,7 +15,6 @@
*/
import App from "./components/app.js";
import { h, render } from "preact";
import "./scss/main.scss";

View File

@ -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>
&nbsp;
<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>
&nbsp;
<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">

View File

@ -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,
});

View File

@ -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,

View File

@ -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}
/>
&nbsp;
<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>

View File

@ -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}
/>
&nbsp;
<input

View File

@ -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);
}
}