taler-typescript-core

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

commit 242669f6ae6599dfc8b1dfe07e2354208ee75b0c
parent f9fd5471809cd33f3b96eb84d4cdb2918db2919a
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Tue,  2 Jun 2026 12:01:51 -0300

show copy button in the notification bar

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 154++++++++++++++++++++++++++++++++++---------------------------------------------
Mpackages/web-util/src/components/CopyButton.tsx | 10+++++++---
Mpackages/web-util/src/components/NotificationBanner.tsx | 23+++++++++--------------
3 files changed, 83 insertions(+), 104 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -20,25 +20,34 @@ */ import { + asPassword, assertUnreachable, Duration, HttpStatusCode, LoginTokenRequest, LoginTokenScope, + Password, TranslatedString, } from "@gnu-taler/taler-util"; import { Button, + undefinedIfEmpty, useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { FormProvider } from "../../components/form/FormProvider.js"; -import { doAutoFocus } from "../../components/form/Input.js"; +import { + FormErrors, + FormProvider, +} from "../../components/form/FormProvider.js"; +import { doAutoFocus, Input } from "../../components/form/Input.js"; import { Tooltip } from "../../components/Tooltip.js"; import { useMerchantChallengeHandlerContext } from "../../context/challenge.js"; import { useSessionContext } from "../../context/session.js"; +import { useMemo } from "preact/hooks"; +import { InputPassword } from "../../components/form/InputPassword.js"; +import { InputWithAddon } from "../../components/form/InputWithAddon.js"; const TALER_SCREEN_ID = 79; @@ -47,6 +56,11 @@ interface Props { focus?: boolean; } +type Form = { + username: string; + password: string; +}; + export const TEMP_TEST_TOKEN = (description: TranslatedString) => ({ scope: LoginTokenScope.All, @@ -62,32 +76,47 @@ export const FOREVER_REFRESHABLE_TOKEN = (description: TranslatedString) => }) as LoginTokenRequest; export function LoginPage({ showCreateAccount, focus }: Props): VNode { - const [password, setPassword] = useState(""); - const { state, lib, logIn, getInstanceForUsername, config } = useSessionContext(); - const [username, setUsername] = useState( - showCreateAccount ? "" : state.instance, - ); const { i18n } = useTranslationContext(); - const [hidePassword, setHidePassword] = useState(true); + + const defaultUsername = showCreateAccount ? undefined : state.instance; + + const [value, valueHandler] = useState<Partial<Form>>({ + username: defaultUsername, + }); + + // const [opFailed, setOpFailed] = useState<"bad-username" | "bad-password">(); + + const errors = undefinedIfEmpty<FormErrors<Form>>({ + username: !value.username + ? i18n.str`Required` + : // : opFailed === "bad-username" + // ? i18n.str`wrong username` + undefined, + password: !value.password + ? i18n.str`Required` + : // : opFailed === "bad-password" + // ? i18n.str`wrong password` + undefined, + }); const { actionHandler, showError } = useNotificationContext(); const mfa = useMerchantChallengeHandlerContext(); - + const pwd = useMemo(() => asPassword(value.password!), [value.password]); const login = actionHandler( /*login*/ - async (ct, usr: string, pwd: string, challengeIds?: string[]) => { + async (ct, usr: string, pwd: Password, challengeIds?: string[]) => { const api = getInstanceForUsername(usr); const resp = await api.createAccessToken( usr, - pwd, + pwd.__value, FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), { challengeIds }, ); return resp; }, - !username || !password ? undefined : [username, password], + !value.username || !value.password ? undefined : [value.username, pwd], ); login.onSuccess = (success, usr) => { logIn(usr, success.access_token); @@ -104,8 +133,12 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { ); return undefined; case HttpStatusCode.Unauthorized: + // setOpFailed("bad-password"); + // return undefined; return i18n.str`Wrong password.`; case HttpStatusCode.NotFound: + // setOpFailed("bad-username"); + // return undefined; return i18n.str`The account doesn't exist.`; default: assertUnreachable(fail); @@ -124,7 +157,12 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { <i18n.Translate>Login required</i18n.Translate> </p> </header> - <form> + <FormProvider<Form> + name="login" + object={value} + errors={errors} + valueHandler={valueHandler} + > <section class="modal-card-body" style={{ @@ -135,76 +173,18 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { overflow: "hidden", }} > - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Username</i18n.Translate> - <Tooltip text={i18n.str`Instance name.`}> - <i class="icon mdi mdi-information" /> - </Tooltip> - </label> - </div> - <div class="field-body"> - <div class="field"> - <p class="control is-expanded"> - <input - class="input" - type="text" - ref={focus ? doAutoFocus : undefined} - // placeholder={i18n.str`instance name`} - name="username" - autoComplete="username" - value={username} - onInput={(e): void => - setUsername(e?.currentTarget.value) - } - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Password</i18n.Translate> - <Tooltip text={i18n.str`Instance password.`}> - <i class="icon mdi mdi-information" /> - </Tooltip> - </label> - </div> - <div class="field-body"> - <div class="field has-addons"> - <p class="control is-expanded"> - <input - class="input" - type={hidePassword ? "password" : "text"} - // placeholder={i18n.str`current password`} - name="token" - autoComplete="current-password" - value={password} - onInput={(e): void => - setPassword(e?.currentTarget.value) - } - /> - </p> - <div - class="control" - style={{ cursor: "pointer" }} - onClick={() => { - setHidePassword((h) => !h); - }} - > - <a class="button is-static point"> - {hidePassword ? ( - <i class="icon mdi mdi-eye-off" /> - ) : ( - <i class="icon mdi mdi-eye" /> - )} - </a> - </div> - </div> - </div> - </div> + <InputWithAddon<Form> + name="username" + label={i18n.str`Username`} + tooltip={i18n.str`Instance name.`} + autoComplete="username" + /> + <InputPassword<Form> + name="password" + label={i18n.str`Password`} + tooltip={i18n.str`Instance password.`} + autoComplete="current-password" + /> </section> <footer class="modal-card-foot " @@ -219,12 +199,12 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { ) : ( <a href={ - !username || username === "admin" + !value.username || value.username === "admin" ? undefined - : `#/account/reset/${username}` + : `#/account/reset/${value.username}` } class="button " - disabled={!username || username === "admin"} + disabled={!value.username || value.username === "admin"} > <i18n.Translate>Forgot password</i18n.Translate> </a> @@ -233,7 +213,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { <i18n.Translate>Confirm</i18n.Translate> </Button> </footer> - </form> + </FormProvider> </div> </div> {!showCreateAccount ? undefined : ( diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx @@ -1,4 +1,5 @@ import { ComponentChildren, h, VNode } from "preact"; +import { CSSProperties } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; export function CopyIcon(): VNode { @@ -9,7 +10,7 @@ export function CopyIcon(): VNode { viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" - class="w-6 h-6" + style={{ width: 24, height: 24 }} > <path stroke-linecap="round" @@ -28,7 +29,7 @@ export function CopiedIcon(): VNode { viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" - class="w-6 h-6" + style={{ width: 24, height: 24 }} > <path stroke-linecap="round" @@ -41,11 +42,13 @@ export function CopiedIcon(): VNode { export function CopyButton({ class: clazz, + style, children, getContent, }: { children?: ComponentChildren; class: string; + style?: CSSProperties; getContent: () => string; }): VNode { const [copied, setCopied] = useState(false); @@ -73,6 +76,7 @@ export function CopyButton({ return ( <button class={clazz} + style={style} onClick={(e) => { e.preventDefault(); copyText(); @@ -84,7 +88,7 @@ export function CopyButton({ ); } return ( - <button class={clazz} disabled> + <button class={clazz} style={style} disabled> <CopiedIcon /> {children} </button> diff --git a/packages/web-util/src/components/NotificationBanner.tsx b/packages/web-util/src/components/NotificationBanner.tsx @@ -2,6 +2,7 @@ import { Fragment, h, VNode } from "preact"; import { useRef, useState } from "preact/compat"; import { Duration } from "../../../taler-util/src/time.js"; import { + CopyButton, Notification, useCommonPreferences, useNotificationContext, @@ -130,20 +131,15 @@ export function ToastBannerBulma(): VNode { <div class="message-header"> <p>{msg.title}</p> <div> - <button - class="copy " - aria-label="copy" - onClick={(e) => { - e.preventDefault(); - - navigator.clipboard.writeText( - fromNodeToText(divHtml.current), - ); - }} + <CopyButton + class="button" + style={{ padding: 8 }} + getContent={() => fromNodeToText(divHtml.current)} /> <button class="delete " aria-label="close" + style={{ margin: 8 }} onClick={() => notification.acknowledge()} /> </div> @@ -167,11 +163,11 @@ export function ToastBannerBulma(): VNode { <i18n.Translate>show more info</i18n.Translate> </a> )} - {/* {showDebugInfo && msg.debug && ( */} + {msg.debug && ( <pre class="whitespace-break-spaces text-black" style={{ - // display: showDebugInfo ? "block" : "none", + display: showDebugInfo ? "block" : "none", }} > {JSON.stringify( @@ -183,7 +179,7 @@ export function ToastBannerBulma(): VNode { 2, )} </pre> - {/* )} */} + )} </div> )} </article> @@ -243,4 +239,3 @@ function fromNodeToText(node: ChildNode | undefined) { } return result; } -