diff options
author | Sebastian <sebasjm@gmail.com> | 2022-11-07 14:38:42 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-11-07 14:38:42 -0300 |
commit | 6f3cd163431fecfa126f740ebfec1b5c5c74f5b7 (patch) | |
tree | ad00931c6dfa427b4f64d89baab7f2711f1864ce | |
parent | 3eafb64912411dd21696a8896e9314ceb49c1326 (diff) |
standard Amount field and add more validation (neg values)
11 files changed, 203 insertions, 83 deletions
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.tsx b/packages/taler-wallet-webextension/src/components/AmountField.tsx new file mode 100644 index 000000000..79c510d2f --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx @@ -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> + ); +} diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx index 1c1f5cc49..7c6eb40a2 100644 --- a/packages/taler-wallet-webextension/src/mui/TextField.tsx +++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx @@ -40,7 +40,9 @@ export interface Props { minRows?: number; multiline?: boolean; onChange?: (s: string) => void; + onInput?: (s: string) => string; min?: string; + step?: string; placeholder?: string; required?: boolean; diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts index aa66e75a8..9d393e5b7 100644 --- a/packages/taler-wallet-webextension/src/mui/handlers.ts +++ b/packages/taler-wallet-webextension/src/mui/handlers.ts @@ -16,7 +16,7 @@ import { TalerError } from "@gnu-taler/taler-wallet-core"; export interface TextFieldHandler { - onInput: (value: string) => Promise<void>; + onInput?: (value: string) => Promise<void>; value: string; error?: string; } diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx index 80f5bd44e..e1c6e7af1 100644 --- a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx +++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx @@ -189,6 +189,7 @@ export function InputBase({ Root = InputBaseRoot, Input, onChange, + onInput, name, placeholder, 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 = ( event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>, ): void => { @@ -290,6 +304,7 @@ export function InputBase({ onKeyDown={onKeyDown} onKeyUp={onKeyUp} type={type} + onInput={handleInput} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} @@ -345,6 +360,7 @@ export function TextareaAutoSize({ // disabled, // size, onChange, + onInput, value, multiline, focused, @@ -480,7 +496,18 @@ export function TextareaAutoSize({ } 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(" ")} value={value} onChange={handleChange} + onInput={handleInput} ref={inputRef} // Apply the rows prop to get a "correct" first SSR paint rows={minRows} diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts index c757610fc..37c50285b 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts @@ -129,6 +129,8 @@ describe("CreateManualWithdraw states", () => { expect(parsedAmount).equal(undefined); + expect(amount.onInput).not.undefined; + if (!amount.onInput) return; amount.onInput("12"); } @@ -188,6 +190,8 @@ async function defaultTestForInputText( const field = getField(); const initialValue = field.value; nextValue = `${initialValue} something else`; + expect(field.onInput).not.undefined; + if (!field.onInput) return; field.onInput(nextValue); } diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx index 5320c6fe2..dd80faccd 100644 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx @@ -260,7 +260,7 @@ export function CreateManualWithdraw({ <input type="number" value={state.amount.value} - onInput={(e) => state.amount.onInput(e.currentTarget.value)} + // onInput={(e) => state.amount.onInput(e.currentTarget.value)} /> </div> </InputWithLabel> diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 2693db79e..d8b752d44 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -49,7 +49,6 @@ export function useComponentState( parsed !== undefined ? Amounts.stringifyValue(parsed) : "0"; // const [accountIdx, setAccountIdx] = useState<number>(0); const [amount, setAmount] = useState(initialValue); - const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined); @@ -124,6 +123,16 @@ export function useComponentState( const firstAccount = accounts[0].uri 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); async function updateAccountFromList(accountStr: string): Promise<void> { diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 9f336ac1a..17e17d185 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -167,6 +167,11 @@ describe("DepositPage states", () => { accounts: [ibanPayto], }, ); + handler.addWalletCallResponse( + WalletApiOperation.GetFeeForDeposit, + undefined, + withoutFee(), + ); const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = mountHook(() => useComponentState(props, mock)); @@ -177,6 +182,11 @@ describe("DepositPage states", () => { } expect(await waitForStateUpdate()).true; + { + const { status } = pullLastResultOrThrow(); + expect(status).equal("loading"); + } + expect(await waitForStateUpdate()).true; { const r = pullLastResultOrThrow(); @@ -219,6 +229,12 @@ describe("DepositPage states", () => { undefined, withoutFee(), ); + + handler.addWalletCallResponse( + WalletApiOperation.GetFeeForDeposit, + undefined, + withoutFee(), + ); handler.addWalletCallResponse( WalletApiOperation.GetFeeForDeposit, undefined, @@ -239,6 +255,12 @@ describe("DepositPage states", () => { } expect(await waitForStateUpdate()).true; + { + const { status } = pullLastResultOrThrow(); + expect(status).equal("loading"); + } + + expect(await waitForStateUpdate()).true; const accountSelected = stringifyPaytoUri(ibanPayto.uri); { @@ -364,6 +386,11 @@ describe("DepositPage states", () => { handler.addWalletCallResponse( WalletApiOperation.GetFeeForDeposit, undefined, + withoutFee(), + ); + handler.addWalletCallResponse( + WalletApiOperation.GetFeeForDeposit, + undefined, withSomeFee(), ); handler.addWalletCallResponse( @@ -381,6 +408,13 @@ describe("DepositPage states", () => { } expect(await waitForStateUpdate()).true; + + { + const { status } = pullLastResultOrThrow(); + expect(status).equal("loading"); + } + + expect(await waitForStateUpdate()).true; const accountSelected = stringifyPaytoUri(ibanPayto.uri); { @@ -409,6 +443,8 @@ describe("DepositPage states", () => { expect(r.depositHandler.onClick).undefined; expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); + expect(r.amount.onInput).not.undefined; + if (!r.amount.onInput) return; r.amount.onInput("10"); } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx index e864c8413..771db828d 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -16,6 +16,7 @@ import { Amounts, PaytoUri } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; +import { AmountField } from "../../components/AmountField.js"; import { ErrorMessage } from "../../components/ErrorMessage.js"; import { LoadingError } from "../../components/LoadingError.js"; import { SelectList } from "../../components/SelectList.js"; @@ -28,6 +29,7 @@ import { } from "../../components/styled/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; +import { Grid } from "../../mui/Grid.js"; import { State } from "./index.js"; export function LoadingErrorView({ error }: State.LoadingUriError): VNode { @@ -167,48 +169,33 @@ export function ReadyView(state: State.Ready): VNode { <p> <AccountDetails account={state.currentAccount} /> </p> - <InputWithLabel invalid={!!state.amount.error}> - <label> - <i18n.Translate>Amount</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - value={state.amount.value} - onInput={(e) => state.amount.onInput(e.currentTarget.value)} + <Grid container spacing={2} columns={1}> + <Grid item xs={1}> + <AmountField + label={<i18n.Translate>Amount</i18n.Translate>} + currency={state.currency} + handler={state.amount} /> - </div> - {state.amount.error && <ErrorText>{state.amount.error}</ErrorText>} - </InputWithLabel> - - <InputWithLabel> - <label> - <i18n.Translate>Deposit fee</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - disabled - value={Amounts.stringifyValue(state.totalFee)} + </Grid> + <Grid item xs={1}> + <AmountField + label={<i18n.Translate>Deposit fee</i18n.Translate>} + currency={state.currency} + handler={{ + value: Amounts.stringifyValue(state.totalFee), + }} /> - </div> - </InputWithLabel> - - <InputWithLabel> - <label> - <i18n.Translate>Total deposit</i18n.Translate> - </label> - <div> - <span>{state.currency}</span> - <input - type="number" - disabled - value={Amounts.stringifyValue(state.totalToDeposit)} + </Grid> + <Grid item xs={1}> + <AmountField + label={<i18n.Translate>Total deposit</i18n.Translate>} + currency={state.currency} + handler={{ + value: Amounts.stringifyValue(state.totalToDeposit), + }} /> - </div> - </InputWithLabel> + </Grid> + </Grid> </section> <footer> <Button diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx index c584f2aae..ba1a560ef 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx @@ -19,6 +19,7 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { styled } from "@linaria/react"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { AmountField } from "../components/AmountField.js"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { SelectList } from "../components/SelectList.js"; @@ -32,6 +33,7 @@ import { useTranslationContext } from "../context/translation.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; import { Grid } from "../mui/Grid.js"; +import { TextFieldHandler } from "../mui/handlers.js"; import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; import { Pages } from "../NavigationBar.js"; @@ -283,11 +285,6 @@ export function DestinationSelectionGetCash({ const [currency, setCurrency] = useState(parsedInitialAmount?.currency); 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 previous1: Contact[] = []; const previous2: Contact[] = [ @@ -326,19 +323,13 @@ export function DestinationSelectionGetCash({ <i18n.Translate>Specify the amount and the origin</i18n.Translate> </h1> <Grid container columns={2} justifyContent="space-between"> - <TextField - label="Amount" - type="number" - min="0" - variant="filled" - error={invalid} + <AmountField + label={<i18n.Translate>Amount</i18n.Translate>} + currency={currency} required - startAdornment={ - <div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div> - } - value={amount} - onChange={(e) => { - setAmount(e); + handler={{ + onInput: async (s) => setAmount(s), + value: amount, }} /> <Button onClick={async () => setCurrency(undefined)}> @@ -431,11 +422,6 @@ export function DestinationSelectionSendCash({ const currency = parsedInitialAmount?.currency; 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 previous1: Contact[] = []; const previous2: Contact[] = [ @@ -474,19 +460,13 @@ export function DestinationSelectionSendCash({ </h1> <div> - <TextField - label="Amount" - type="number" - min="0" - variant="filled" + <AmountField + label={<i18n.Translate>Amount</i18n.Translate>} + currency={currency} required - error={invalid} - startAdornment={ - <div style={{ padding: "25px 12px 8px 12px" }}>{currency}</div> - } - value={amount} - onChange={(e) => { - positiveSetAmount(e); + handler={{ + onInput: async (s) => setAmount(s), + value: amount, }} /> </div> diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx index 832ca91b7..326e078f4 100644 --- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -416,9 +416,10 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode { fullWidth value={value} error={value !== undefined && !!errors?.value} + disabled={!field.onInput} onChange={(v) => { setValue(v); - if (!errors) { + if (!errors && field.onInput) { field.onInput(`payto://bitcoin/${v}`); } }} @@ -456,9 +457,10 @@ function TalerBankAddressAccount({ fullWidth value={host} error={host !== undefined && !!errors?.host} + disabled={!field.onInput} onChange={(v) => { setHost(v); - if (!errors) { + if (!errors && field.onInput) { field.onInput(`payto://x-taler-bank/${v}/${account}`); } }} @@ -470,11 +472,12 @@ function TalerBankAddressAccount({ label="Bank account" variant="standard" fullWidth + disabled={!field.onInput} value={account} error={account !== undefined && !!errors?.account} onChange={(v) => { setAccount(v || ""); - if (!errors) { + if (!errors && field.onInput) { field.onInput(`payto://x-taler-bank/${host}/${v}`); } }} @@ -502,9 +505,10 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode { fullWidth value={number} error={number !== undefined && !!errors?.number} + disabled={!field.onInput} onChange={(v) => { setNumber(v); - if (!errors) { + if (!errors && field.onInput) { field.onInput(`payto://iban/${v}?receiver-name=${name}`); } }} @@ -518,10 +522,13 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode { fullWidth value={name} error={name !== undefined && !!errors?.name} + disabled={!field.onInput} onChange={(v) => { setName(v); - if (!errors) { - field.onInput(`payto://iban/${number}?receiver-name=${encodeURIComponent(v)}`); + if (!errors && field.onInput) { + field.onInput( + `payto://iban/${number}?receiver-name=${encodeURIComponent(v)}`, + ); } }} /> |