commit 2221a25a7e0ab3c48e79a8d65dc981da5a3ecbb2
parent 457673564cb6ba9db874e8f5ed163a1f46d13da2
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 5 Mar 2026 12:25:10 -0300
fix #11190
Diffstat:
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;
+}