summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/components/AmountField.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/AmountField.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.tsx223
1 files changed, 223 insertions, 0 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..c330c72b5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx
@@ -0,0 +1,223 @@
+/*
+ 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 {
+ amountFractionalBase,
+ amountFractionalLength,
+ AmountJson,
+ amountMaxValue,
+ Amounts,
+ Result,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AmountFieldHandler } from "../mui/handlers.js";
+import { TextField } from "../mui/TextField.js";
+
+const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
+const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
+
+/**
+ * Show normalized value based on the currency unit
+ */
+export function AmountField({
+ label,
+ handler,
+ lowestDenom = 1,
+ highestDenom = 1,
+ required,
+}: {
+ label: TranslatedString;
+ lowestDenom?: number;
+ highestDenom?: number;
+ required?: boolean;
+ handler: AmountFieldHandler;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [unit, setUnit] = useState(1);
+ const [error, setError] = useState<string>("");
+
+ const normal = normalize(handler.value, unit);
+ const previousValue = Amounts.stringifyValue(normal);
+
+ const [textValue, setTextValue] = useState<string>(previousValue);
+ useEffect(() => {
+ setTextValue(previousValue);
+ }, [previousValue]);
+
+ function updateUnit(newUnit: number) {
+ setUnit(newUnit);
+ const newNorm = normalize(handler.value, newUnit);
+ setTextValue(Amounts.stringifyValue(newNorm));
+ }
+
+ const currency = handler.value.currency;
+
+ const currencyLabels = buildLabelsForCurrency(
+ currency,
+ lowestDenom,
+ highestDenom,
+ );
+
+ function positiveAmount(value: string): string {
+ if (!value) {
+ if (handler.onInput) {
+ handler.onInput(Amounts.zeroOfCurrency(currency));
+ }
+ } else
+ try {
+ const parsed = Amounts.parseOrThrow(`${currency}:${value.trim()}`);
+
+ const realValue = denormalize(parsed, unit);
+
+ if (handler.onInput) {
+ handler.onInput(realValue);
+ }
+ setError("");
+ } catch (e) {
+ setError(i18n.str`Amount is not valid`);
+ }
+ setTextValue(value);
+ return value;
+ }
+
+ return (
+ <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);
+ updateUnit(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}
+ />
+ {error && <div style={{ color: "red" }}>{error}</div>}
+ </Fragment>
+ );
+}
+
+/**
+ * Return the real value of a normalized unit
+ * If the value is 20 and the unit is kilo == 1000 the returned value will be amount * 1000
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function denormalize(amount: AmountJson, unit: number): AmountJson {
+ if (unit === 1 || Amounts.isZero(amount)) return amount;
+ const result =
+ unit < 1
+ ? Amounts.divide(amount, 1 / unit)
+ : Amounts.mult(amount, unit).amount;
+ return result;
+}
+
+/**
+ * Return the amount in the current unit.
+ * If the value is 20000 and the unit is kilo == 1000 and the returned value will be amount / unit
+ *
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function normalize(amount: AmountJson, unit: number): AmountJson {
+ if (unit === 1 || Amounts.isZero(amount)) return amount;
+ const result =
+ unit < 1
+ ? Amounts.mult(amount, 1 / unit).amount
+ : Amounts.divide(amount, unit);
+ return result;
+}
+
+/**
+ * Take every label in HIGH_DENOM_SYMBOL and LOW_DENOM_SYMBOL and create
+ * which create the corresponding unit multiplier
+ * @param currency
+ * @param lowestDenom
+ * @param highestDenom
+ * @returns
+ */
+function buildLabelsForCurrency(
+ currency: string,
+ lowestDenom: number,
+ highestDenom: number,
+): Array<{ name: string; unit: number }> {
+ let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
+ let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
+
+ const result: Array<{ name: string; unit: number }> = [
+ {
+ name: currency,
+ unit: 1,
+ },
+ ];
+
+ while (hd > 0) {
+ result.push({
+ name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
+ unit: Math.pow(10, hd * 3),
+ });
+ hd--;
+ }
+ while (ld > 0) {
+ result.push({
+ name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
+ unit: Math.pow(10, -1 * ld * 3),
+ });
+ ld--;
+ }
+ return result;
+}