diff options
Diffstat (limited to 'packages/demobank-ui/src')
-rw-r--r-- | packages/demobank-ui/src/declaration.d.ts | 8 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/access.ts | 17 | ||||
-rw-r--r-- | packages/demobank-ui/src/hooks/circuit.ts | 6 | ||||
-rw-r--r-- | packages/demobank-ui/src/index.tsx | 1 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage.tsx | 67 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BusinessAccount.tsx | 20 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaymentOptions.tsx | 7 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx | 35 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WalletWithdrawForm.tsx | 25 |
9 files changed, 99 insertions, 87 deletions
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index 8dc5fd8b2..4d8484a4f 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -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. diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 8282210d4..750b95fa0 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -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 }; } diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 423ed1a5b..548862d85 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -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 }; diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx index a0ce8cb59..2e0f740fe 100644 --- a/packages/demobank-ui/src/index.tsx +++ b/packages/demobank-ui/src/index.tsx @@ -15,7 +15,6 @@ */ import App from "./components/app.js"; - import { h, render } from "preact"; import "./scss/main.scss"; diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx index bd9a5acd7..c6ec7c88e 100644 --- a/packages/demobank-ui/src/pages/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/AccountPage.tsx @@ -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> - </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> - )} + <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> - </section> - <section id="payments"> - <div class="payments"> - <h2>{i18n.str`Payments`}</h2> - <PaymentOptions currency={balance.currency} /> + ) : ( + <div class="large-amount amount"> + {balanceIsDebit ? <b>-</b> : null} + <span class="value">{`${Amounts.stringifyValue(balance)}`}</span> + + <span class="currency">{`${balance.currency}`}</span> </div> - </section> - </Fragment> - )} + )} + </div> + </section> + <section id="payments"> + <div class="payments"> + <h2>{i18n.str`Payments`}</h2> + <PaymentOptions limit={limit} /> + </div> + </section> <section style={{ marginTop: "2em" }}> <div class="active"> diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 9bd799746..128b47114 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -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, }); diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 610efafc0..291f2aa9e 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -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, diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index f25680481..027f8e25a 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -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> diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index c1ad2f0cf..8bbfe0713 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -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 |