diff options
author | Sebastian <sebasjm@gmail.com> | 2022-11-22 15:43:39 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-11-22 15:43:39 -0300 |
commit | 88618df7b870732f4f29a80686dd4f4cf20887f8 (patch) | |
tree | 07eb8deb17cbd430c3f1f6a5767980cf67ddf296 /packages | |
parent | dc08d7d20eb805d95e7a74b1b6d5275e9e790953 (diff) |
amount field
Diffstat (limited to 'packages')
18 files changed, 298 insertions, 815 deletions
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx new file mode 100644 index 000000000..3183364a8 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx @@ -0,0 +1,65 @@ +/* + 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AmountJson, Amounts } from "@gnu-taler/taler-util"; +import { styled } from "@linaria/react"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { useTranslationContext } from "../context/translation.js"; +import { Grid } from "../mui/Grid.js"; +import { AmountFieldHandler, TextFieldHandler } from "../mui/handlers.js"; +import { AmountField } from "./AmountField.js"; + +export default { + title: "components/amountField", +}; + +function RenderAmount(): VNode { + const [value, setValue] = useState<AmountJson | undefined>(undefined); + + const error = value === undefined ? undefined : undefined; + + const handler: AmountFieldHandler = { + value: value ?? Amounts.zeroOfCurrency("USD"), + onInput: async (e) => { + setValue(e); + }, + error, + }; + const { i18n } = useTranslationContext(); + return ( + <Fragment> + <AmountField + required + label={<i18n.Translate>Amount</i18n.Translate>} + currency="USD" + highestDenom={2000000} + lowestDenom={0.01} + handler={handler} + /> + <p> + <pre>{JSON.stringify(value, undefined, 2)}</pre> + </p> + </Fragment> + ); +} + +export const AmountFieldExample = (): VNode => RenderAmount(); diff --git a/packages/taler-wallet-webextension/src/components/AmountField.tsx b/packages/taler-wallet-webextension/src/components/AmountField.tsx index 1c57be0df..6081e70ff 100644 --- a/packages/taler-wallet-webextension/src/components/AmountField.tsx +++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx @@ -14,51 +14,182 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { + amountFractionalBase, + amountFractionalLength, + AmountJson, + amountMaxValue, + Amounts, + Result, +} from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; -import { TextFieldHandler } from "../mui/handlers.js"; +import { useState } from "preact/hooks"; +import { AmountFieldHandler } from "../mui/handlers.js"; import { TextField } from "../mui/TextField.js"; -import { ErrorText } from "./styled/index.js"; + +const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"]; +const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"]; export function AmountField({ label, handler, - currency, + lowestDenom = 1, + highestDenom = 1, required, }: { label: VNode; + lowestDenom?: number; + highestDenom?: number; required?: boolean; - currency: string; - handler: TextFieldHandler; + handler: AmountFieldHandler; }): VNode { + const [unit, setUnit] = useState(1); + const [dotAtTheEnd, setDotAtTheEnd] = useState(false); + const currency = handler.value.currency; + + let hd = Math.floor(Math.log10(highestDenom || 1) / 3); + let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3); + + const currencyLabels: Array<{ name: string; unit: number }> = [ + { + name: currency, + unit: 1, + }, + ]; + + while (hd > 0) { + currencyLabels.push({ + name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`, + unit: Math.pow(10, hd * 3), + }); + hd--; + } + while (ld > 0) { + currencyLabels.push({ + name: `${LOW_DENOM_SYMBOL[ld]}${currency}`, + unit: Math.pow(10, -1 * ld * 3), + }); + ld--; + } + + const prev = Amounts.stringifyValue(handler.value); + function positiveAmount(value: string): string { - if (!value) return ""; - try { - const num = Number.parseFloat(value); - if (Number.isNaN(num) || num < 0) return handler.value; + setDotAtTheEnd(value.endsWith(".")); + if (!value) { if (handler.onInput) { - handler.onInput(value); + handler.onInput(Amounts.zeroOfCurrency(currency)); } - return value; + return ""; + } + try { + //remove all but last dot + const parsed = value.replace(/(\.)(?=.*\1)/g, ""); + const real = parseValue(currency, parsed); + + if (!real || real.value < 0) { + return prev; + } + + const normal = normalize(real, unit); + + console.log(real, unit, normal); + if (normal && handler.onInput) { + handler.onInput(normal); + } + return parsed; } catch (e) { // do nothing } - return handler.value; + return prev; } + + const normal = denormalize(handler.value, unit) ?? handler.value; + + const textValue = Amounts.stringifyValue(normal) + (dotAtTheEnd ? "." : ""); return ( - <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} - /> + <Fragment> + <TextField + label={label} + type="text" + min="0" + inputmode="decimal" + step="0.1" + variant="filled" + error={handler.error} + required={required} + startAdornment={ + currencyLabels.length === 1 ? ( + <div + style={{ + marginTop: 20, + padding: "5px 12px 8px 12px", + }} + > + {currency} + </div> + ) : ( + <select + disabled={!handler.onInput} + onChange={(e) => { + const unit = Number.parseFloat(e.currentTarget.value); + setUnit(unit); + }} + value={String(unit)} + style={{ + marginTop: 20, + padding: "5px 12px 8px 12px", + background: "transparent", + border: 0, + }} + > + {currencyLabels.map((c) => ( + <option key={c} value={c.unit}> + <div>{c.name}</div> + </option> + ))} + </select> + ) + } + value={textValue} + disabled={!handler.onInput} + onInput={positiveAmount} + /> + </Fragment> ); } + +function parseValue(currency: string, s: string): AmountJson | undefined { + const [intPart, fractPart] = s.split("."); + const tail = "." + (fractPart || "0"); + if (tail.length > amountFractionalLength + 1) { + return undefined; + } + const value = Number.parseInt(intPart, 10); + if (Number.isNaN(value) || value > amountMaxValue) { + return undefined; + } + return { + currency, + fraction: Math.round(amountFractionalBase * Number.parseFloat(tail)), + value, + }; +} + +function normalize(amount: AmountJson, unit: number): AmountJson | undefined { + if (unit === 1 || Amounts.isZero(amount)) return amount; + const result = + unit < 1 + ? Amounts.divide(amount, 1 / unit) + : Amounts.mult(amount, unit).amount; + return result; +} + +function denormalize(amount: AmountJson, unit: number): AmountJson | undefined { + if (unit === 1 || Amounts.isZero(amount)) return amount; + const result = + unit < 1 + ? Amounts.mult(amount, 1 / unit).amount + : Amounts.divide(amount, unit); + return result; +} diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index e5ce4140f..f8b23081d 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -57,9 +57,9 @@ export function TransactionItem(props: { tx: Transaction }): VNode { ? !tx.withdrawalDetails.confirmed ? i18n.str`Need approval in the Bank` : i18n.str`Exchange is waiting the wire transfer` - : undefined - : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer - ? i18n.str`Exchange is waiting the wire transfer` + : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer + ? i18n.str`Exchange is waiting the wire transfer` + : "" //pending but no message : undefined } /> diff --git a/packages/taler-wallet-webextension/src/components/index.stories.tsx b/packages/taler-wallet-webextension/src/components/index.stories.tsx index d71adf689..2e4e7fa2e 100644 --- a/packages/taler-wallet-webextension/src/components/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/components/index.stories.tsx @@ -25,5 +25,6 @@ import * as a3 from "./Amount.stories.js"; import * as a4 from "./ShowFullContractTermPopup.stories.js"; import * as a5 from "./TermsOfService/stories.js"; import * as a6 from "./QR.stories"; +import * as a7 from "./AmountField.stories.js"; -export default [a1, a2, a3, a4, a5, a6]; +export default [a1, a2, a3, a4, a5, a6, a7]; diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx index ba05158fa..42ac49a00 100644 --- a/packages/taler-wallet-webextension/src/mui/TextField.tsx +++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx @@ -41,6 +41,7 @@ export interface Props { multiline?: boolean; onChange?: (s: string) => void; onInput?: (s: string) => string; + inputmode?: string; min?: string; step?: string; placeholder?: string; diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts index 9d393e5b7..655fceef9 100644 --- a/packages/taler-wallet-webextension/src/mui/handlers.ts +++ b/packages/taler-wallet-webextension/src/mui/handlers.ts @@ -13,6 +13,7 @@ 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 { AmountJson } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-wallet-core"; export interface TextFieldHandler { @@ -21,6 +22,12 @@ export interface TextFieldHandler { error?: string; } +export interface AmountFieldHandler { + onInput?: (value: AmountJson) => Promise<void>; + value: AmountJson; + error?: string; +} + export interface ButtonHandler { onClick?: () => Promise<void>; error?: TalerError; diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx deleted file mode 100644 index 2154d35de..000000000 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { createExample } from "../test-utils.js"; -import { CreateManualWithdraw as TestedComponent } from "./CreateManualWithdraw.js"; - -export default { - title: "wallet/manual withdraw/creation", - component: TestedComponent, - argTypes: {}, -}; - -// , -const exchangeUrlWithCurrency = { - "http://exchange.taler:8081": "COL", - "http://exchange.tal": "EUR", -}; - -export const WithoutAnyExchangeKnown = createExample(TestedComponent, { - exchangeUrlWithCurrency: {}, -}); - -export const InitialState = createExample(TestedComponent, { - exchangeUrlWithCurrency, -}); - -export const WithAmountInitialized = createExample(TestedComponent, { - initialAmount: "10", - exchangeUrlWithCurrency, -}); - -export const WithExchangeError = createExample(TestedComponent, { - error: "The exchange url seems invalid", - exchangeUrlWithCurrency, -}); - -export const WithAmountError = createExample(TestedComponent, { - initialAmount: "e", - exchangeUrlWithCurrency, -}); diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts deleted file mode 100644 index 37c50285b..000000000 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* - 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { expect } from "chai"; -import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; -import { mountHook } from "../test-utils.js"; -import { useComponentState } from "./CreateManualWithdraw.js"; - -const exchangeListWithARSandUSD = { - url1: "USD", - url2: "ARS", - url3: "ARS", -}; - -const exchangeListEmpty = {}; - -describe("CreateManualWithdraw states", () => { - it("should set noExchangeFound when exchange list is empty", () => { - const { pullLastResultOrThrow } = mountHook(() => - useComponentState(exchangeListEmpty, undefined, undefined), - ); - - const { noExchangeFound } = pullLastResultOrThrow(); - - expect(noExchangeFound).equal(true); - }); - - it("should set noExchangeFound when exchange list doesn't include selected currency", () => { - const { pullLastResultOrThrow } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "COL"), - ); - - const { noExchangeFound } = pullLastResultOrThrow(); - - expect(noExchangeFound).equal(true); - }); - - it("should select the first exchange from the list", () => { - const { pullLastResultOrThrow } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, undefined), - ); - - const { exchange } = pullLastResultOrThrow(); - - expect(exchange.value).equal("url1"); - }); - - it("should select the first exchange with the selected currency", () => { - const { pullLastResultOrThrow } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - const { exchange } = pullLastResultOrThrow(); - - expect(exchange.value).equal("url2"); - }); - - it("should change the exchange when currency change", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - { - const { exchange, currency } = pullLastResultOrThrow(); - - expect(exchange.value).equal("url2"); - if (currency.onChange === undefined) expect.fail(); - currency.onChange("USD"); - } - - expect(await waitForStateUpdate()).true; - - { - const { exchange } = pullLastResultOrThrow(); - expect(exchange.value).equal("url1"); - } - }); - - it("should change the currency when exchange change", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - { - const { exchange, currency } = pullLastResultOrThrow(); - - expect(exchange.value).equal("url2"); - expect(currency.value).equal("ARS"); - - if (exchange.onChange === undefined) expect.fail(); - exchange.onChange("url1"); - } - - expect(await waitForStateUpdate()).true; - - { - const { exchange, currency } = pullLastResultOrThrow(); - - expect(exchange.value).equal("url1"); - expect(currency.value).equal("USD"); - } - }); - - it("should update parsed amount when amount change", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - { - const { amount, parsedAmount } = pullLastResultOrThrow(); - - expect(parsedAmount).equal(undefined); - - expect(amount.onInput).not.undefined; - if (!amount.onInput) return; - amount.onInput("12"); - } - - expect(await waitForStateUpdate()).true; - - { - const { parsedAmount } = pullLastResultOrThrow(); - - expect(parsedAmount).deep.equals({ - value: 12, - fraction: 0, - currency: "ARS", - }); - } - }); - - it("should have an amount field", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - await defaultTestForInputText( - waitForStateUpdate, - () => pullLastResultOrThrow().amount, - ); - }); - - it("should have an exchange selector ", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - await defaultTestForInputSelect( - waitForStateUpdate, - () => pullLastResultOrThrow().exchange, - ); - }); - - it("should have a currency selector ", async () => { - const { pullLastResultOrThrow, waitForStateUpdate } = mountHook(() => - useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), - ); - - await defaultTestForInputSelect( - waitForStateUpdate, - () => pullLastResultOrThrow().currency, - ); - }); -}); - -async function defaultTestForInputText( - awaiter: () => Promise<boolean>, - getField: () => TextFieldHandler, -): Promise<void> { - let nextValue = ""; - { - const field = getField(); - const initialValue = field.value; - nextValue = `${initialValue} something else`; - expect(field.onInput).not.undefined; - if (!field.onInput) return; - field.onInput(nextValue); - } - - expect(await awaiter()).true; - - { - const field = getField(); - expect(field.value).equal(nextValue); - } -} - -async function defaultTestForInputSelect( - awaiter: () => Promise<boolean>, - getField: () => SelectFieldHandler, -): Promise<void> { - let nextValue = ""; - - { - const field = getField(); - const initialValue = field.value; - const keys = Object.keys(field.list); - const nextIdx = keys.indexOf(initialValue) + 1; - if (keys.length < nextIdx) { - throw new Error("no enough values"); - } - nextValue = keys[nextIdx]; - if (field.onChange === undefined) expect.fail(); - field.onChange(nextValue); - } - - expect(await awaiter()).true; - - { - const field = getField(); - - expect(field.value).equal(nextValue); - } -} diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx deleted file mode 100644 index dd80faccd..000000000 --- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx +++ /dev/null @@ -1,282 +0,0 @@ -/* - 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { ErrorMessage } from "../components/ErrorMessage.js"; -import { SelectList } from "../components/SelectList.js"; -import { - BoldLight, - Centered, - Input, - InputWithLabel, - LightText, - LinkPrimary, - SubTitle, -} from "../components/styled/index.js"; -import { useTranslationContext } from "../context/translation.js"; -import { Button } from "../mui/Button.js"; -import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js"; -import { Pages } from "../NavigationBar.js"; - -export interface Props { - error: string | undefined; - initialAmount?: string; - exchangeUrlWithCurrency: Record<string, string>; - onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>; - initialCurrency?: string; -} - -export interface State { - noExchangeFound: boolean; - parsedAmount: AmountJson | undefined; - amount: TextFieldHandler; - currency: SelectFieldHandler; - exchange: SelectFieldHandler; -} - -export function useComponentState( - exchangeUrlWithCurrency: Record<string, string>, - initialAmount: string | undefined, - initialCurrency: string | undefined, -): State { - const exchangeSelectList = Object.keys(exchangeUrlWithCurrency); - const currencySelectList = Object.values(exchangeUrlWithCurrency); - const exchangeMap = exchangeSelectList.reduce( - (p, c) => ({ ...p, [c]: `${c} (${exchangeUrlWithCurrency[c]})` }), - {} as Record<string, string>, - ); - const currencyMap = currencySelectList.reduce( - (p, c) => ({ ...p, [c]: c }), - {} as Record<string, string>, - ); - - const foundExchangeForCurrency = exchangeSelectList.findIndex( - (e) => exchangeUrlWithCurrency[e] === initialCurrency, - ); - - const initialExchange = - foundExchangeForCurrency !== -1 - ? exchangeSelectList[foundExchangeForCurrency] - : !initialCurrency && exchangeSelectList.length > 0 - ? exchangeSelectList[0] - : undefined; - - const [exchange, setExchange] = useState(initialExchange || ""); - const [currency, setCurrency] = useState( - initialExchange ? exchangeUrlWithCurrency[initialExchange] : "", - ); - - const [amount, setAmount] = useState(initialAmount || ""); - const parsedAmount = Amounts.parse(`${currency}:${amount}`); - - async function changeExchange(exchange: string): Promise<void> { - setExchange(exchange); - setCurrency(exchangeUrlWithCurrency[exchange]); - } - - async function changeCurrency(currency: string): Promise<void> { - setCurrency(currency); - const found = Object.entries(exchangeUrlWithCurrency).find( - (e) => e[1] === currency, - ); - - if (found) { - setExchange(found[0]); - } else { - setExchange(""); - } - } - return { - noExchangeFound: initialExchange === undefined, - currency: { - list: currencyMap, - value: currency, - onChange: changeCurrency, - }, - exchange: { - list: exchangeMap, - value: exchange, - onChange: changeExchange, - }, - amount: { - value: amount, - onInput: async (e: string) => setAmount(e), - }, - parsedAmount, - }; -} - -export function CreateManualWithdraw({ - initialAmount, - exchangeUrlWithCurrency, - error, - initialCurrency, - onCreate, -}: Props): VNode { - const { i18n } = useTranslationContext(); - - const state = useComponentState( - exchangeUrlWithCurrency, - initialAmount, - initialCurrency, - ); - - if (state.noExchangeFound) { - if (initialCurrency) { - return ( - <section> - <SubTitle> - <i18n.Translate> - Manual Withdrawal for {initialCurrency} - </i18n.Translate> - </SubTitle> - <LightText> - <i18n.Translate> - Choose a exchange from where the coins will be withdrawn. The - exchange will send the coins to this wallet after receiving a wire - transfer with the correct subject. - </i18n.Translate> - </LightText> - <Centered style={{ marginTop: 100 }}> - <BoldLight> - <i18n.Translate> - No exchange found for {initialCurrency} - </i18n.Translate> - </BoldLight> - <LinkPrimary - href={Pages.settingsExchangeAdd({ currency: initialCurrency })} - style={{ marginLeft: "auto" }} - > - <i18n.Translate>Add Exchange</i18n.Translate> - </LinkPrimary> - </Centered> - </section> - ); - } - return ( - <section> - <SubTitle> - <i18n.Translate> - Manual Withdrawal for {state.currency.value} - </i18n.Translate> - </SubTitle> - <LightText> - <i18n.Translate> - Choose a exchange from where the coins will be withdrawn. The - exchange will send the coins to this wallet after receiving a wire - transfer with the correct subject. - </i18n.Translate> - </LightText> - <Centered style={{ marginTop: 100 }}> - <BoldLight> - <i18n.Translate>No exchange configured</i18n.Translate> - </BoldLight> - <LinkPrimary - href={Pages.settingsExchangeAdd({})} - style={{ marginLeft: "auto" }} - > - <i18n.Translate>Add Exchange</i18n.Translate> - </LinkPrimary> - </Centered> - </section> - ); - } - - return ( - <Fragment> - <section> - {error && ( - <ErrorMessage - title={ - <i18n.Translate>Can't create the reserve</i18n.Translate> - } - description={error} - /> - )} - <SubTitle> - <i18n.Translate> - Manual Withdrawal for {state.currency.value} - </i18n.Translate> - </SubTitle> - <LightText> - <i18n.Translate> - Choose a exchange from where the coins will be withdrawn. The - exchange will send the coins to this wallet after receiving a wire - transfer with the correct subject. - </i18n.Translate> - </LightText> - <p> - <Input> - <SelectList - label={<i18n.Translate>Currency</i18n.Translate>} - name="currency" - {...state.currency} - /> - </Input> - <Input> - <SelectList - label={<i18n.Translate>Exchange</i18n.Translate>} - name="exchange" - {...state.exchange} - /> - </Input> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <LinkPrimary - href={Pages.settingsExchangeAdd({})} - style={{ marginLeft: "auto" }} - > - <i18n.Translate>Add Exchange</i18n.Translate> - </LinkPrimary> - </div> - {state.currency.value && ( - <InputWithLabel - invalid={!!state.amount.value && !state.parsedAmount} - > - <label> - <i18n.Translate>Amount</i18n.Translate> - </label> - <div> - <span>{state.currency.value}</span> - <input - type="number" - value={state.amount.value} - // onInput={(e) => state.amount.onInput(e.currentTarget.value)} - /> - </div> - </InputWithLabel> - )} - </p> - </section> - <footer> - <div /> - <Button - variant="contained" - disabled={!state.parsedAmount || !state.exchange.value} - onClick={() => onCreate(state.exchange.value, state.parsedAmount!)} - > - <i18n.Translate>Start withdrawal</i18n.Translate> - </Button> - </footer> - </Fragment> - ); -} diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts index 373045833..3f23515b2 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts @@ -18,6 +18,7 @@ import { AmountJson, PaytoUri } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; import { + AmountFieldHandler, ButtonHandler, SelectFieldHandler, TextFieldHandler, @@ -98,7 +99,7 @@ export namespace State { totalFee: AmountJson; totalToDeposit: AmountJson; - amount: TextFieldHandler; + amount: AmountFieldHandler; account: SelectFieldHandler; cancelHandler: ButtonHandler; depositHandler: ButtonHandler; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts index 91883c823..bbf2c2771 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -52,9 +52,13 @@ export function useComponentState( }); const initialValue = - parsed !== undefined ? Amounts.stringifyValue(parsed) : "0"; + parsed !== undefined + ? parsed + : currency !== undefined + ? Amounts.zeroOfCurrency(currency) + : undefined; // const [accountIdx, setAccountIdx] = useState<number>(0); - const [amount, setAmount] = useState(initialValue); + const [amount, setAmount] = useState<AmountJson>(initialValue ?? ({} as any)); const [selectedAccount, setSelectedAccount] = useState<PaytoUri>(); const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined); @@ -81,7 +85,7 @@ export function useComponentState( } const { accounts, balances } = hook.response; - const parsedAmount = Amounts.parse(`${currency}:${amount}`); + // const parsedAmount = Amounts.parse(`${currency}:${amount}`); if (addingAccount) { return { @@ -129,8 +133,8 @@ export function useComponentState( const firstAccount = accounts[0].uri; const currentAccount = !selectedAccount ? firstAccount : selectedAccount; - if (fee === undefined && parsedAmount) { - getFeeForAmount(currentAccount, parsedAmount, api).then((initialFee) => { + if (fee === undefined) { + getFeeForAmount(currentAccount, amount, api).then((initialFee) => { setFee(initialFee); }); return { @@ -143,9 +147,9 @@ export function useComponentState( async function updateAccountFromList(accountStr: string): Promise<void> { const uri = !accountStr ? undefined : parsePaytoUri(accountStr); - if (uri && parsedAmount) { + if (uri) { try { - const result = await getFeeForAmount(uri, parsedAmount, api); + const result = await getFeeForAmount(uri, amount, api); setSelectedAccount(uri); setFee(result); } catch (e) { @@ -155,17 +159,15 @@ export function useComponentState( } } - async function updateAmount(numStr: string): Promise<void> { - const parsed = Amounts.parse(`${currency}:${numStr}`); - if (parsed) { - try { - const result = await getFeeForAmount(currentAccount, parsed, api); - setAmount(numStr); - setFee(result); - } catch (e) { - setAmount(numStr); - setFee(undefined); - } + async function updateAmount(newAmount: AmountJson): Promise<void> { + // const parsed = Amounts.parse(`${currency}:${numStr}`); + try { + const result = await getFeeForAmount(currentAccount, newAmount, api); + setAmount(newAmount); + setFee(result); + } catch (e) { + setAmount(newAmount); + setFee(undefined); } } @@ -175,32 +177,29 @@ export function useComponentState( : Amounts.zeroOfCurrency(currency); const totalToDeposit = - parsedAmount && fee !== undefined - ? Amounts.sub(parsedAmount, totalFee).amount + fee !== undefined + ? Amounts.sub(amount, totalFee).amount : Amounts.zeroOfCurrency(currency); const isDirty = amount !== initialValue; const amountError = !isDirty ? undefined - : !parsedAmount - ? "Invalid amount" - : Amounts.cmp(balance, parsedAmount) === -1 + : Amounts.cmp(balance, amount) === -1 ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}` : undefined; const unableToDeposit = - !parsedAmount || //no amount specified Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee fee === undefined || //no fee calculated yet amountError !== undefined; //amount field may be invalid async function doSend(): Promise<void> { - if (!parsedAmount || !currency) return; + if (!currency) return; const depositPaytoUri = stringifyPaytoUri(currentAccount); - const amount = Amounts.stringify(parsedAmount); + const amountStr = Amounts.stringify(amount); await api.wallet.call(WalletApiOperation.CreateDepositGroup, { - amount, + amount: amountStr, depositPaytoUri, }); onSuccess(currency); @@ -211,7 +210,7 @@ export function useComponentState( error: undefined, currency, amount: { - value: String(amount), + value: amount, onInput: updateAmount, error: amountError, }, diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx index f03788d4e..75c544c84 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx @@ -52,7 +52,7 @@ export const WithNoAccountForIBAN = createExample(ReadyView, { onInput: async () => { null; }, - value: "10:USD", + value: Amounts.parseOrThrow("USD:10"), }, onAddAccount: {}, cancelHandler: {}, @@ -87,7 +87,7 @@ export const WithIBANAccountTypeSelected = createExample(ReadyView, { onInput: async () => { null; }, - value: "10:USD", + value: Amounts.parseOrThrow("USD:10"), }, onAddAccount: {}, cancelHandler: {}, @@ -123,7 +123,7 @@ export const NewBitcoinAccountTypeSelected = createExample(ReadyView, { onInput: async () => { null; }, - value: "10:USD", + value: Amounts.parseOrThrow("USD:10"), }, cancelHandler: {}, depositHandler: { diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts index 17e17d185..3f08c678c 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -194,7 +194,7 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(stringifyPaytoUri(ibanPayto.uri)); - expect(r.amount.value).eq("0"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(r.depositHandler.onClick).undefined; } @@ -269,7 +269,7 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); - expect(r.amount.value).eq("0"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(r.depositHandler.onClick).undefined; expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.account.onChange).not.undefined; @@ -285,7 +285,7 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(accountSelected); - expect(r.amount.value).eq("0"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.depositHandler.onClick).undefined; } @@ -423,7 +423,7 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(stringifyPaytoUri(talerBankPayto.uri)); - expect(r.amount.value).eq("0"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); expect(r.depositHandler.onClick).undefined; expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)); expect(r.account.onChange).not.undefined; @@ -439,13 +439,13 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(accountSelected); - expect(r.amount.value).eq("0"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0")); 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"); + r.amount.onInput(Amounts.parseOrThrow("EUR:10")); } expect(await waitForStateUpdate()).true; @@ -456,7 +456,7 @@ describe("DepositPage states", () => { expect(r.cancelHandler.onClick).not.undefined; expect(r.currency).eq(currency); expect(r.account.value).eq(accountSelected); - expect(r.amount.value).eq("10"); + expect(r.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10")); expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)); expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)); expect(r.depositHandler.onClick).not.undefined; diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx index 771db828d..6a28f31e1 100644 --- a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx @@ -173,25 +173,22 @@ export function ReadyView(state: State.Ready): VNode { <Grid item xs={1}> <AmountField label={<i18n.Translate>Amount</i18n.Translate>} - currency={state.currency} handler={state.amount} /> </Grid> <Grid item xs={1}> <AmountField label={<i18n.Translate>Deposit fee</i18n.Translate>} - currency={state.currency} handler={{ - value: Amounts.stringifyValue(state.totalFee), + value: state.totalFee, }} /> </Grid> <Grid item xs={1}> <AmountField label={<i18n.Translate>Total deposit</i18n.Translate>} - currency={state.currency} handler={{ - value: Amounts.stringifyValue(state.totalToDeposit), + value: state.totalToDeposit, }} /> </Grid> diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx index ba1a560ef..7e4c775e6 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection.tsx @@ -33,9 +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"; import arrowIcon from "../svg/chevron-down.svg"; import bankIcon from "../svg/ri-bank-line.svg"; @@ -279,12 +277,13 @@ export function DestinationSelectionGetCash({ const parsedInitialAmount = !initialAmount ? undefined : Amounts.parse(initialAmount); - const parsedInitialAmountValue = !parsedInitialAmount - ? "0" - : Amounts.stringifyValue(parsedInitialAmount); + const [currency, setCurrency] = useState(parsedInitialAmount?.currency); - const [amount, setAmount] = useState(parsedInitialAmountValue); + const [amount, setAmount] = useState( + parsedInitialAmount ?? Amounts.zeroOfCurrency(currency ?? "KUDOS"), + ); + const { i18n } = useTranslationContext(); const previous1: Contact[] = []; const previous2: Contact[] = [ @@ -313,10 +312,8 @@ export function DestinationSelectionGetCash({ </div> ); } - const currencyAndAmount = `${currency}:${amount}`; - const parsedAmount = Amounts.parse(currencyAndAmount); - // const dirty = parsedInitialAmountValue !== amount; - const invalid = !parsedAmount || Amounts.isZero(parsedAmount); + const currencyAndAmount = Amounts.stringify(amount); + const invalid = Amounts.isZero(amount); return ( <Container> <h1> @@ -325,7 +322,6 @@ export function DestinationSelectionGetCash({ <Grid container columns={2} justifyContent="space-between"> <AmountField label={<i18n.Translate>Amount</i18n.Translate>} - currency={currency} required handler={{ onInput: async (s) => setAmount(s), @@ -416,12 +412,12 @@ export function DestinationSelectionSendCash({ const parsedInitialAmount = !initialAmount ? undefined : Amounts.parse(initialAmount); - const parsedInitialAmountValue = !parsedInitialAmount - ? "" - : Amounts.stringifyValue(parsedInitialAmount); + const currency = parsedInitialAmount?.currency; - const [amount, setAmount] = useState(parsedInitialAmountValue); + const [amount, setAmount] = useState( + parsedInitialAmount ?? Amounts.zeroOfCurrency(currency ?? "KUDOS"), + ); const { i18n } = useTranslationContext(); const previous1: Contact[] = []; const previous2: Contact[] = [ @@ -450,9 +446,9 @@ export function DestinationSelectionSendCash({ </div> ); } - const currencyAndAmount = `${currency}:${amount}`; - const parsedAmount = Amounts.parse(currencyAndAmount); - const invalid = !parsedAmount || Amounts.isZero(parsedAmount); + const currencyAndAmount = Amounts.stringify(amount); + //const parsedAmount = Amounts.parse(currencyAndAmount); + const invalid = Amounts.isZero(amount); return ( <Container> <h1> @@ -462,7 +458,6 @@ export function DestinationSelectionSendCash({ <div> <AmountField label={<i18n.Translate>Amount</i18n.Translate>} - currency={currency} required handler={{ onInput: async (s) => setAmount(s), diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts index 39fbb6ce2..63d545b97 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts @@ -44,21 +44,22 @@ export function useComponentState( const comparingExchanges = selectedIdx !== initialValue; - const initialExchange = - comparingExchanges ? exchanges[initialValue] : undefined; + const initialExchange = comparingExchanges + ? exchanges[initialValue] + : undefined; const hook = useAsyncAsHook(async () => { const selected = !selectedExchange ? undefined : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { - exchangeBaseUrl: selectedExchange.exchangeBaseUrl, - }); + exchangeBaseUrl: selectedExchange.exchangeBaseUrl, + }); const original = !initialExchange ? undefined : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, { - exchangeBaseUrl: initialExchange.exchangeBaseUrl, - }); + exchangeBaseUrl: initialExchange.exchangeBaseUrl, + }); return { exchanges, diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx deleted file mode 100644 index e2284a466..000000000 --- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* - 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 { - AcceptManualWithdrawalResult, - AmountJson, - Amounts, - NotificationType, - parsePaytoUri, - PaytoUri, -} from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { Loading } from "../components/Loading.js"; -import { LoadingError } from "../components/LoadingError.js"; -import { useTranslationContext } from "../context/translation.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; -import { wxApi } from "../wxApi.js"; -import { CreateManualWithdraw } from "./CreateManualWithdraw.js"; -import { ReserveCreated } from "./ReserveCreated.js"; - -interface Props { - amount?: string; - onCancel: () => Promise<void>; -} - -export function ManualWithdrawPage({ amount, onCancel }: Props): VNode { - const [success, setSuccess] = useState< - | { - response: AcceptManualWithdrawalResult; - exchangeBaseUrl: string; - amount: AmountJson; - paytoURI: PaytoUri | undefined; - payto: string; - } - | undefined - >(undefined); - const [error, setError] = useState<string | undefined>(undefined); - - const state = useAsyncAsHook(() => - wxApi.wallet.call(WalletApiOperation.ListExchanges, {}), - ); - useEffect(() => { - return wxApi.listener.onUpdateNotification( - [NotificationType.ExchangeAdded], - state?.retry, - ); - }); - const { i18n } = useTranslationContext(); - - async function doCreate( - exchangeBaseUrl: string, - amount: AmountJson, - ): Promise<void> { - try { - const response = await wxApi.wallet.call( - WalletApiOperation.AcceptManualWithdrawal, - { - exchangeBaseUrl: exchangeBaseUrl, - amount: Amounts.stringify(amount), - }, - ); - const payto = response.exchangePaytoUris[0]; - const paytoURI = parsePaytoUri(payto); - setSuccess({ exchangeBaseUrl, response, amount, paytoURI, payto }); - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } else { - setError("unexpected error"); - } - setSuccess(undefined); - } - } - - if (success) { - return ( - <ReserveCreated - reservePub={success.response.reservePub} - paytoURI={success.paytoURI} - // payto={success.payto} - exchangeBaseUrl={success.exchangeBaseUrl} - amount={success.amount} - onCancel={onCancel} - /> - ); - } - - if (!state) { - return <Loading />; - } - if (state.hasError) { - return ( - <LoadingError - title={ - <i18n.Translate> - Could not load the list of known exchanges - </i18n.Translate> - } - error={state} - /> - ); - } - - const exchangeList = state.response.exchanges.reduce( - (p, c) => ({ - ...p, - [c.exchangeBaseUrl]: c.currency || "??", - }), - {} as Record<string, string>, - ); - - const parsedAmount = !amount ? undefined : Amounts.parse(amount); - const currency = parsedAmount?.currency; - const amountValue = !parsedAmount - ? undefined - : Amounts.stringifyValue(parsedAmount); - return ( - <CreateManualWithdraw - error={error} - exchangeUrlWithCurrency={exchangeList} - onCreate={doCreate} - initialCurrency={currency} - initialAmount={amountValue} - /> - ); -} diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx index 42808b573..ef1295846 100644 --- a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx @@ -20,7 +20,6 @@ */ import * as a1 from "./Backup.stories.js"; -import * as a3 from "./CreateManualWithdraw.stories.js"; import * as a4 from "./DepositPage/stories.js"; import * as a5 from "./ExchangeAddConfirm.stories.js"; import * as a6 from "./ExchangeAddSetUrl.stories.js"; @@ -40,7 +39,6 @@ import * as a20 from "./ManageAccount/stories.js"; export default [ a1, - a3, a4, a5, a6, |