aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/components
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-11-22 15:43:39 -0300
committerSebastian <sebasjm@gmail.com>2022-11-22 15:43:39 -0300
commit88618df7b870732f4f29a80686dd4f4cf20887f8 (patch)
tree07eb8deb17cbd430c3f1f6a5767980cf67ddf296 /packages/taler-wallet-webextension/src/components
parentdc08d7d20eb805d95e7a74b1b6d5275e9e790953 (diff)
downloadwallet-core-88618df7b870732f4f29a80686dd4f4cf20887f8.tar.xz
amount field
Diffstat (limited to 'packages/taler-wallet-webextension/src/components')
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.stories.tsx65
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.tsx185
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/components/index.stories.tsx3
4 files changed, 228 insertions, 31 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];