taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2221a25a7e0ab3c48e79a8d65dc981da5a3ecbb2
parent 457673564cb6ba9db874e8f5ed163a1f46d13da2
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu,  5 Mar 2026 12:25:10 -0300

fix #11190

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 19+++++++++++++------
Apackages/merchant-backoffice-ui/src/components/form/InputCode.tsx | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/scss/main.scss | 17+++++++++++++++--
4 files changed, 214 insertions(+), 10 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -27,6 +27,7 @@ import { } from "../hooks/preference.js"; import { FormErrors, FormProvider } from "./form/FormProvider.js"; import { Input } from "./form/Input.js"; +import { InputCode } from "./form/InputCode.js"; const TALER_SCREEN_ID = 5; @@ -157,14 +158,20 @@ function SolveChallenge({ ); } })()} - <Input<Form> - label={i18n.str`Verification code`} + + <InputCode<Form> name="code" - focus={focus} - readonly={showExpired} - dontRemember - inputType="numeric" + label={i18n.str`Verification code`} + size={8} + focus + filter={(c) => { + const v = Number.parseInt(c, 10); + if (Number.isNaN(v) || v > 9 || v < 0) return undefined; + return String(v); + }} + dashesIndex={[3]} /> + {expiration.t_ms === "never" ? undefined : ( <p> <i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCode.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCode.tsx @@ -0,0 +1,184 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 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 { Fragment, h, VNode } from "preact"; +import { Tooltip } from "../Tooltip.js"; +import { InputProps, useField } from "./useField.js"; +import { useRef } from "preact/hooks"; +import { doAutoFocus } from "./Input.js"; + +interface Props<T> extends InputProps<T> { + inputExtra?: any; + focus?: boolean; + size: number; + dashesIndex?: number[]; + filter: (c: string) => string | undefined; +} + +export function InputCode<T>({ + name, + readonly, + tooltip, + label, + help, + focus, + inputExtra, + size, + dashesIndex = [], + filter, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + + const elementArray = Array.from<HTMLInputElement | null>({ + length: size, + }).fill(null); + + const elements = useRef(elementArray); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {required && ( + <span class="has-text-danger" style={{ marginLeft: 5 }}> + * + </span> + )} + {tooltip && ( + <Tooltip text={tooltip}> + <i class="icon mdi mdi-information" /> + </Tooltip> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <fieldset> + {elementArray.map((v, idx) => { + const strValue = value as string; + const defValue = + !strValue || strValue.length <= idx + ? undefined + : filter(strValue[idx]); + + const prevHaveDash = dashesIndex.indexOf(idx - 1) !== -1; + const haveDash = dashesIndex.indexOf(idx) !== -1; + return ( + <Fragment> + <input + id={String(idx)} + {...(inputExtra as any)} + // ref={focus && idx === 0 ? doAutoFocus : undefined} + ref={(el) => { + elements.current[idx] = el; + if (focus && idx === 0 && !value) { + doAutoFocus(el); + } + }} + class={ + error ? "input is-danger mfa-code" : "input mfa-code" + } + type={"number"} + inputMode={"numeric"} + placeholder={idx + 1} + cols={1} + defaultValue={defValue} + style={{ + padding: 1, + width: 26, + textAlign: "center", + borderLeft: idx === 0 || prevHaveDash ? undefined : "0px", + borderTopLeftRadius: + idx === 0 || prevHaveDash ? undefined : "0px", + borderBottomLeftRadius: + idx === 0 || prevHaveDash ? undefined : "0px", + + borderRight: + idx === size - 1 || haveDash ? undefined : "0px", + borderTopRightRadius: + idx === size - 1 || haveDash ? undefined : "0px", + borderBottomRightRadius: + idx === size - 1 || haveDash ? undefined : "0px", + }} + readonly={readonly} + disabled={readonly} + autoComplete={"off"} + name={String(name)} + onFocus={(e) => { + e.preventDefault(); + e.currentTarget.select() + }} + onChange={(e) => { + e.preventDefault(); + 2 + const v = filter(e.currentTarget.value); + e.currentTarget.value = v ?? ""; + console.log("v", v) + if (v === undefined) { + console.log("undef") + onChange(undefined as any) + return; + } + elements.current[idx + 1]?.focus(); + for (const e of elements.current) { + if (!e?.value) break; + } + + const total = elements.current.reduce((prev, cur) => { + if (!cur?.value) return prev; + return prev + cur?.value; + }, ""); + console.log("total ",total) + if (total.length === size) { + onChange(total as any); + } + }} + /> + {haveDash ? ( + <input + class="input mfa-code" + defaultValue={"-"} + tabIndex={-1} + // disabled + readOnly + style={{ + border: 0, + padding: 1, + width: 26, + textAlign: "center", + }} + /> + ) : undefined} + </Fragment> + ); + })} + </fieldset> + {help} + {error && ( + <p class="help is-danger" style={{ fontSize: 16 }}> + {error} + </p> + )} + </div> + </div> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -37,13 +37,14 @@ import { useLocalStorage, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { FormErrors, FormProvider, } from "../../components/form/FormProvider.js"; import { Input } from "../../components/form/Input.js"; +import { InputPassword } from "../../components/form/InputPassword.js"; import { InputToggle } from "../../components/form/InputToggle.js"; import { InputWithAddon } from "../../components/form/InputWithAddon.js"; import { SolveMFAChallenges } from "../../components/SolveMFA.js"; @@ -53,7 +54,6 @@ import { INSTANCE_ID_REGEX, PHONE_JUST_NUMBERS_REGEX, } from "../../utils/constants.js"; -import { InputPassword } from "../../components/form/InputPassword.js"; import { maybeTryFirstMFA } from "../instance/accounts/create/CreatePage.js"; const TALER_SCREEN_ID = 80; diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -186,4 +186,18 @@ div[data-tooltip]::before { .modal-card-body>p.warning { background-color: #fffbdd; border: solid 1px #f2e9bf; -} -\ No newline at end of file +} + input.mfa-code::-webkit-outer-spin-button, +input.mfa-code::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input.mfa-code:focus { + background-color: rgb(217, 217, 233); +} + +/* Firefox */ +input.mfa-code[type=number] { + -moz-appearance: textfield; +}