standard Amount field and add more validation (neg values)

This commit is contained in:
Sebastian 2022-11-07 14:38:42 -03:00
parent 3eafb64912
commit 6f3cd16343
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
11 changed files with 203 additions and 83 deletions

View File

@ -0,0 +1,67 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { Fragment, h, VNode } from "preact";
import { TextFieldHandler } from "../mui/handlers.js";
import { TextField } from "../mui/TextField.js";
import { ErrorText } from "./styled/index.js";
export function AmountField({
label,
handler,
currency,
required,
}: {
label: VNode;
required?: boolean;
currency: string;
handler: TextFieldHandler;
}): VNode {
function positiveAmount(value: string): string {
if (!value) return "";
try {
const num = Number.parseFloat(value);
if (Number.isNaN(num) || num < 0) return handler.value;
if (handler.onInput) {
handler.onInput(value);
}
return value;
} catch (e) {
// do nothing
}
return handler.value;
}
return (
<Fragment>
<TextField
label={label}
type="number"
min="0"
step="0.1"
variant="filled"
error={!!handler.error}
required={required}
startAdornment={
<div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div>
}
value={handler.value}
disabled={!handler.onInput}
onInput={positiveAmount}
/>
{handler.error && <ErrorText>{handler.error}</ErrorText>}
</Fragment>
);
}

View File

@ -40,7 +40,9 @@ export interface Props {
minRows?: number; minRows?: number;
multiline?: boolean; multiline?: boolean;
onChange?: (s: string) => void; onChange?: (s: string) => void;
onInput?: (s: string) => string;
min?: string; min?: string;
step?: string;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;

View File

@ -16,7 +16,7 @@
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
export interface TextFieldHandler { export interface TextFieldHandler {
onInput: (value: string) => Promise<void>; onInput?: (value: string) => Promise<void>;
value: string; value: string;
error?: string; error?: string;
} }

View File

@ -189,6 +189,7 @@ export function InputBase({
Root = InputBaseRoot, Root = InputBaseRoot,
Input, Input,
onChange, onChange,
onInput,
name, name,
placeholder, placeholder,
readOnly, readOnly,
@ -254,6 +255,19 @@ export function InputBase({
} }
}; };
const handleInput = (
event: JSX.TargetedEvent<HTMLElement & { value?: string }>,
): void => {
// if (inputPropsProp.onChange) {
// inputPropsProp.onChange(event, ...args);
// }
// Perform in the willUpdate
if (onInput) {
event.currentTarget.value = onInput(event.currentTarget.value);
}
};
const handleClick = ( const handleClick = (
event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>, event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>,
): void => { ): void => {
@ -290,6 +304,7 @@ export function InputBase({
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
type={type} type={type}
onInput={handleInput}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
@ -345,6 +360,7 @@ export function TextareaAutoSize({
// disabled, // disabled,
// size, // size,
onChange, onChange,
onInput,
value, value,
multiline, multiline,
focused, focused,
@ -480,7 +496,18 @@ export function TextareaAutoSize({
} }
if (onChange) { if (onChange) {
onChange(event); onChange(event.target.value);
}
};
const handleInput = (event: any): void => {
renders.current = 0;
if (!isControlled) {
syncHeight();
}
if (onInput) {
event.currentTarget.value = onInput(event.currentTarget.value);
} }
}; };
@ -498,6 +525,7 @@ export function TextareaAutoSize({
].join(" ")} ].join(" ")}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
onInput={handleInput}
ref={inputRef} ref={inputRef}
// Apply the rows prop to get a "correct" first SSR paint // Apply the rows prop to get a "correct" first SSR paint
rows={minRows} rows={minRows}

View File

@ -129,6 +129,8 @@ describe("CreateManualWithdraw states", () => {
expect(parsedAmount).equal(undefined); expect(parsedAmount).equal(undefined);
expect(amount.onInput).not.undefined;
if (!amount.onInput) return;
amount.onInput("12"); amount.onInput("12");
} }
@ -188,6 +190,8 @@ async function defaultTestForInputText(
const field = getField(); const field = getField();
const initialValue = field.value; const initialValue = field.value;
nextValue = `${initialValue} something else`; nextValue = `${initialValue} something else`;
expect(field.onInput).not.undefined;
if (!field.onInput) return;
field.onInput(nextValue); field.onInput(nextValue);
} }

View File

@ -260,7 +260,7 @@ export function CreateManualWithdraw({
<input <input
type="number" type="number"
value={state.amount.value} value={state.amount.value}
onInput={(e) => state.amount.onInput(e.currentTarget.value)} // onInput={(e) => state.amount.onInput(e.currentTarget.value)}
/> />
</div> </div>
</InputWithLabel> </InputWithLabel>

View File

@ -49,7 +49,6 @@ export function useComponentState(
parsed !== undefined ? Amounts.stringifyValue(parsed) : "0"; parsed !== undefined ? Amounts.stringifyValue(parsed) : "0";
// const [accountIdx, setAccountIdx] = useState<number>(0); // const [accountIdx, setAccountIdx] = useState<number>(0);
const [amount, setAmount] = useState(initialValue); const [amount, setAmount] = useState(initialValue);
const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined); const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined);
@ -124,6 +123,16 @@ export function useComponentState(
const firstAccount = accounts[0].uri const firstAccount = accounts[0].uri
const currentAccount = !selectedAccount ? firstAccount : selectedAccount; const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
if (fee === undefined && parsedAmount) {
getFeeForAmount(currentAccount, parsedAmount, api).then(initialFee => {
setFee(initialFee)
})
return {
status: "loading",
error: undefined,
};
}
const accountMap = createLabelsForBankAccount(accounts); const accountMap = createLabelsForBankAccount(accounts);
async function updateAccountFromList(accountStr: string): Promise<void> { async function updateAccountFromList(accountStr: string): Promise<void> {

View File

@ -167,6 +167,11 @@ describe("DepositPage states", () => {
accounts: [ibanPayto], accounts: [ibanPayto],
}, },
); );
handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit,
undefined,
withoutFee(),
);
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook(() => useComponentState(props, mock)); mountHook(() => useComponentState(props, mock));
@ -176,6 +181,11 @@ describe("DepositPage states", () => {
expect(status).equal("loading"); expect(status).equal("loading");
} }
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
{ {
@ -214,6 +224,12 @@ describe("DepositPage states", () => {
accounts: [talerBankPayto, ibanPayto], accounts: [talerBankPayto, ibanPayto],
}, },
); );
handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit, WalletApiOperation.GetFeeForDeposit,
undefined, undefined,
@ -238,6 +254,12 @@ describe("DepositPage states", () => {
expect(status).equal("loading"); expect(status).equal("loading");
} }
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@ -361,6 +383,11 @@ describe("DepositPage states", () => {
accounts: [talerBankPayto, ibanPayto], accounts: [talerBankPayto, ibanPayto],
}, },
); );
handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit,
undefined,
withoutFee(),
);
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.GetFeeForDeposit, WalletApiOperation.GetFeeForDeposit,
undefined, undefined,
@ -380,6 +407,13 @@ describe("DepositPage states", () => {
expect(status).equal("loading"); expect(status).equal("loading");
} }
expect(await waitForStateUpdate()).true;
{
const { status } = pullLastResultOrThrow();
expect(status).equal("loading");
}
expect(await waitForStateUpdate()).true; expect(await waitForStateUpdate()).true;
const accountSelected = stringifyPaytoUri(ibanPayto.uri); const accountSelected = stringifyPaytoUri(ibanPayto.uri);
@ -409,6 +443,8 @@ describe("DepositPage states", () => {
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.amount.onInput).not.undefined;
if (!r.amount.onInput) return;
r.amount.onInput("10"); r.amount.onInput("10");
} }

View File

@ -16,6 +16,7 @@
import { Amounts, PaytoUri } from "@gnu-taler/taler-util"; import { Amounts, PaytoUri } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js";
import { ErrorMessage } from "../../components/ErrorMessage.js"; import { ErrorMessage } from "../../components/ErrorMessage.js";
import { LoadingError } from "../../components/LoadingError.js"; import { LoadingError } from "../../components/LoadingError.js";
import { SelectList } from "../../components/SelectList.js"; import { SelectList } from "../../components/SelectList.js";
@ -28,6 +29,7 @@ import {
} from "../../components/styled/index.js"; } from "../../components/styled/index.js";
import { useTranslationContext } from "../../context/translation.js"; import { useTranslationContext } from "../../context/translation.js";
import { Button } from "../../mui/Button.js"; import { Button } from "../../mui/Button.js";
import { Grid } from "../../mui/Grid.js";
import { State } from "./index.js"; import { State } from "./index.js";
export function LoadingErrorView({ error }: State.LoadingUriError): VNode { export function LoadingErrorView({ error }: State.LoadingUriError): VNode {
@ -167,48 +169,33 @@ export function ReadyView(state: State.Ready): VNode {
<p> <p>
<AccountDetails account={state.currentAccount} /> <AccountDetails account={state.currentAccount} />
</p> </p>
<InputWithLabel invalid={!!state.amount.error}> <Grid container spacing={2} columns={1}>
<label> <Grid item xs={1}>
<i18n.Translate>Amount</i18n.Translate> <AmountField
</label> label={<i18n.Translate>Amount</i18n.Translate>}
<div> currency={state.currency}
<span>{state.currency}</span> handler={state.amount}
<input
type="number"
value={state.amount.value}
onInput={(e) => state.amount.onInput(e.currentTarget.value)}
/> />
</div> </Grid>
{state.amount.error && <ErrorText>{state.amount.error}</ErrorText>} <Grid item xs={1}>
</InputWithLabel> <AmountField
label={<i18n.Translate>Deposit fee</i18n.Translate>}
<InputWithLabel> currency={state.currency}
<label> handler={{
<i18n.Translate>Deposit fee</i18n.Translate> value: Amounts.stringifyValue(state.totalFee),
</label> }}
<div>
<span>{state.currency}</span>
<input
type="number"
disabled
value={Amounts.stringifyValue(state.totalFee)}
/> />
</div> </Grid>
</InputWithLabel> <Grid item xs={1}>
<AmountField
<InputWithLabel> label={<i18n.Translate>Total deposit</i18n.Translate>}
<label> currency={state.currency}
<i18n.Translate>Total deposit</i18n.Translate> handler={{
</label> value: Amounts.stringifyValue(state.totalToDeposit),
<div> }}
<span>{state.currency}</span>
<input
type="number"
disabled
value={Amounts.stringifyValue(state.totalToDeposit)}
/> />
</div> </Grid>
</InputWithLabel> </Grid>
</section> </section>
<footer> <footer>
<Button <Button

View File

@ -19,6 +19,7 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { styled } from "@linaria/react"; import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AmountField } from "../components/AmountField.js";
import { Loading } from "../components/Loading.js"; import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js"; import { LoadingError } from "../components/LoadingError.js";
import { SelectList } from "../components/SelectList.js"; import { SelectList } from "../components/SelectList.js";
@ -32,6 +33,7 @@ import { useTranslationContext } from "../context/translation.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js"; import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js"; import { Grid } from "../mui/Grid.js";
import { TextFieldHandler } from "../mui/handlers.js";
import { Paper } from "../mui/Paper.js"; import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js"; import { TextField } from "../mui/TextField.js";
import { Pages } from "../NavigationBar.js"; import { Pages } from "../NavigationBar.js";
@ -283,11 +285,6 @@ export function DestinationSelectionGetCash({
const [currency, setCurrency] = useState(parsedInitialAmount?.currency); const [currency, setCurrency] = useState(parsedInitialAmount?.currency);
const [amount, setAmount] = useState(parsedInitialAmountValue); const [amount, setAmount] = useState(parsedInitialAmountValue);
function positiveSetAmount(e: string):void {
const value = Number.parseInt(e, 10);
if (value < 0) return
setAmount(String(value))
}
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const previous1: Contact[] = []; const previous1: Contact[] = [];
const previous2: Contact[] = [ const previous2: Contact[] = [
@ -326,19 +323,13 @@ export function DestinationSelectionGetCash({
<i18n.Translate>Specify the amount and the origin</i18n.Translate> <i18n.Translate>Specify the amount and the origin</i18n.Translate>
</h1> </h1>
<Grid container columns={2} justifyContent="space-between"> <Grid container columns={2} justifyContent="space-between">
<TextField <AmountField
label="Amount" label={<i18n.Translate>Amount</i18n.Translate>}
type="number" currency={currency}
min="0"
variant="filled"
error={invalid}
required required
startAdornment={ handler={{
<div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div> onInput: async (s) => setAmount(s),
} value: amount,
value={amount}
onChange={(e) => {
setAmount(e);
}} }}
/> />
<Button onClick={async () => setCurrency(undefined)}> <Button onClick={async () => setCurrency(undefined)}>
@ -431,11 +422,6 @@ export function DestinationSelectionSendCash({
const currency = parsedInitialAmount?.currency; const currency = parsedInitialAmount?.currency;
const [amount, setAmount] = useState(parsedInitialAmountValue); const [amount, setAmount] = useState(parsedInitialAmountValue);
function positiveSetAmount(e: string):void {
const value = Number.parseInt(e, 10);
if (value < 0) return
setAmount(String(value))
}
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const previous1: Contact[] = []; const previous1: Contact[] = [];
const previous2: Contact[] = [ const previous2: Contact[] = [
@ -474,19 +460,13 @@ export function DestinationSelectionSendCash({
</h1> </h1>
<div> <div>
<TextField <AmountField
label="Amount" label={<i18n.Translate>Amount</i18n.Translate>}
type="number" currency={currency}
min="0"
variant="filled"
required required
error={invalid} handler={{
startAdornment={ onInput: async (s) => setAmount(s),
<div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div> value: amount,
}
value={amount}
onChange={(e) => {
positiveSetAmount(e);
}} }}
/> />
</div> </div>

View File

@ -416,9 +416,10 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
fullWidth fullWidth
value={value} value={value}
error={value !== undefined && !!errors?.value} error={value !== undefined && !!errors?.value}
disabled={!field.onInput}
onChange={(v) => { onChange={(v) => {
setValue(v); setValue(v);
if (!errors) { if (!errors && field.onInput) {
field.onInput(`payto://bitcoin/${v}`); field.onInput(`payto://bitcoin/${v}`);
} }
}} }}
@ -456,9 +457,10 @@ function TalerBankAddressAccount({
fullWidth fullWidth
value={host} value={host}
error={host !== undefined && !!errors?.host} error={host !== undefined && !!errors?.host}
disabled={!field.onInput}
onChange={(v) => { onChange={(v) => {
setHost(v); setHost(v);
if (!errors) { if (!errors && field.onInput) {
field.onInput(`payto://x-taler-bank/${v}/${account}`); field.onInput(`payto://x-taler-bank/${v}/${account}`);
} }
}} }}
@ -470,11 +472,12 @@ function TalerBankAddressAccount({
label="Bank account" label="Bank account"
variant="standard" variant="standard"
fullWidth fullWidth
disabled={!field.onInput}
value={account} value={account}
error={account !== undefined && !!errors?.account} error={account !== undefined && !!errors?.account}
onChange={(v) => { onChange={(v) => {
setAccount(v || ""); setAccount(v || "");
if (!errors) { if (!errors && field.onInput) {
field.onInput(`payto://x-taler-bank/${host}/${v}`); field.onInput(`payto://x-taler-bank/${host}/${v}`);
} }
}} }}
@ -502,9 +505,10 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
fullWidth fullWidth
value={number} value={number}
error={number !== undefined && !!errors?.number} error={number !== undefined && !!errors?.number}
disabled={!field.onInput}
onChange={(v) => { onChange={(v) => {
setNumber(v); setNumber(v);
if (!errors) { if (!errors && field.onInput) {
field.onInput(`payto://iban/${v}?receiver-name=${name}`); field.onInput(`payto://iban/${v}?receiver-name=${name}`);
} }
}} }}
@ -518,10 +522,13 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
fullWidth fullWidth
value={name} value={name}
error={name !== undefined && !!errors?.name} error={name !== undefined && !!errors?.name}
disabled={!field.onInput}
onChange={(v) => { onChange={(v) => {
setName(v); setName(v);
if (!errors) { if (!errors && field.onInput) {
field.onInput(`payto://iban/${number}?receiver-name=${encodeURIComponent(v)}`); field.onInput(
`payto://iban/${number}?receiver-name=${encodeURIComponent(v)}`,
);
} }
}} }}
/> />