diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/AmountField.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/components/AmountField.tsx | 223 |
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; +} |