wallet-core/packages/taler-wallet-webextension/src/components/AmountField.tsx
2023-02-28 07:41:51 -03:00

221 lines
5.7 KiB
TypeScript

/*
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 { useState } from "preact/hooks";
import { useTranslationContext } from "../context/translation.js";
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);
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;
}